字節(jié)碼增強(qiáng)技術(shù)-Byte Buddy

為什么需要在運(yùn)行時生成代碼?

Java 是一個強(qiáng)類型語言系統(tǒng),要求變量和對象都有一個確定的類型,不兼容類型賦值都會造成轉(zhuǎn)換異常,通常情況下這種錯誤都會被編譯器檢查出來,如此嚴(yán)格的類型在大多數(shù)情況下是比較令人滿意的,這對構(gòu)建具有非常強(qiáng)可讀性和穩(wěn)定性的應(yīng)用有很大的幫助,這也是 Java 能在企業(yè)編程中的普及的一個原因之一。
然而,因?yàn)槠饛?qiáng)類型的檢查,限制了其他領(lǐng)域語言應(yīng)用范圍。比如在編寫一個框架是,通常我們并不知道應(yīng)用程序定義的類型,因?yàn)楫?dāng)這個庫被編譯時,我們還不知道這些類型,為了能在這種情況下能調(diào)用或者訪問應(yīng)用程序的方法或者變量,Java 類庫提供了一套反射 API。使用這套反射 API,我們就可以反省為知類型,進(jìn)而調(diào)用方法或者訪問屬性。但是,Java 反射有如下缺點(diǎn):

  • 需要執(zhí)行一個相當(dāng)昂貴的方法查找來獲取描述特定方法的對象,因此,相比硬編碼的方法調(diào)用,使用 反射 API 非常慢。
  • 反射 API 能繞過類型安全檢查,可能會因?yàn)槭褂貌划?dāng)照成意想不到的問題,這樣就錯失了 Java 編程語言的一大特性。

簡介

正如官網(wǎng)說的:Byte Buddy 是一個代碼生成和操作庫,用于在Java應(yīng)用程序運(yùn)行時創(chuàng)建和修改Java類,而無需編譯器的幫助。
除了Java類庫附帶的代碼生成實(shí)用程序外,Byte Buddy還允許創(chuàng)建任意類,并且不限于實(shí)現(xiàn)用于創(chuàng)建運(yùn)行時代理的接口。
此外,Byte Buddy提供了一種方便的API,可以使用Java代理或在構(gòu)建過程中手動更改類。
Byte Buddy 相比其他字節(jié)碼操作庫有如下優(yōu)勢:

  • 無需理解字節(jié)碼格式,即可操作,簡單易行的 API 能很容易操作字節(jié)碼。
  • 支持 Java 任何版本,庫輕量,僅取決于Java字節(jié)代碼解析器庫ASM的訪問者API,它本身不需要任何其他依賴項(xiàng)。
  • 比起JDK動態(tài)代理、cglib、Javassist,Byte Buddy在性能上具有優(yōu)勢。

性能

在選擇字節(jié)碼操作庫時,往往需要考慮庫本身的性能。對于許多應(yīng)用程序,生成代碼的運(yùn)行時特性更有可能確定最佳選擇。而在生成的代碼本身的運(yùn)行時間之外,用于創(chuàng)建動態(tài)類的運(yùn)行時也是一個問題。官網(wǎng)對庫進(jìn)行了性能測試,給出以下結(jié)果圖:


file

圖中的每一行分別為,類的創(chuàng)建、接口實(shí)現(xiàn)、方法調(diào)用、類型擴(kuò)展、父類方法調(diào)用的性能結(jié)果。從性能報告中可以看出,Byte Buddy 的主要側(cè)重點(diǎn)在于以最少的運(yùn)行時生成代碼,需要注意的是,我們這些衡量 Java 代碼性能的測試,都由 Java 虛擬機(jī)即時編譯器優(yōu)化過,如果你的代碼只是偶爾運(yùn)行,沒有得到虛擬機(jī)的優(yōu)化,可能性能會有所偏差。所以我們在使用 Byte Buddy 開發(fā)時,我們希望監(jiān)控這些指標(biāo),以避免在添加新功能時造成性能損失。

Hello world!

