天天看點

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

本文來自部落格園,作者:Aitozi,轉載請注明原文連結:https://www.cnblogs.com/Aitozi/p/15707976.html