字節碼編程,Byte-buddy篇一《基於Byte Buddy語法創建的第一個HelloWorld》



作者:小傅哥
博客:https://bugstack.cn

沉淀、分享、成長,讓自己和他人都能有所收獲!

一、前言

相對於小傅哥之前編寫的字節碼編程; ASMJavassist 系列,Byte Buddy 玩法上更加高級,你可以完全不需要了解一個類和方法塊是如何通過 指令碼 LDC、LOAD、STORE、IRETURN... 生成出來的。就像它的官網介紹;

Byte Buddy 是一個代碼生成和操作庫,用於在 Java 應用程序運行時創建和修改 Java 類,而無需編譯器的幫助。除了 Java 類庫附帶的代碼生成實用程序外,Byte Buddy 還允許創建任意類,並且不限於實現用於創建運行時代理的接口。此外,Byte Buddy 提供了一種方便的 API,可以使用 Java 代理或在構建過程中手動更改類。

  • 無需理解字節碼指令,即可使用簡單的 API 就能很容易操作字節碼,控制類和方法。
  • 已支持Java 11,庫輕量,僅取決於Java字節代碼解析器庫ASM的訪問者API,它本身不需要任何其他依賴項。
  • 比起JDK動態代理、cglib、Javassist,Byte Buddy在性能上具有一定的優勢。

2015年10月,Byte Buddy被 Oracle 授予了 Duke's Choice大獎。該獎項對Byte Buddy的“ Java技術方面的巨大創新 ”表示贊賞。我們為獲得此獎項感到非常榮幸,並感謝所有幫助Byte Buddy取得成功的用戶以及其他所有人。我們真的很感激!

除了這些簡單的介紹外,還可以通過官網:https://bytebuddy.net,去了解更多關於 Byte Buddy 的內容。

好! 那么接下來,我們開始從 HelloWorld 開始。深入了解一個技能前,先多多運行,這樣總歸能讓找到學習的快樂。

二、開發環境

  1. JDK 1.8.0
  2. byte-buddy 1.10.9
  3. byte-buddy-agent 1.10.9
  4. 本章涉及源碼在:itstack-demo-bytecode-2-01,可以關注公眾號bugstack蟲洞棧,回復源碼下載獲取。你會獲得一個下載鏈接列表,打開后里面的第17個「因為我有好多開源代碼」,記得給個Star

三、案例目標

每一個程序員,都運行過 N 多個 HelloWorld,就像很熟悉的 Java

public class Hi {

    public static void main(String[] args) {
        System.out.println("Byte-buddy Hi HelloWorld By 小傅哥(bugstack.cn)");
    }

}

那么我們接下來就通過使用動態字節碼生成的方式,來創建出可以輸出 HelloWorld 的程序。

新知識點的學習不要慌,最主要是找到一個可以入手的點,通過這樣的一個點去慢慢解開整個程序的面紗。

四、技術實現

1. 官網經典例子

在我們看官網文檔中,從它的介紹了就已經提供了一個非常簡單的例子,用於輸出 HelloWorld,我們在這展示並講解下。

案例代碼:

String helloWorld = new ByteBuddy()
            .subclass(Object.class)
            .method(named("toString"))
            .intercept(FixedValue.value("Hello World!"))
            .make()
            .load(getClass().getClassLoader())
            .getLoaded()
            .newInstance()
            .toString();    

System.out.println(helloWorld);  // Hello World!

他的運行結果就是一行,Hello World!,整個代碼塊核心功能就是通過 method(named("toString")),找到 toString 方法,再通過攔截 intercept,設定此方法的返回值。FixedValue.value("Hello World!")。到這里其實一個基本的方法就通過 Byte-buddy ,改造完成。

接下來的這一段主要是用於加載生成后的 Class 和執行,以及調用方法 toString()。也就是最終我們輸出了想要的結果。那么,如果你不能看到這樣一段方法塊,把我們的代碼改造后的樣子,心里還是有點虛。那么,我們通過字節碼輸出到文件,看下具體被改造后的樣子,如下;

編譯后的Class文件ByteBuddyHelloWorld.class

public class HelloWorld {
    public String toString() {
        return "Hello World!";
    }

    public HelloWorld() {
    }
}

在官網來看,這是一個非常簡單並且能體現 Byte buddy 的例子。但是與我們平時想創建出來的 main 方法相比,還是有些差異。那么接下來,我們嘗試使用字節碼編程技術創建出這樣一個方法。

2. 字節碼創建類和方法

接下來的例子會通過一點點的增加代碼梳理,不斷的把一個方法完整的創建出來。

2.1 定義輸出字節碼方法

為了可以更加清晰的看到每一步對字節碼編程后,所創建出來的方法樣子(clazz),我們需要輸出字節碼生成 clazz。在Byte buddy中默認提供了一個 dynamicType.saveIn() 方法,我們暫時先不使用,而是通過字節碼進行保存。