Class<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .method(ElementMatchers.named("toString"))
                .intercept(FixedValue.value("Hello World"))
                .make()
                .load(HelloWorldBuddy.class.getClassLoader())
                .getLoaded();

        Object instance = dynamicType.newInstance();
        String toString = instance.toString();
        System.out.println(toString);
        System.out.println(instance.getClass().getCanonicalName());

從例子中看到,操作創(chuàng)建一個類如此的簡單。正如 ByteBuddy 說明的,ByteBuddy 提供了一個領(lǐng)域特定語言,這樣就可以盡可能地提高人類可讀性簡單易行的 API,可能能讓你在初次使用的過程中就能不需要查閱 API 的前提下完成編碼。這也真是 ByteBuddy 能完爆其他同類型庫的一個原因。

上面的示例中使用的默認(rèn)ByteBuddy配置會以最新版本的類文件格式創(chuàng)建Java類,該類文件格式可以被正在處理的Java虛擬機(jī)理解。subclass 指定了新創(chuàng)建的類的父類,同時 method 指定了 ObjecttoString 方法,intercept 攔截了 toString 方法并返回固定的 value ,最后 make 方法生產(chǎn)字節(jié)碼,有類加載器加載到虛擬機(jī)中。

此外,Byte Buddy不僅限于創(chuàng)建子類和操作類,還可以轉(zhuǎn)換現(xiàn)有代碼。Byte Buddy 還提供了一個方便的 API,用于定義所謂的 Java 代理,該代理允許在任何 Java 應(yīng)用程序的運(yùn)行期間進(jìn)行代碼轉(zhuǎn)換,代理會在下篇單獨(dú)寫一篇文章講解。

創(chuàng)建一個類

任何一個由 ByteBuddy 創(chuàng)建的類型都是通過 ByteBuddy 類的實(shí)例來完成的。通過簡單地調(diào)用 new ByteBuddy() 就可以創(chuàng)建一個新實(shí)例。

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .make();

上面的示例代碼會創(chuàng)建一個繼承至 Object 類型的類。這個動態(tài)創(chuàng)建的類型與直接擴(kuò)展 Object 并且沒有實(shí)現(xiàn)任何方法、屬性和構(gòu)造函數(shù)的類型是等價的。該列子沒有命名動態(tài)生成的類型,但是在定義 Java 類時卻是必須的,所以很容易的你會想到,ByteBuddy 會有默認(rèn)的策略給我們生成。當(dāng)然,你也可以很容易地明確地命名這個類型。

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make();

那么默認(rèn)的策略是如何做的呢?這個將與 ByteBuddy 與 約定大于配置息息相關(guān),它提供了我們認(rèn)為比較全面的默認(rèn)配置。至于類型命名,ByteBuddy 的默認(rèn)配置提供了 NamingStrategy,它基于動態(tài)類型的超類名稱來隨機(jī)生成類名。此外,名稱定義在與父類相同的包下,這樣父類的包級訪問權(quán)限的方法對動態(tài)類型也可見。如果你將示例子類命名為 example.Foo,那么生成的名稱將會類似于 example.FooByteBuddy1376491271,這里的數(shù)字序列是隨機(jī)的。

此外,在一些需要指定類型的場景中,可以通過重寫 NamingStrategy 的方法來實(shí)現(xiàn),或者使用 ByteBuddy 內(nèi)置的NamingStrategy.SuffixingRandom 來實(shí)現(xiàn)。

同時需要注意的是,我們編碼時需要遵守所謂的領(lǐng)域特定語言和不變性原則,這是說明意思呢?就是說在 ByteBuddy 中,幾乎所有的類都被構(gòu)建成不可變的;極少數(shù)情況,我們不可能把對象構(gòu)建成不可變的。請看下面一個例子:

ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.with(new NamingStrategy.SuffixingRandom("suffix"));
DynamicType.Unloaded<?> dynamicType1 = byteBuddy.subclass(Object.class).make();

上述例子你會發(fā)現(xiàn)類的命名策略還是默認(rèn)的,其根本原因就是沒有遵守上述原則導(dǎo)致的。所以在編碼過程中要基于此原則進(jìn)行。

加載類

上節(jié)創(chuàng)建的 DynamicType.Unloaded,代表一個尚未加載的類,顧名思義,這些類型不會加載到 Java 虛擬機(jī)中,它僅僅表示創(chuàng)建好了類的字節(jié)碼,通過 DynamicType.Unloaded 中的 getBytes 方法你可以獲取到該字節(jié)碼,在你的應(yīng)用程序中,你可能需要將該字節(jié)碼保存到文件,或者注入的現(xiàn)在的 jar 文件中,因此該類型還提供了一個 saveIn(File) 方法,可以將類存儲在給定的文件夾中; inject(File) 方法將類注入到現(xiàn)有的 Jar 文件中,另外你只需要將該字節(jié)碼直接加載到虛擬機(jī)使用,你可以通過 ClassLoadingStrategy 來加載。

如果不指定ClassLoadingStrategy,Byte Buffer根據(jù)你提供的ClassLoader來推導(dǎo)出一個策略,內(nèi)置的策略定義在枚舉ClassLoadingStrategy.Default中

  • WRAPPER:創(chuàng)建一個新的Wrapping類加載器
  • CHILD_FIRST:類似上面,但是子加載器優(yōu)先負(fù)責(zé)加載目標(biāo)類
  • INJECTION:利用反射機(jī)制注入動態(tài)類型

示例

Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .make()
  .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

這樣我們創(chuàng)建并加載了一個類。我們使用 WRAPPER 策略來加載適合大多數(shù)情況的類。getLoaded 方法返回一個 Java Class 的實(shí)例,它就表示現(xiàn)在加載的動態(tài)類。

重新加載類

得益于JVM的HostSwap特性,已加載的類可以被重新定義:

// 安裝Byte Buddy的Agent,除了通過-javaagent靜態(tài)安裝,還可以:
ByteBuddyAgent.install();
Foo foo = new Foo();
new ByteBuddy()
  .redefine(Bar.class)
  .name(Foo.class.getName())
  .make()
  .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
assertThat(foo.m(), is("bar")); 

可以看到,即使時已經(jīng)存在的對象,也會受到類Reloading的影響。但是需要注意的是HostSwap具有限制:

  • 類再重新載入前后,必須具有相同的Schema,也就是方法、字段不能減少(可以增加)
  • 不支持具有靜態(tài)初始化塊的類

修改類

redefine

重定義一個類時,Byte Buddy 可以對一個已有的類添加屬性和方法,或者刪除已經(jīng)存在的方法實(shí)現(xiàn)。新添加的方法,如果簽名和原有方法一致,則原有方法會消失。

rebase

類似于redefine,但是原有的方法不會消失,而是被重命名,添加后綴 $original,這樣,就沒有實(shí)現(xiàn)會被丟失。重定義的方法可以繼續(xù)通過它們重命名過的名稱調(diào)用原來的方法,例如類:

class Foo {
  String bar() { return "bar"; }
}

rebase 之后:

class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}

方法攔截

通過匹配模式攔截

ByteBuddy 提供了很多用于匹配方法的 DSL,如下例子:

Foo dynamicFoo = new ByteBuddy()
  .subclass(Foo.class)
  // 匹配由Foo.class聲明的方法
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
  // 匹配名為foo的方法
  .method(named("foo")).intercept(FixedValue.value("Two!"))
  // 匹配名為foo,入?yún)?shù)量為1的方法
  .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

ByteBuddy 通過 net.bytebuddy.matcher.ElementMatcher 來定義配置策略,可以通過此接口實(shí)現(xiàn)自己定義的匹配策略。庫本身提供的 Matcher 非常多。

方法委托

使用MethodDelegation可以將方法調(diào)用委托給任意POJO。Byte Buddy不要求Source(被委托類)、Target類的方法名一致

class Source {
  public String hello(String name) { return null; }
}

class Target {
  public static String hello(String name) {
    return "Hello " + name + "!";
  }
}

