比官方還詳細的ByteBuddy入門教程

JavaDog發表於2019-03-30

簡介

ByteBuddy是一個程式碼生成和操作的庫,類似於cglib、javassist,那麼為什麼選用該庫呢?javassist更偏向底層,比較難於使用並且在動態組合字串以實現更復雜的邏輯時很容易出錯,而cglib現在維護的則相當慢了,基本處於無人維護的階段了,而這些缺點ByteBuddy都沒有,並且ByteBuddy效能相對來說在三者中是最優的,具體參照更詳細的內容參照 ByteBuddy官網

PS:當前Mockito、Hibernate、Jackson等系統都在使用ByteBuddy,具體參照 ByteBuddy的git統計,對於我來說可能更喜歡的是ByteBuddy的流式程式設計風格。

注意:本文僅是一個使用者友好版的Get Start!!!下面開始教程。

首先是建立一個類

首先是建立一個類,繼承Object並且重寫toString方法,示例如下:

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String toString = "hello ByteBuddy";
        DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
                .subclass(Object.class)
                .method(named("toString"))
                .intercept(FixedValue.value(toString))
                .make();

        Class<? extends Object> clazz = unloaded
            .load(ByteBuddyTest.class.getClassLoader())
            .getLoaded();
        Assert.assertEquals(clazz.newInstance().toString(), toString);
    }
}
複製程式碼

可以看到ByteBuddy的程式碼語義還是很清晰的,subclass方法宣告瞭建立的類的父類,method宣告瞭要攔截的方法(實際底層是一個方法過濾器),而intercept則對上一步過濾出來的方法進行了實際攔截處理。

同時可以注意到上邊並沒有為生成的Class指定名稱,如果要為生成的Class指定名稱可以使用name()方法,如下例子,將生成的Class名指定為com.joe.ByteBuddyObject

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String toString = "hello ByteBuddy";
        String name = "com.joe.ByteBuddyObject";
        DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
                .subclass(Object.class)
                .name("com.joe.ByteBuddyObject")
                .method(named("toString"))
                .intercept(FixedValue.value(toString))
                .make();

        Class<? extends Object> clazz = unloaded
            .load(ByteBuddyTest.class.getClassLoader())
            .getLoaded();
        Assert.assertEquals(clazz.newInstance().toString(), toString);
        Assert.assertEquals(clazz.getName(), name);
    }
}
複製程式碼

代理方法到其他實現

前一個例子簡單的重寫了toString並返回了固定的值,但是實際使用中很少有返回固定值的,一般都是呼叫某個函式然後返回該函式計算結果,那麼這該怎麼實現呢?別接,看下面的例子。

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        DynamicType.Unloaded<People> unloaded = new ByteBuddy()
                .subclass(People.class)
                .name("com.joe.ByteBuddyObject")
                .method(named("say"))
                .intercept(MethodDelegation.to(new JoeKerouac()))
                .make();

        Class<? extends People> clazz = unloaded
            .load(ByteBuddyTest.class.getClassLoader())
            .getLoaded();
        Assert.assertSame(clazz.getInterfaces()[0], People.class);
        Assert.assertEquals(clazz.newInstance().say(), "hello JoeKerouac");
    }

    public interface People{
        String say();
    }

    public class JoeKerouac {
        public String say() {
            return "hello JoeKerouac";
        }
    }
}
複製程式碼

需要注意的是:

  • People這個介面和JoeKerouac這個類必須是公共的可訪問的
  • JoeKerouac的say方法除了名字其他定義必須與People相同,即方法必須是公共的、返回值必須是String型別,必須是無引數的,而名字則可以不叫say
  • 該寫法僅支援同一個類中(本示例就是JoeKerouac中)有且只有一個相同簽名的函式(符合上邊三要素的),否則會報錯,該問題後續會解決。錯誤示例:

    public class JoeKerouac {
     public String sayHello() {
        return "hello JoeKerouac";
     }
    
     public String sayHi() {
             return "hi JoeKerouac";
     }
    }
    複製程式碼

這樣簡單的幾行就動態實現了一個People的子類。

代理方法到其他實現-進階

