Byte Buddy學習筆記


本文轉載自Byte Buddy學習筆記

簡介

Byte Buddy是一個JVM的運行時代碼生成器,你可以利用它創建任何類,且不像JDK動態代理那樣強制實現一個接口。Byte Buddy還提供了簡單的API,便於手工、通過Java Agent,或者在構建期間修改字節碼。
Java反射API可以做很多和字節碼生成器類似的工作,但是它具有以下缺點:

  1. 相比硬編碼的方法調用,使用 反射 API 非常慢
  2. 反射 API 能繞過類型安全檢查
    比起JDK動態代理、cglib、Javassist,Byte Buddy在性能上具有優勢。

入門

創建新類型

下面是一個最簡單的例子:

Class<?> dynamicType = new ByteBuddy()
  // 指定父類
  .subclass(Object.class)
   // 根據名稱來匹配需要攔截的方法
  .method(ElementMatchers.named("toString"))
  // 攔截方法調用,返回固定值
  .intercept(FixedValue.value("Hello World!"))
  // 產生字節碼
  .make()
  // 加載類
  .load(getClass().getClassLoader())
  // 獲得Class對象
  .getLoaded();
 
assertThat(dynamicType.newInstance().toString(), is("Hello World!"));

ByteBuddy利用Implementation接口來表示一個動態定義的方法,FixedValue.value就是該接口的實例。

完全實現Implementation比較繁瑣,因此實際情況下會使用MethodDelegation代替。使用MethodDelegation,你可以在一個POJO中實現方法攔截器:

public class GreetingInterceptor {
  // 方法簽名隨意
  public Object greet(Object argument) {
    return "Hello from " + argument;
  }
}
 
Class<? extends java.util.function.Function> dynamicType = new ByteBuddy()
  // 實現一個Function子類
  .subclass(java.util.function.Function.class)
  .method(ElementMatchers.named("apply"))
  // 攔截Function.apply調用,委托給GreetingInterceptor處理
  .intercept(MethodDelegation.to(new GreetingInterceptor()))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded();
 
assertThat((String) dynamicType.newInstance().apply("Byte Buddy"), is("Hello from Byte Buddy"));

編寫攔截器時,你可以指定一些注解,ByteBuddy會自動注入:

public class GeneralInterceptor {
  // 提示ByteBuddy根據被攔截方法的實際類型,對此攔截器的返回值進行Cast
  @RuntimeType
  //                      所有入參的數組
  public Object intercept(@AllArguments Object[] allArguments,
  //                      被攔截的原始方法
                          @Origin Method method) {
  }
}

修改已有類型

上面的兩個例子中,我們利用ByteBuddy創建了指定接口的新子類型,ByteBuddy也可以用來修改已存在的。

ByteBuddy提供了便捷的創建Java Agent的API,本節的例子就是通過Java Agent方式來修改已存在的Java類型:public class TimerAgent {

public static void premain(String arguments, 
                             Instrumentation instrumentation) {
    new AgentBuilder.Default()
      // 匹配被攔截方法
      .type(ElementMatchers.nameEndsWith("Timed"))
      .transform(
          (builder, type, classLoader, module) -> 
              builder.method(ElementMatchers.any()) .intercept(MethodDelegation.to(TimingInterceptor.class))
      ).installOn(instrumentation);
  }
}
 
public class TimingInterceptor {
  @RuntimeType
  public static Object intercept(@Origin Method method, 
                                 // 調用該注解后的Runnable/Callable,會導致調用被代理的非抽象父方法
                                 @SuperCall Callable<?> callable) {
    long start = System.currentTimeMillis();
    try {
      return callable.call();
    } finally {
      System.out.println(method + " took " + (System.currentTimeMillis() - start));
    }
  }
}

API

創建類

subclass

調用此方法可以創建一個目標類的子類:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")  // 子類的名稱
  .make();

如果不指定子類名稱,Byte Buddy會有一套自動的策略來生成。你還可以指定子類命名策略:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .with(new NamingStrategy.AbstractBase() {
    @Override
    public String subclass(TypeDescription superClass) {
        return "i.love.ByteBuddy." + superClass.getSimpleName();
    }
  })
  .subclass(Object.class)
  .make();

加載類

上節創建的DynamicType.Unloaded,代表一個尚未加載的類,你可以通過ClassLoadingStrategy來加載這種類。

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

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

示例:

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

修改類

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"; }
}

重新加載類

得益於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具有限制:

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

操控未加載類

Byte Buddy提供了類似於Javassist的、操控未加載類的API。它在TypePool中維護類型的元數據TypeDescription:

// 獲取默認類型池
TypePool typePool = TypePool.Default.ofClassPath();
new ByteBuddy()
  .redefine(typePool.describe("foo.Bar").resolve(), // 根據名稱進行解析類
            // ClassFileLocator用於定位到被修改類的.class文件
            ClassFileLocator.ForClassLoader.ofClassPath())
  .defineField("qux", String.class) // 定義一個新的字段
  .make()
  .load(ClassLoader.getSystemClassLoader());
assertThat(Bar.class.getDeclaredField("qux"), notNullValue());

攔截方法

匹配方法

Byte Buddy提供了很多用於匹配方法的DSL:

class Foo {
  public String bar() { return null; }
  public String foo() { return null; }
  public String foo(Object o) { return null; }
}
 
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();

委托方法

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

class Source {
  public String hello(String name) { return null; }
}
 
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 hello(String name) {
    return "Hello " + name + "!";
  }
} 

也可以如下:

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遵循一個最接近原則:

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

參數綁定

你可以在Target的方法中使用注解進行參數綁定:

void foo(Object o1, Object o2)
// 等價於
void foo(@Argument(0) Object o1, @Argument(1) Object o2)

全部注解如下表

注解 說明
@Argument 綁定單個參數
@AllArguments 綁定所有參數的數組
@This 當前被攔截的、動態生成的那個對象
@Super 當前被攔截的、動態生成的那個對象的父類對象
@Origin 可以綁定到以下類型的參數:Method 被調用的原始方法 Constructor 被調用的原始構造器 Class 當前動態創建的類 MethodHandle MethodType String 動態類的toString()的返回值 int 動態方法的修飾符
@DefaultCall 調用默認方法而非super的方法
@SuperCall 用於調用父類版本的方法
@Super 注入父類型對象,可以是接口,從而調用它的任何方法
@RuntimeType 可以用在返回值、參數上,提示ByteBuddy禁用嚴格的類型檢查
@Empty 注入參數的類型的默認值
@StubValue 注入一個存根值。對於返回引用、void的方法,注入null;對於返回原始類型的方法,注入0
@FieldValue 注入被攔截對象的一個字段的值
@Morph 類似於@SuperCall,但是允許指定調用參數

添加字段

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
  .defineField("interceptor", Interceptor.class, Visibility.PRIVATE);

方法調用也可以委托給字段(而非外部對象):

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.toField("interceptor"));  


免責聲明!

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



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