String helloWorld = new ByteBuddy()
  .subclass(Source.class)
  .method(named("hello")).intercept(MethodDelegation.to(Target.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .hello("World");

其中 Target 還可以如下實(shí)現(xiàn):

class Target {
  public static String intercept(String name) { return "Hello " + name + "!"; }
  public static String intercept(int i) { return Integer.toString(i); }
  public static String intercept(Object o) { return o.toString(); }
}

前一個實(shí)現(xiàn)因?yàn)橹挥幸粋€方法,而且類型也匹配,很好理解,那么后一個呢,Byte Buddy到底會委托給哪個方法?Byte Buddy遵循一個最接近原則:

  • intercept(int)因?yàn)閰?shù)類型不匹配,直接Pass
  • 另外兩個方法參數(shù)都匹配,但是 intercept(String)類型更加接近,因此會委托給它

同時需要注意的是被攔截的方法需要聲明為 public,否則沒法進(jìn)行攔截增強(qiáng)。除此之外,還可以使用 @RuntimeType 注解來標(biāo)注方法

@RuntimeType
public static Object intercept(@RuntimeType Object value) {
        System.out.println("Invoked method with: " + value);
        return value;
}

參數(shù)綁定

可以在攔截器(Target)的攔截方法 intercept 中使用注解注入?yún)?shù),ByteBuddy 會根據(jù)注解給我們注入對于的參數(shù)值。比如:

void intercept(Object o1, Object o2)
// 等同于
void intercept(@Argument(0) Object o1, @Argument(1) Object o2)

常用的注解如下表:

注解 描述
@Argument 綁定單個參數(shù)
@AllArguments 綁定所有參數(shù)的數(shù)組
@This 當(dāng)前被攔截的、動態(tài)生成的那個對象
@DefaultCall 調(diào)用默認(rèn)方法而非super的方法
@SuperCall 用于調(diào)用父類版本的方法
@RuntimeType 可以用在返回值、參數(shù)上,提示ByteBuddy禁用嚴(yán)格的類型檢查
@Super 當(dāng)前被攔截的、動態(tài)生成的那個對象的父類對象
@FieldValue 注入被攔截對象的一個字段的值

字段屬性

public class UserType {
  public String doSomething() { return null; }
}

public interface Interceptor {
  String doSomethingElse();
}

public interface InterceptionAccessor {
  Interceptor getInterceptor();
  void setInterceptor(Interceptor interceptor);
}

public interface InstanceCreator {
  Object makeInstance();
}

public class HelloWorldInterceptor implements Interceptor {
  @Override
  public String doSomethingElse() {
    return "Hello World!";
  }
}

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
    .method(not(isDeclaredBy(Object.class))) // 非父類 Object 聲明的方法
    .intercept(MethodDelegation.toField("interceptor")) // 攔截委托給屬性字段 interceptor
  .defineField("interceptor", Interceptor.class, Visibility.PRIVATE) // 定義一個屬性字段
  .implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty()) // 實(shí)現(xiàn) InterceptionAccessor 接口
  .make()
  .load(getClass().getClassLoader())
  .getLoaded();
    
InstanceCreator factory = new ByteBuddy()
  .subclass(InstanceCreator.class)
    .method(not(isDeclaredBy(Object.class))) // 非父類 Object 聲明的方法
    .intercept(MethodDelegation.toConstructor(dynamicUserType)) // 委托攔截的方法來調(diào)用提供的類型的構(gòu)造函數(shù)
  .make()
  .load(dynamicUserType.getClassLoader())
  .getLoaded().newInstance();

UserType userType = (UserType) factory.makeInstance();
((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());
String s = userType.doSomething();
System.out.println(s); // Hello World!

上述例子將 UserType 類實(shí)現(xiàn)了 InterceptionAccessor 接口,同時使用 MethodDelegation.toField 可以使攔截的方法可以委托給新增的字段。

End

本文是自己學(xué)習(xí) ByteBuddy 后自己稍加整理的基礎(chǔ)教程。最后感謝你閱讀?。?!

微信公眾號關(guān)注:ByteZ,獲取更多學(xué)習(xí)資料

file
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容