ByteBuddy代碼生成技術


簡介

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

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

創建類

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
    Class<?> dynamicType = new ByteBuddy()
            .subclass(Object.class)
            .method(ElementMatchers.named("toString"))
            .intercept(FixedValue.value("Hello World"))
            .make()
            .load(HelloByteBuddy.class.getClassLoader())
            .getLoaded();

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

Hello World
net.bytebuddy.renamed.java.lang.Object$ByteBuddy$4oGQtGr3

上面的例子中創建了一個新的類型(在輸出中可以看到相應的類名),繼承自Object類型,並覆寫了它的toString方法,返回一個固定值,api的可讀性很高

  • subclass指定了新創建的類的父類
  • method 指定了需要攔截的方法
  • intercept攔截了toString方法並返回固定的value,最后make方法產生字節碼,由類加載器加載到java虛擬機中

方法攔截

上面的例子是攔截了toString方法到一個FixedValue實現,實際使用中可能會實現一些更復雜的場景。Byte Buddy提供了MethodDelegation方法,可以將源方法的調用委托給任意一個POJO對象

假設target對象的實現

public class GreetingInterceptor {
  public Object greet(Object argument) {
    return "Hello from " + argument;
  }
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        Class<? extends java.util.function.Function> dynamicType = new ByteBuddy()
                .subclass(java.util.function.Function.class)
                .method(ElementMatchers.named("apply"))
                .intercept(MethodDelegation.to(new GreetingInterceptor()))
                .make()
                .load(MethodDelegationTest.class.getClassLoader())
                .getLoaded();

        System.out.println((String) dynamicType.newInstance().apply("Byte Buddy"));
    }

    public static class GreetingInterceptor {
        public Object greet(Object argument) {
            return "Hello from " + argument;
        }
    }
Hello from Byte Buddy

將java.util.function.Function的apply方法代理到了GreetingInterceptor的greet方法上,這里代理的時候查找的greet方法是通過返回值和參數來確認的,並不依賴方法名一致,如果有兩個返回值和參數一致的方法就會產生歧義,無法正確的代理。

攔截器還可以通過注解定義接收更多的參數,以下攔截方法會在攔截到一個Funcition:apply方法后,將原方法的參數以及原方法的Method對象傳入intercept方法,在intercept中實現一些自定義的邏輯。在方法上@RuntimeType注解的作用是會通知ByteBuddy在最終會將返回值cast成被攔截的方法的返回值類型。

public class GeneralInterceptor {
  @RuntimeType
  public Object intercept(@AllArguments Object[] allArguments,
                          @Origin Method method) {
    // intercept any method of any signature
  }
}

其他注解:
@SuperCall 傳入的是一個Callable類型,可以在被代理類之外調用原方法
@Argument(0) 方法調用的第一個參數,可以使用0-n標記
@This 表示調用方法的原始對象
@AllArguments 被AllArguments標注的參數需要是一個數組類型,並且原參數的類型都要能和數組的類型兼容,

原生支持的注解還有很多,ByteBuddy會根據注解給我們注入相應的參數,可以參閱官方文檔了解更多可以使用的注解,同時還能支持自定義的注解形式。
並且這里值得注意的是,雖然在GeneralInterceptor類中使用了bytebuddy中的注解,但是在生成新的子類的時候這些注解都會被忽略,保持生成的代碼並不依賴bytebuddy框架。
關於攔截方法的選擇上,ByteBuddy不要求Source(被委托的類)和target類的方法名一致,而是通過最接近原則去選取最合適的方法,主要是針對方法參數類型,方法返回值類型,如果存在歧義會報錯,也可以通過注解定義優先級。

Java agent

Bytebuddy不僅能通過api創建新的類,還能夠修改現有類,在不修改源代碼的情況下,做一些侵入,實現一些特定功能。通過java agent可以在main函數之前修改已經存在的類定義,以下的例子是對所有的以Timed結尾的方法實現打印方法執行耗時

代理方法

public class TimingInterceptor {
  @RuntimeType
  public static Object intercept(@Origin Method method, 
                                 @SuperCall Callable<?> callable) {
    long start = System.currentTimeMillis();
    try {
      return callable.call();
    } finally {
      System.out.println(method + " took " + (System.currentTimeMillis() - start));
    }
  }
}

定義premain方法

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

通過maven插件,指定premain的mainfest屬性

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-jar-plugin</artifactId>
	<configuration>
		<archive>
			<manifestEntries>
				<Premain-Class>com.aitozi.bytebuddy.TimerAgent</Premain-Class>
			</manifestEntries>
		</archive>
	</configuration>
</plugin>

在啟動java進程時通過加上以下參數:-javaagent:timingagent.jar,這樣在啟動后所有的以Timed結尾的方法都被注入會打印相應的執行耗時。

重新加載類

除了通過agent實現啟動前redefine class。利用jvm hotswap的特性,已經加載的類也可以被重新定義,通常這樣可以很方便的編寫測試,直接修改類的行為來模擬攔截情況

class Foo {
  String m() { return "foo"; }
}
 
class Bar {
  String m() { return "bar"; }
}

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

使用場景

通過這種非常方便的字節碼生成技術,可以做一些有意思的功能,比如以上例子中,不修改源碼計算某些方法的耗時。我注意到這個框架主要是因為在blink中也使用了這個lib。
在blink中目前支持sql,datastream,和tableapi作業,作業的資源都是在平台上在執行計划上設置每個節點的資源。
對於sql作業的執行計划的生成其實是引擎代碼的邏輯,可以直接拿到用戶在平台設置的內存和cpu參數設置到每一個sql節點上,但是對於datastream作業由於streamgraph的生成過程是在用戶代碼的main函數中,需要侵入用戶代碼,這就有了byte buddy的用武之地。通過字節碼修改技術可以在用戶的main函數執行之前,攔截transformation以及StreamNode構造方法,在創建這些方法的地方注入用戶在平台上設置的每個計算節點的資源值,達到通過平台設置用戶作業資源的目的

參考

官方文檔
官網的翻譯
深入理解instrument
JVM源碼分析之javaagent原理完全解讀
https://juejin.im/post/5da2fd6a6fb9a04e23576dd4


免責聲明!

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



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