private static void outputClazz(byte[] bytes) {
    FileOutputStream out = null;
    try {
        String pathName = ApiTest.class.getResource("/").getPath() + "ByteBuddyHelloWorld.class";
        out = new FileOutputStream(new File(pathName));
        System.out.println("類輸出路徑:" + pathName);
        out.write(bytes);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (null != out) try {
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 這個方法我們在之前也用到過,主要就是一個 Java 基礎的內容,輸出字節碼到文件中。

2.2 創建類信息

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        .subclass(Object.class)
        .name("org.itstack.demo.bytebuddy.HelloWorld")
        .make();

// 輸出類字節碼
outputClazz(dynamicType.getBytes());
  • 創建類和定義類名,如果不寫類名會自動生成要給類名。

此時class文件:

public class HelloWorld {
    public HelloWorld() {
    }
}

2.3 創建main方法

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        .subclass(Object.class)
        .name("org.itstack.demo.bytebuddy.HelloWorld")
        .defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC)
        .withParameter(String[].class, "args")
        .intercept(FixedValue.value("Hello World!"))
        .make();

與上面相比新增的代碼片段;

  • defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC),定義方法;名稱、返回類型、屬性public static
  • withParameter(String[].class, "args"),定義參數;參數類型、參數名稱
  • intercept(FixedValue.value("Hello World!")),攔截設置返回值,但此時還能滿足我們的要求。

這里有一個知識點,Modifier.PUBLIC + Modifier.STATIC,這是一個是二進制相加,每一個類型都在二進制中占有一位。例如 1 2 4 8 ... 對應的二進制占位 1111。所以可以執行相加運算,並又能保留原有單元的屬性。

此時class文件:

public class HelloWorld {
    public static void main(String[] args) {
        String var10000 = "Hello World!";
    }

    public HelloWorld() {
    }
}

此時基本已經可以看到我們平常編寫的 Hello World 影子了,但還能輸出結果。

2.4 委托函數使用

為了能讓我們使用字節碼編程創建的方法去輸出一段 Hello World ,那么這里需要使用到委托

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        .subclass(Object.class)
        .name("org.itstack.demo.bytebuddy.HelloWorld")
        .defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC)
        .withParameter(String[].class, "args")
        .intercept(MethodDelegation.to(Hi.class))
        .make();
  • 整體來看變化並不大,只有 intercept(MethodDelegation.to(Hi.class)),使用了一段委托函數,真正去執行輸出的是另外的函數方法。

    • MethodDelegation,需要是 public
    • 被委托的方法與需要與原方法有着一樣的入參、出參、方法名,否則不能映射上

此時class文件:

public class HelloWorld {
    public static void main(String[] args) {
        Hi.main(var0);
    }

    public HelloWorld() {
    }
}
  • 那么此時就可以輸出我們需要的內容了,Hi.main 是定義出來的委托函數。也就是一個 HelloWorld

五、測試結果

為了可以讓整個方法運行起來,我們需要添加字節碼加載和反射調用的代碼塊,如下;

// 加載類
Class<?> clazz = dynamicType.load(GenerateClazzMethod.class.getClassLoader())
        .getLoaded();

// 反射調用
clazz.getMethod("main", String[].class).invoke(clazz.newInstance(), (Object) new String[1]);

運行結果

類輸出路徑:/User/xiaofuge/itstack/git/github.com/itstack-demo-bytecode/itstack-demo-bytecode-2-01/target/test-classes/ByteBuddyHelloWorld.class
helloWorld

Process finished with exit code 0

效果圖

Byte buddy HelloWorld 效果圖

六、總結

  • 在本章節 Byte buddy 中,需要掌握幾個關鍵信息;創建方法、定義屬性、攔截委托、輸出字節碼,以及最終的運行。這樣的一個簡單過程,可以很快的了解到如何使用 Byte buddy
  • 本系列文章后續會繼續更新,把常用的 Byte buddy 方法通過實際的案例去模擬建設,在這個過程中加強學習使用。一些基礎知識也可以通過官方文檔進行學習;https://bytebuddy.net
  • 在學習整理的過程中發現,關於字節碼編程方面的資料並不是很全,主要源於大家平時的開發中基本是用不到的,誰也不可能總去修改字節碼。但對於補全這樣的成體系完善技術棧資料,卻可以幫助很多需要的人。因此我也會持續輸出類似這樣空白的技術文章。

七、彩蛋

CodeGuide | 程序員編碼指南 Go!
本代碼庫是作者小傅哥多年從事一線互聯網 Java 開發的學習歷程技術匯總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心內容。如果本倉庫能為您提供幫助,請給予支持(關注、點贊、分享)!

CodeGuide | 程序員編碼指南


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM