位元組碼程式設計,Byte-buddy篇一《基於Byte Buddy語法建立的第一個HelloWorld》
阿新 • • 發佈:2020-05-11
![](https://img-blog.csdnimg.cn/20200508230149746.jpg)
作者:小傅哥
部落格:[https://bugstack.cn](https://bugstack.cn)
>沉澱、分享、成長,讓自己和他人都能有所收穫!
## 一、前言
相對於[`小傅哥`](https://bugstack.cn)之前編寫的位元組碼程式設計; `ASM`、`Javassist` 系列,`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`](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蟲洞棧`](https://bugstack.cn/assets/images/qrcode.png),回覆原始碼下載獲取。`你會獲得一個下載連結列表,開啟后里面的第17個「因為我有好多開原始碼」`,記得給個`Star`!
## 三、案例目標
每一個程式設計師,都執行過 `N` 多個` HelloWorld`,就像很熟悉的 `Java`;
```java
public class Hi {
public static void main(String[] args) {
System.out.println("Byte-buddy Hi HelloWorld By 小傅哥(bugstack.cn)");
}
}
```
那麼我們接下來就通過使用動態位元組碼生成的方式,來創建出可以輸出 `HelloWorld` 的程式。
*新知識點的學習不要慌,最主要是找到一個可以入手的點,通過這樣的一個點去慢慢解開整個程式的面紗。*
## 四、技術實現
### 1. 官網經典例子
在我們看官網文件中,從它的介紹了就已經提供了一個非常簡單的例子,用於輸出 `HelloWorld`,我們在這展示並講解下。
**案例程式碼:**
```java
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`
```java
public class HelloWorld {
public String toString() {
return "Hello World!";
}
public HelloWorld() {
}
}
```
在官網來看,這是一個非常簡單並且能體現 `Byte buddy` 的例子。但是與我們平時想創建出來的 `main` 方法相比,還是有些差異。那麼接下來,我們嘗試使用位元組碼程式設計技術創建出這樣一個方法。
### 2. 位元組碼建立類和方法
接下來的例子會通過一點點的增加程式碼梳理,不斷的把一個方法完整的創建出來。
#### 2.1 定義輸出位元組碼方法
為了可以更加清晰的看到每一步對位元組碼程式設計後,所創建出來的方法樣子(clazz),我們需要輸出位元組碼生成 `clazz`。在Byte buddy中預設提供了一個 `dynamicType.saveIn()` 方法,我們暫時先不使用,而是通過位元組碼進行儲存。
```java
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 建立類資訊
```java
DynamicType.Unloaded dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("org.itstack.demo.bytebuddy.HelloWorld")
.make();
// 輸出類位元組碼
outputClazz(dynamicType.getBytes());
```
- 建立類和定義類名,如果不寫類名會自動生成要給類名。
**此時class檔案:**
```java
public class HelloWorld {
public HelloWorld() {
}
}
```
#### 2.3 建立main方法
```java
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檔案:**
```java
public class HelloWorld {
public static void main(String[] args) {
String var10000 = "Hello World!";
}
public HelloWorld() {
}
}
```
此時基本已經可以看到我們平常編寫的 `Hello World` 影子了,但還能輸出結果。
#### 2.4 委託函式使用
為了能讓我們使用位元組碼程式設計建立的方法去輸出一段 `Hello World` ,那麼這裡需要使用到`委託`。
```java
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檔案:**
```java
public class HelloWorld {
public static void main(String[] args) {
Hi.main(var0);
}
public HelloWorld() {
}
}
```
- 那麼此時就可以輸出我們需要的內容了,`Hi.main` 是定義出來的委託函式。也就是一個 `HelloWorld`
## 五、測試結果
為了可以讓整個方法執行起來,我們需要新增位元組碼載入和反射呼叫的程式碼塊,如下;
```java
// 載入類
Class clazz = dynamicType.load(GenerateClazzMethod.class.getClassLoader())
.getLoaded();
// 反射呼叫
clazz.getMethod("main", String[].class).invoke(clazz.newInstance(), (Object) new String[1]);
```
**執行結果**
```java
類輸出路徑:/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 效果圖](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9idWdzdGFjay5jbi9hc3NldHMvaW1hZ2VzLzIwMjAvaXRzdGFjay1kZW1vLWJ5dGVjb2RlLTItMDEtMS5wbmc?x-oss-process=image/format,png)
## 六、總結
- 在本章節 `Byte buddy` 中,需要掌握幾個關鍵資訊;建立方法、定義屬性、攔截委託、輸出位元組碼,以及最終的執行。這樣的一個簡單過程,可以很快的瞭解到如何使用 `Byte buddy`。
- 本系列文章後續會繼續更新,把常用的 `Byte buddy` 方法通過實際的案例去模擬建設,在這個過程中加強學習使用。一些基礎知識也可以通過官方文件進行學習;[https://bytebuddy.net](https://bytebuddy.net)。
- 在學習整理的過程中發現,關於位元組碼程式設計方面的資料並不是很全,主要源於大家平時的開發中基本是用不到的,誰也不可能總去修改位元組碼。但對於補全這樣的成體系完善技術棧資料,卻可以幫助很多需要的人。因此我也會持續輸出類似這樣空白的技術文章。
## 七、彩蛋
[`CodeGuide | 程式設計師編碼指南 Go!`](https://github.com/fuzhengwei/CodeGuide/wiki)
*本程式碼庫是作者小傅哥多年從事一線網際網路 Java 開發的學習歷程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心內容。如果本倉庫能為您提供幫助,請給予支援(關注、點贊、分享)!*
![CodeGuide | 程式設計師編碼指南](https://img-blog.csdnimg.cn/20200505214424670.png)