本文轉載自字節碼增強技術-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在性能上具有優勢。
性能
在選擇字節碼操作庫時,往往需要考慮庫本身的性能。對於許多應用程序,生成代碼的運行時特性更有可能確定最佳選擇。而在生成的代碼本身的運行時間之外,用於創建動態類的運行時也是一個問題。官網對庫進行了性能測試,給出以下結果圖:
圖中的每一行分別為,類的創建、接口實現、方法調用、類型擴展、父類方法調用的性能結果。從性能報告中可以看出,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
指定了 Object
的 toString
方法,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
可以使攔截的方法可以委托給新增的字段。