字節碼增強技術-Byte Buddy


本文轉載自字節碼增強技術-Byte Buddy

為什么需要在運行時生成代碼?

Java 是一個強類型語言系統,要求變量和對象都有一個確定的類型,不兼容類型賦值都會造成轉換異常,通常情況下這種錯誤都會被編譯器檢查出來,如此嚴格的類型在大多數情況下是比較令人滿意的,這對構建具有非常強可讀性和穩定性的應用有很大的幫助,這也是 Java 能在企業編程中的普及的一個原因之一。然而,因為起強類型的檢查,限制了其他領域語言應用范圍。比如在編寫一個框架是,通常我們並不知道應用程序定義的類型,因為當這個庫被編譯時,我們還不知道這些類型,為了能在這種情況下能調用或者訪問應用程序的方法或者變量,Java 類庫提供了一套反射 API。使用這套反射 API,我們就可以反省為知類型,進而調用方法或者訪問屬性。但是,Java 反射有如下缺點:

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

簡介

正如官網說的:Byte Buddy 是一個代碼生成和操作庫,用於在Java應用程序運行時創建和修改Java類,而無需編譯器的幫助。除了Java類庫附帶的代碼生成實用程序外,Byte Buddy還允許創建任意類,並且不限於實現用於創建運行時代理的接口。此外,Byte Buddy提供了一種方便的API,可以使用Java代理或在構建過程中手動更改類。Byte Buddy 相比其他字節碼操作庫有如下優勢:

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

性能

在選擇字節碼操作庫時,往往需要考慮庫本身的性能。對於許多應用程序,生成代碼的運行時特性更有可能確定最佳選擇。而在生成的代碼本身的運行時間之外,用於創建動態類的運行時也是一個問題。官網對庫進行了性能測試,給出以下結果圖:file

圖中的每一行分別為,類的創建、接口實現、方法調用、類型擴展、父類方法調用的性能結果。從性能報告中可以看出,Byte Buddy 的主要側重點在於以最少的運行時生成代碼,需要注意的是,我們這些衡量 Java 代碼性能的測試,都由 Java 虛擬機即時編譯器優化過,如果你的代碼只是偶爾運行,沒有得到虛擬機的優化,可能性能會有所偏差。所以我們在使用 Byte Buddy 開發時,我們希望監控這些指標,以避免在添加新功能時造成性能損失。

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());

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

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

此外,Byte Buddy不僅限於創建子類和操作類,還可以轉換現有代碼。Byte Buddy 還提供了一個方便的 API,用於定義所謂的 Java 代理,該代理允許在任何 Java 應用程序的運行期間進行代碼轉換,代理會在下篇單獨寫一篇文章講解。

創建一個類

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

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

上面的示例代碼會創建一個繼承至 Object 類型的類。這個動態創建的類型與直接擴展 Object 並且沒有實現任何方法、屬性和構造函數的類型是等價的。該列子沒有命名動態生成的類型,但是在定義 Java 類時卻是必須的,所以很容易的你會想到,ByteBuddy 會有默認的策略給我們生成。當然,你也可以很容易地明確地命名這個類型。

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

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

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

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

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

上述例子你會發現類的命名策略還是默認的,其根本原因就是沒有遵守上述原則導致的。所以在編碼過程中要基於此原則進行。

加載類

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

如果不指定ClassLoadingStrategy,Byte Buffer根據你提供的ClassLoader來推導出一個策略,內置的策略定義在枚舉ClassLoadingStrategy.Default中

  • WRAPPER:創建一個新的Wrapping類加載器
  • CHILD_FIRST:類似上面,但是子加載器優先負責加載目標類
  • INJECTION:利用反射機制注入動態類型

示例

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

這樣我們創建並加載了一個類。我們使用 WRAPPER 策略來加載適合大多數情況的類。getLoaded 方法返回一個 Java Class 的實例,它就表示現在加載的動態類。

重新加載類

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

// 安裝Byte Buddy的Agent,除了通過-javaagent靜態安裝,還可以:
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"));    

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

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

修改類

redefine

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

rebase

類似於redefine,但是原有的方法不會消失,而是被重命名,添加后綴 $original,這樣,就沒有實現會被丟失。重定義的方法可以繼續通過它們重命名過的名稱調用原來的方法,例如類:

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,入參數量為1的方法
  .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

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

方法委托

使用MethodDelegation可以將方法調用委托給任意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 還可以如下實現:

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(); }
}

前一個實現因為只有一個方法,而且類型也匹配,很好理解,那么后一個呢,Byte Buddy到底會委托給哪個方法?Byte Buddy遵循一個最接近原則:

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

同時需要注意的是被攔截的方法需要聲明為 public,否則沒法進行攔截增強。除此之外,還可以使用 @RuntimeType 注解來標注方法

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

參數綁定

可以在攔截器(Target)的攔截方法 intercept 中使用注解注入參數,ByteBuddy 會根據注解給我們注入對於的參數值。比如:

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

常用的注解如下表:

注解 描述
@Argument 綁定單個參數
@AllArguments 綁定所有參數的數組
@This 當前被攔截的、動態生成的那個對象
@DefaultCall 調用默認方法而非super的方法
@SuperCall 用於調用父類版本的方法
@RuntimeType 可以用在返回值、參數上,提示ByteBuddy禁用嚴格的類型檢查
@Super 當前被攔截的、動態生成的那個對象的父類對象
@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()) // 實現 InterceptionAccessor 接口
  .make()
  .load(getClass().getClassLoader())
  .getLoaded();
    
InstanceCreator factory = new ByteBuddy()
  .subclass(InstanceCreator.class)
    .method(not(isDeclaredBy(Object.class))) // 非父類 Object 聲明的方法
    .intercept(MethodDelegation.toConstructor(dynamicUserType)) // 委托攔截的方法來調用提供的類型的構造函數
  .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 類實現了 InterceptionAccessor 接口,同時使用 MethodDelegation.toField 可以使攔截的方法可以委托給新增的字段。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM