字節碼增強技術,不止有 Java Proxy、 Cglib 和 Javassist 還有 Byte Buddy
提到字節碼增強技術,相信用過 Spring 的小伙伴都會知道 Java Proxy 和 Cglib。
畢竟面試準備的八股文中說過,Spring 的動態代理有兩種實現方式,在有接口存在的時候使用 Java Proxy,當沒有接口的時候使用的是 Cglib。
這兩種方式的區別不在本文的討論范圍之內,今天想給大家介紹了是另一個字節碼增強技術 Byte Buddy。
Byte Buddy
根據 Byte Buddy 官網所說,Byte Buddy 是一個代碼生成和操作庫,用于在 Java 應用程序運行時創建和修改 Java 類,而無需編譯器的幫助。
Byte Buddy 提供一套簡單易用的 API,可以很方便的使用 Java 流式編程的形式來動態創建類或者創建接口的實現類,這一點跟 Java Proxy 和 Cglib 不一樣。
使用 Byte Buddy 的方式也非常簡單,只要直接引入 Maven 依賴即可,沒有其他繁瑣的依賴。總的來說,使用 Byte Buddy 有下面的優勢:
- 無需理解字節碼格式,簡單易用的 API 能很容易操作字節碼;
- 支持 Java 任何版本,庫輕量,僅取決于 Java 字節代碼解析器庫 ASM 的訪問者 API,它本身不需要任何其他依賴項。
- 比起 JDK 動態代理、cglib、Javassist,Byte Buddy 在性能上具有優勢。
圖片
這一份測試報告是官網提供的,表中的每一行分別為,類的創建、接口實現、方法調用、類型擴展、父類方法調用的性能結果。
從性能報告中可以看出,Byte Buddy 在一些場景是有優勢的,但是在有些場景也不見得特別有優勢,不過整體來看還是不錯的。
測試
說了那么多,下面給大家演示一下,如果使用 Byte Buddy,首先我們需要引入 Maven 依賴,我這里用的版本是 1.14.6,也可以使用其他版本。
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.6</version>
</dependency>
創建一個類,并覆蓋 toString
public static void test1() {
try {
Class<?> dynamicType = new ByteBuddy().
subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(ByteBuddyDemo.class.getClassLoader())
.getLoaded();
System.out.println(dynamicType.newInstance().toString());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
public static void test2() {
try {
DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make();
DynamicType.Loaded<Object> load = unloaded.load(ByteBuddyDemo.class.getClassLoader());
System.out.println(load.getLoaded().newInstance().toString());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
整個代碼的思路是通過 Byte Buddy,構造出一個 Class 對象,然后調用 Class 對象的 newInstance() 方法,再執行 toString() 方法。上面兩個方式的功能是一樣的,寫出來更方便大家理解。
其中各個方法的含義如下:
subClass:表示構造的類是 Object 的子類;
method:表示要構造的具體方法,類似于過濾的功能;
intercept:表示對過濾后的方法進行攔截;
FixedValue.value("Hello World!"):表示構造返回一個”Hello World!“ 字符串;
make:創建 DynamicType.Unloaded 對象,此時這個對象被構造出來,但是還沒有被 JVM 加載,還不能使用;
load,getLoaded:加載當前類的構造器,并進行加載;
等到加載到 JVM 過后,就可以使用 newInstance().toString() 進行調用了。
代理方法
上面的例子是創建一個簡單的類和方法,下面我們介紹一個代理方法的使用,這里我們有一個目標類 Target 和一個方法 saySomething() 方法,有一個代理類 Agent,里面有一個代理方法 agentSaySomething(),如下所示:
public class Target {
public String saySomething() {
return "Hello target";
}
}
public class Agent {
public static String agentSaySomething() {
System.out.println("agentSaySomething");
return "hello agent";
}
}
public static void test4() {
try {
DynamicType.Unloaded<Target> agent = new ByteBuddy()
.subclass(Target.class)
.method(named("saySomething")
.and(isDeclaredBy(Target.class)
.and(returns(String.class))))
.intercept(MethodDelegation.to(Agent.class))
.make();
// 將 agent 字節碼寫入文件中
outputClazz(agent.getBytes());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void outputClazz(byte[] bytes) {
FileOutputStream out = null;
try {
String pathName = ByteBuddyDemo.class.getResource("/").getPath() + "AgentTarget.class";
out = new FileOutputStream(new File(pathName));
System.out.println("類輸出路徑:" + pathName);
out.write(bytes);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
test4();
}
運行過后我們可以看到生成了一個 class 文件,通過查看代碼如下,可以看到是創建了一個 Target 的子類,并且調用了 Agent 的 agentSaySomething 方法。
圖片
總結
Byte Buddy的 API 很豐富,這里只是很簡單的給大家使用了幾個 API,還有包括方法,字段的設定等等,感興趣的小伙伴可以繼續去學習學習。