為什么需要在運行時生成代碼业岁?
Java 是一個強類型語言系統(tǒng)充蓝,要求變量和對象都有一個確定的類型,不兼容類型賦值都會造成轉(zhuǎn)換異常纬乍,通常情況下這種錯誤都會被編譯器檢查出來,如此嚴格的類型在大多數(shù)情況下是比較令人滿意的裸卫,這對構(gòu)建具有非常強可讀性和穩(wěn)定性的應(yīng)用有很大的幫助仿贬,這也是 Java 能在企業(yè)編程中的普及的一個原因之一。然而墓贿,因為起強類型的檢查茧泪,限制了其他領(lǐng)域語言應(yīng)用范圍退个。比如在編寫一個框架是,通常我們并不知道應(yīng)用程序定義的類型调炬,因為當這個庫被編譯時,我們還不知道這些類型舱馅,為了能在這種情況下能調(diào)用或者訪問應(yīng)用程序的方法或者變量缰泡,Java 類庫提供了一套反射 API。使用這套反射 API代嗤,我們就可以反省為知類型棘钞,進而調(diào)用方法或者訪問屬性。但是干毅,Java 反射有如下缺點:
- 需要執(zhí)行一個相當昂貴的方法查找來獲取描述特定方法的對象宜猜,因此,相比硬編碼的方法調(diào)用硝逢,使用 反射 API 非常慢姨拥。
- 反射 API 能繞過類型安全檢查,可能會因為使用不當照成意想不到的問題渠鸽,這樣就錯失了 Java 編程語言的一大特性叫乌。
簡介
正如官網(wǎng)說的:Byte Buddy 是一個代碼生成和操作庫,用于在Java應(yīng)用程序運行時創(chuàng)建和修改Java類徽缚,而無需編譯器的幫助憨奸。除了Java類庫附帶的代碼生成實用程序外,Byte Buddy還允許創(chuàng)建任意類凿试,并且不限于實現(xiàn)用于創(chuàng)建運行時代理的接口排宰。此外,Byte Buddy提供了一種方便的API那婉,可以使用Java代理或在構(gòu)建過程中手動更改類板甘。Byte Buddy 相比其他字節(jié)碼操作庫有如下優(yōu)勢:
- 無需理解字節(jié)碼格式,即可操作详炬,簡單易行的 API 能很容易操作字節(jié)碼虾啦。
- 支持 Java 任何版本,庫輕量痕寓,僅取決于Java字節(jié)代碼解析器庫ASM的訪問者API傲醉,它本身不需要任何其他依賴項。
- 比起JDK動態(tài)代理呻率、cglib硬毕、Javassist,Byte Buddy在性能上具有優(yōu)勢礼仗。
性能
在選擇字節(jié)碼操作庫時吐咳,往往需要考慮庫本身的性能逻悠。對于許多應(yīng)用程序,生成代碼的運行時特性更有可能確定最佳選擇韭脊。而在生成的代碼本身的運行時間之外童谒,用于創(chuàng)建動態(tài)類的運行時也是一個問題。官網(wǎng)對庫進行了性能測試沪羔,給出以下結(jié)果圖:
圖中的每一行分別為饥伊,類的創(chuàng)建、接口實現(xiàn)蔫饰、方法調(diào)用琅豆、類型擴展、父類方法調(diào)用的性能結(jié)果篓吁。從性能報告中可以看出茫因,Byte Buddy 的主要側(cè)重點在于以最少的運行時生成代碼,需要注意的是杖剪,我們這些衡量 Java 代碼性能的測試冻押,都由 Java 虛擬機即時編譯器優(yōu)化過,如果你的代碼只是偶爾運行盛嘿,沒有得到虛擬機的優(yōu)化翼雀,可能性能會有所偏差。所以我們在使用 Byte Buddy 開發(fā)時孩擂,我們希望監(jiān)控這些指標狼渊,以避免在添加新功能時造成性能損失。
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());復制代碼
從例子中看到类垦,操作創(chuàng)建一個類如此的簡單狈邑。正如 ByteBuddy
說明的,ByteBuddy
提供了一個領(lǐng)域特定語言蚤认,這樣就可以盡可能地提高人類可讀性簡單易行的 API米苹,可能能讓你在初次使用的過程中就能不需要查閱 API 的前提下完成編碼。這也真是 ByteBuddy
能完爆其他同類型庫的一個原因砰琢。
上面的示例中使用的默認ByteBuddy配置會以最新版本的類文件格式創(chuàng)建Java類蘸嘶,該類文件格式可以被正在處理的Java虛擬機理解。subclass
指定了新創(chuàng)建的類的父類陪汽,同時 method
指定了 Object
的 toString
方法训唱,intercept
攔截了 toString
方法并返回固定的 value ,最后 make
方法生產(chǎn)字節(jié)碼挚冤,有類加載器加載到虛擬機中况增。
此外,Byte Buddy不僅限于創(chuàng)建子類和操作類训挡,還可以轉(zhuǎn)換現(xiàn)有代碼澳骤。Byte Buddy 還提供了一個方便的 API歧强,用于定義所謂的 Java 代理,該代理允許在任何 Java 應(yīng)用程序的運行期間進行代碼轉(zhuǎn)換为肮,代理會在下篇單獨寫一篇文章講解摊册。
創(chuàng)建一個類
任何一個由 ByteBuddy
創(chuàng)建的類型都是通過 ByteBuddy
類的實例來完成的。通過簡單地調(diào)用 new ByteBuddy()
就可以創(chuàng)建一個新實例颊艳。
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.make();復制代碼
上面的示例代碼會創(chuàng)建一個繼承至 Object 類型的類茅特。這個動態(tài)創(chuàng)建的類型與直接擴展 Object 并且沒有實現(xiàn)任何方法、屬性和構(gòu)造函數(shù)的類型是等價的籽暇。該列子沒有命名動態(tài)生成的類型,但是在定義 Java 類時卻是必須的饭庞,所以很容易的你會想到戒悠,ByteBuddy
會有默認的策略給我們生成。當然舟山,你也可以很容易地明確地命名這個類型绸狐。
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("example.Type")
.make();復制代碼
那么默認的策略是如何做的呢?這個將與 ByteBuddy
與 約定大于配置息息相關(guān)累盗,它提供了我們認為比較全面的默認配置寒矿。至于類型命名,ByteBuddy
的默認配置提供了 NamingStrategy若债,它基于動態(tài)類型的超類名稱來隨機生成類名符相。此外,名稱定義在與父類相同的包下蠢琳,這樣父類的包級訪問權(quán)限的方法對動態(tài)類型也可見啊终。如果你將示例子類命名為 example.Foo,那么生成的名稱將會類似于 example.FooByteBuddy1376491271傲须,這里的數(shù)字序列是隨機的蓝牲。
此外,在一些需要指定類型的場景中泰讽,可以通過重寫 NamingStrategy
的方法來實現(xiàn)例衍,或者使用 ByteBuddy
內(nèi)置的NamingStrategy.SuffixingRandom 來實現(xiàn)。
同時需要注意的是已卸,我們編碼時需要遵守所謂的領(lǐng)域特定語言和不變性
原則佛玄,這是說明意思呢?就是說在 ByteBuddy
中累澡,幾乎所有的類都被構(gòu)建成不可變的翎嫡;極少數(shù)情況,我們不可能把對象構(gòu)建成不可變的永乌。請看下面一個例子:
ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.with(new NamingStrategy.SuffixingRandom("suffix"));
DynamicType.Unloaded<?> dynamicType1 = byteBuddy.subclass(Object.class).make();復制代碼
上述例子你會發(fā)現(xiàn)類的命名策略還是默認的惑申,其根本原因就是沒有遵守上述原則導致的具伍。所以在編碼過程中要基于此原則進行。
加載類
上節(jié)創(chuàng)建的 DynamicType.Unloaded
圈驼,代表一個尚未加載的類人芽,顧名思義,這些類型不會加載到 Java 虛擬機中绩脆,它僅僅表示創(chuàng)建好了類的字節(jié)碼萤厅,通過 DynamicType.Unloaded
中的 getBytes
方法你可以獲取到該字節(jié)碼,在你的應(yīng)用程序中靴迫,你可能需要將該字節(jié)碼保存到文件惕味,或者注入的現(xiàn)在的 jar 文件中,因此該類型還提供了一個 saveIn(File)
方法玉锌,可以將類存儲在給定的文件夾中名挥; inject(File)
方法將類注入到現(xiàn)有的 Jar 文件中,另外你只需要將該字節(jié)碼直接加載到虛擬機使用主守,你可以通過 ClassLoadingStrategy
來加載禀倔。
如果不指定ClassLoadingStrategy,Byte Buffer根據(jù)你提供的ClassLoader來推導出一個策略参淫,內(nèi)置的策略定義在枚舉ClassLoadingStrategy.Default中
- WRAPPER:創(chuàng)建一個新的Wrapping類加載器
- CHILD_FIRST:類似上面救湖,但是子加載器優(yōu)先負責加載目標類
- INJECTION:利用反射機制注入動態(tài)類型
示例
Class<?> type = new ByteBuddy()
.subclass(Object.class)
.make()
.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();復制代碼
這樣我們創(chuàng)建并加載了一個類。我們使用 WRAPPER 策略來加載適合大多數(shù)情況的類涎才。getLoaded
方法返回一個 Java Class 的實例鞋既,它就表示現(xiàn)在加載的動態(tài)類。
重新加載類
得益于JVM的HostSwap特性耍铜,已加載的類可以被重新定義:
// 安裝Byte Buddy的Agent涛救,除了通過-javaagent靜態(tài)安裝,還可以:
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")); 復制代碼
可以看到业扒,即使時已經(jīng)存在的對象检吆,也會受到類Reloading的影響。但是需要注意的是HostSwap具有限制:
- 類再重新載入前后程储,必須具有相同的Schema蹭沛,也就是方法、字段不能減少(可以增加)
- 不支持具有靜態(tài)初始化塊的類
修改類
redefine
重定義一個類時章鲤,Byte Buddy 可以對一個已有的類添加屬性和方法摊灭,或者刪除已經(jīng)存在的方法實現(xiàn)。新添加的方法败徊,如果簽名和原有方法一致帚呼,則原有方法會消失。
rebase
類似于redefine,但是原有的方法不會消失煤杀,而是被重命名眷蜈,添加后綴 $original,這樣沈自,就沒有實現(xiàn)會被丟失酌儒。重定義的方法可以繼續(xù)通過它們重命名過的名稱調(diào)用原來的方法,例如類:
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忌怎,入?yún)?shù)量為1的方法
.method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance();復制代碼
ByteBuddy
通過 net.bytebuddy.matcher.ElementMatcher
來定義配置策略,可以通過此接口實現(xiàn)自己定義的匹配策略酪夷。庫本身提供的 Matcher 非常多榴啸。[圖片上傳失敗...(image-c7cf43-1659713619658)]
方法委托
使用MethodDelegation可以將方法調(diào)用委托給任意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 還可以如下實現(xiàn):
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(); }
}復制代碼
前一個實現(xiàn)因為只有一個方法鸥印,而且類型也匹配,很好理解腥例,那么后一個呢辅甥,Byte Buddy到底會委托給哪個方法酝润?Byte Buddy遵循一個最接近原則:
- intercept(int)因為參數(shù)類型不匹配燎竖,直接Pass
- 另外兩個方法參數(shù)都匹配,但是 intercept(String)類型更加接近要销,因此會委托給它
同時需要注意的是被攔截的方法需要聲明為 public构回,否則沒法進行攔截增強。除此之外疏咐,還可以使用 @RuntimeType
注解來標注方法
@RuntimeType
public static Object intercept(@RuntimeType Object value) {
System.out.println("Invoked method with: " + value);
return value;
}復制代碼
參數(shù)綁定
可以在攔截器(Target)的攔截方法 intercept
中使用注解注入?yún)?shù)纤掸,ByteBuddy
會根據(jù)注解給我們注入對于的參數(shù)值。比如:
void intercept(Object o1, Object o2)
// 等同于
void intercept(@Argument(0) Object o1, @Argument(1) Object o2)復制代碼
常用的注解如下表:
| 注解 | 描述 |
| @Argument
| 綁定單個參數(shù) |
| @AllArguments
| 綁定所有參數(shù)的數(shù)組 |
| @This
| 當前被攔截的浑塞、動態(tài)生成的那個對象 |
| @DefaultCall
| 調(diào)用默認方法而非super的方法 |
| @SuperCall
| 用于調(diào)用父類版本的方法 |
| @RuntimeType
| 可以用在返回值借跪、參數(shù)上,提示ByteBuddy禁用嚴格的類型檢查 |
| @Super
| 當前被攔截的酌壕、動態(tài)生成的那個對象的父類對象 |
| @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()) // 實現(xiàn) InterceptionAccessor 接口
.make()
.load(getClass().getClassLoader())
.getLoaded();
InstanceCreator factory = new ByteBuddy()
.subclass(InstanceCreator.class)
.method(not(isDeclaredBy(Object.class))) // 非父類 Object 聲明的方法
.intercept(MethodDelegation.toConstructor(dynamicUserType)) // 委托攔截的方法來調(diào)用提供的類型的構(gòu)造函數(shù)
.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
類實現(xiàn)了 InterceptionAccessor
接口掏愁,同時使用 MethodDelegation.toField
可以使攔截的方法可以委托給新增的字段。
End
本文是自己學習 ByteBuddy
后自己稍加整理的基礎(chǔ)教程卵牍。最后感謝你閱讀9邸!糊昙!