字節(jié)碼增強技術(shù)-Byte Buddy

為什么需要在運行時生成代碼业岁?

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é)果圖:


image.png

圖中的每一行分別為饥伊,類的創(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 指定了 ObjecttoString 方法训唱,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邸!糊昙!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辛掠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌萝衩,老刑警劉巖回挽,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異欠气,居然都是意外死亡厅各,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門预柒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來队塘,“玉大人,你說我怎么就攤上這事宜鸯°竟牛” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵淋袖,是天一觀的道長鸿市。 經(jīng)常有香客問我,道長即碗,這世上最難降的妖魔是什么焰情? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮剥懒,結(jié)果婚禮上内舟,老公的妹妹穿的比我還像新娘。我一直安慰自己初橘,他們只是感情好验游,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著保檐,像睡著了一般耕蝉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上夜只,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天垒在,我揣著相機與錄音,去河邊找鬼扔亥。 笑死场躯,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的砸王。 我是一名探鬼主播推盛,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谦铃!你這毒婦竟也來了耘成?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瘪菌,沒想到半個月后撒会,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡师妙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年诵肛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片默穴。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡怔檩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蓄诽,到底是詐尸還是另有隱情薛训,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布仑氛,位于F島的核電站乙埃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏锯岖。R本人自食惡果不足惜介袜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望出吹。 院中可真熱鬧遇伞,春花似錦、人聲如沸趋箩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽叫确。三九已至,卻和暖如春芍锦,著一層夾襖步出監(jiān)牢的瞬間竹勉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工娄琉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留次乓,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓孽水,卻偏偏與公主長得像票腰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子女气,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容