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

簡介

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

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

注意:本文僅是一個用戶友好版的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則對上一步過濾出來的方法進行了實際攔截處理。app

同時能夠注意到上邊並無爲生成的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源碼也不算太難,因此這應該不是一個難事兒,本文僅用於結合一些實際場景快速入門。



沒有關注的能夠掃下方二維碼關注我,若是在閱讀過程當中有任何問題還能夠加我QQ1213812243詢問~



         

長按二維碼關注我吧

不要錯過



本文分享自微信公衆號 - java初學者(JoeKerouac_public)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索