Java 是一個強類型語言系統,要求變量和對象都有一個肯定的類型,不兼容類型賦值都會形成轉換異常,一般狀況下這種錯誤都會被編譯器檢查出來,如此嚴格的類型在大多數狀況下是比較使人滿意的,這對構建具備很是強可讀性和穩定性的應用有很大的幫助,這也是 Java 能在企業編程中的普及的一個緣由之一。然而,由於起強類型的檢查,限制了其餘領域語言應用範圍。好比在編寫一個框架是,一般咱們並不知道應用程序定義的類型,由於當這個庫被編譯時,咱們還不知道這些類型,爲了能在這種狀況下能調用或者訪問應用程序的方法或者變量,Java 類庫提供了一套反射 API。使用這套反射 API,咱們就能夠檢討爲知類型,進而調用方法或者訪問屬性。可是,Java 反射有以下缺點:java
正如官網說的:Byte Buddy 是一個代碼生成和操做庫,用於在Java應用程序運行時建立和修改Java類,而無需編譯器的幫助。除了Java類庫附帶的代碼生成實用程序外,Byte Buddy還容許建立任意類,而且不限於實現用於建立運行時代理的接口。此外,Byte Buddy提供了一種方便的API,可使用Java代理或在構建過程當中手動更改類。Byte Buddy 相比其餘字節碼操做庫有以下優點:編程
在選擇字節碼操做庫時,每每須要考慮庫自己的性能。對於許多應用程序,生成代碼的運行時特性更有可能肯定最佳選擇。而在生成的代碼自己的運行時間以外,用於建立動態類的運行時也是一個問題。官網對庫進行了性能測試,給出如下結果圖:數組
圖中的每一行分別爲,類的建立、接口實現、方法調用、類型擴展、父類方法調用的性能結果。從性能報告中能夠看出,Byte Buddy 的主要側重點在於以最少的運行時生成代碼,須要注意的是,咱們這些衡量 Java 代碼性能的測試,都由 Java 虛擬機即時編譯器優化過,若是你的代碼只是偶爾運行,沒有獲得虛擬機的優化,可能性能會有所誤差。因此咱們在使用 Byte Buddy 開發時,咱們但願監控這些指標,以免在添加新功能時形成性能損失。安全
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
方法生產字節碼,有類加載器加載到虛擬機中。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中
示例
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具備限制:
重定義一個類時,Byte Buddy 能夠對一個已有的類添加屬性和方法,或者刪除已經存在的方法實現。新添加的方法,若是簽名和原有方法一致,則原有方法會消失。
相似於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遵循一個最接近原則:
同時須要注意的是被攔截的方法須要聲明爲 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
可使攔截的方法能夠委託給新增的字段。
本文是本身學習 ByteBuddy
後本身稍加整理的基礎教程。最後感謝你閱讀!!!
微信公衆號關注:ByteZ,獲取更多學習資料