上邊的注意事項說明中有該寫法僅支援同一個類中(本示例就是JoeKerouac中)有且只有一個相同簽名的函式(符合上邊三要素的),否則會報錯,該問題後續會解決。,那麼是不是意味著ByteBuddy有很大侷限性呢?並不是的,其實這個問題很好解決,報錯的原因是如果存在多個簽名相同的方法ByteBuddy不能決定到底用哪個方法。既然ByteBuddy不能決定,那麼我們幫他決定不就好了?ByteBuddy的作者顯然也意識到了該問題,並且提供瞭解決方案,示例如下:

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String hiMsg = "hi JoeKerouac";
        String helloMsg = "hello JoeKerouac";

        People hi = build("sayHi");
        People hello = build("sayHello");

        Assert.assertEquals(hi.say(), hiMsg);
        Assert.assertEquals(hello.say(), helloMsg);
    }

    private People build(String method) throws Exception {
        DynamicType.Unloaded<People> unloaded = new ByteBuddy()
                .subclass(People.class)
                .name("com.joe.ByteBuddyObject")
                .method(named("say"))
                .intercept(MethodDelegation
                        .withDefaultConfiguration()
                        .filter(ElementMatchers.named(method))
                        .to(new JoeKerouac())
                )
                .make();

        Class<? extends People> clazz = unloaded
                .load(ByteBuddyTest.class.getClassLoader())
                .getLoaded();
        return clazz.newInstance();
    }

    public interface People{
        String say();
    }

    public class JoeKerouac {

        public String sayHello() {
            return "hello JoeKerouac";
        }

        public String sayHi() {
            return "hi JoeKerouac";
        }
    }
}
複製程式碼

這樣,即使同一個類中有多個相同簽名(此處的簽名與java中的方法簽名語義不一樣,是符合上邊三要素的簽名,不要搞混)的方法也能區分開來,並且可以自主選擇使用哪個方法,同時ElementMatchers也提供了很多其他開箱即用的選擇器,可以自己看原始碼來學習使用,並不算太難。

一個需要注意的點兒

ByteBuddy構建構成中生成的物件都是不可變物件,會出現下面的問題:

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String toString = "hello ByteBuddy";
        DynamicType.Builder<Object> builder = new ByteBuddy()
                .subclass(Object.class);
        builder.method(named("toString"))
                .intercept(FixedValue.value(toString));

        Class<? extends Object> clazz = builder.make()
                .load(ByteBuddyTest.class.getClassLoader())
                .getLoaded();
        // 會報錯
        Assert.assertEquals(clazz.newInstance().toString(), toString);
    }
}
複製程式碼

將第一個示例中的程式碼稍加改動,你會發現這個測試用例跑不通了,原因是因為在builder.method這一行開始一直到intercept方法結束後會生成一個新的builder,而不是更改原來的builder,因為ByteBuddy生成的中間物件都是不可變的,只能新建不能修改,所以需要將上述程式碼稍加修改就能通過測試了,修改如下:

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String toString = "hello ByteBuddy";
        DynamicType.Builder<Object> builder = new ByteBuddy()
                .subclass(Object.class);
        builder = builder.method(named("toString"))
                .intercept(FixedValue.value(toString));

        Class<? extends Object> clazz = builder.make()
                .load(ByteBuddyTest.class.getClassLoader())
                .getLoaded();
        Assert.assertEquals(clazz.newInstance().toString(), toString);
    }
}
複製程式碼

我們只需要將中間狀態記錄下來然後在後續使用中使用就行,這樣這個測試用例就又能跑通了~

End

本文僅是一個Get Start教程,可以參照著ByteBuddy官網來看,同時裡邊將一些ByteBuddy沒有的內容補充了一下,還有一些坑也做了一下說明,後續可能會寫一個更詳細的教程,在此之前如果想要深入瞭解一些其他用法只能自己通過看原始碼來學習了,這可能是ByteBuddy最不友好的一點兒了,不過一般使用ByteBuddy的場景都比較底層,而用的上ByteBuddy的人一般也有一定原始碼閱讀能力,並且ByteBuddy原始碼也不算太難,所以這應該不是一個難事兒,本文僅用於結合一些實際場景快速入門。

比官方還詳細的ByteBuddy入門教程


相關文章