字節碼加強技術-Byte Buddy

爲何須要在運行時生成代碼?

Java 是一個強類型語言系統,要求變量和對象都有一個肯定的類型,不兼容類型賦值都會形成轉換異常,一般狀況下這種錯誤都會被編譯器檢查出來,如此嚴格的類型在大多數狀況下是比較使人滿意的,這對構建具備很是強可讀性和穩定性的應用有很大的幫助,這也是 Java 能在企業編程中的普及的一個緣由之一。然而,由於起強類型的檢查,限制了其餘領域語言應用範圍。好比在編寫一個框架是,一般咱們並不知道應用程序定義的類型,由於當這個庫被編譯時,咱們還不知道這些類型,爲了能在這種狀況下能調用或者訪問應用程序的方法或者變量,Java 類庫提供了一套反射 API。使用這套反射 API,咱們就能夠檢討爲知類型,進而調用方法或者訪問屬性。可是,Java 反射有以下缺點: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在性能上具備優點。

性能

在選擇字節碼操做庫時,每每須要考慮庫自己的性能。對於許多應用程序,生成代碼的運行時特性更有可能肯定最佳選擇。而在生成的代碼自己的運行時間以外,用於建立動態類的運行時也是一個問題。官網對庫進行了性能測試,給出如下結果圖:file數組

圖中的每一行分別爲,類的建立、接口實現、方法調用、類型擴展、父類方法調用的性能結果。從性能報告中能夠看出,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 指定了 ObjecttoString 方法,intercept 攔截了 toString 方法並返回固定的 value ,最後 make 方法生產字節碼,有類加載器加載到虛擬機中。app

此外,Byte Buddy不只限於建立子類和操做類,還能夠轉換現有代碼。Byte Buddy 還提供了一個方便的 API,用於定義所謂的 Java 代理,該代理容許在任何 Java 應用程序的運行期間進行代碼轉換,代理會在下篇單獨寫一篇文章講解。框架

建立一個類

任何一個由 ByteBuddy 建立的類型都是經過 ByteBuddy 類的實例來完成的。經過簡單地調用 new ByteBuddy() 就能夠建立一個新實例。dom

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,這裏的數字序列是隨機的。ide

此外,在一些須要指定類型的場景中,能夠經過重寫 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 很是多。Uploading file...

方法委託

使用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 可使攔截的方法能夠委託給新增的字段。

End

本文是本身學習 ByteBuddy 後本身稍加整理的基礎教程。最後感謝你閱讀!!!

微信公衆號關注:ByteZ,獲取更多學習資料

file

相關文章
相關標籤/搜索