Class-File API 簡介
1. 簡介
Java Class-File API 是在JEP-484中作為 Java 24 的一部分引入的。它旨在創建一個接口,允許類文件處理,而無需依賴舊版 JDK 的 ASM 庫的內部複製實作。
在本教程中,我們將了解如何從頭開始建立類別檔案以及如何使用 Class-File API 將一個類別檔案轉換為另一個類別檔案。
2. 核心類別檔案 API 元件
Class-File 有三個核心元素來產生和轉換我們稍後會看到的特徵:
- 元素代表程式碼的一部分,如變數、指令、方法或類別。此外,一個元素可能包含其他元素。例如,類別元素可能包含方法元素,其中包括變數或指令元素。
- 建構器(例如方法建構器和程式碼建構器)用於建立每種類型的元素。
- 可以使用轉換函數透過建構器將元素轉換為其他元素。
讓我們透過以下部分的實際範例來探討這三個組件如何連接。
3. 產生類別文件
在本節中,我們將了解如何使用MethodBuilder
和CodeBuilder
類別來產生類別檔案。
3.1.演示方法
為了說明類別的生成,讓我們來看一個簡單的程式碼片段,該程式碼片段根據員工的職能和基本薪資計算員工的薪資:
public double calculateAnnualBonus(double baseSalary, String role) {
if (role.equals("sales")) {
return baseSalary * 0.35;
}
if (role.equals("engineer")) {
return baseSalary * 0.25;
}
return baseSalary * 0.15;
}
3.2.使用MethodBuilder
和CodeBuilder
要產生具有與calculateAnnualBonus()
相同功能的方法,我們可以使用MethodBuilder
和CodeBuilder
類別。因此,讓我們先用Consumer<
MethodBuilder>
定義generate()
方法,該方法將用於建構方法:
public static void generate() throws IOException {
Consumer<MethodBuilder> calculateAnnualBonusBuilder = methodBuilder -> methodBuilder.withCode(codeBuilder -> {
Label notSales = codeBuilder.newLabel();
Label notEngineer = codeBuilder.newLabel();
ClassDesc stringClass = ClassDesc.of("java.lang.String");
// ...
});
}
首先,我們定義兩個Label
對象,稍後將用於在條件語句之間跳轉。此外,我們也定義了一個ClassDesc
常數,它代表String
類別文件,以便稍後使用。
然後,我們可以在calculateAnnualBonusBuilder
的lambda表達式中加入邏輯的第一部分:
codeBuilder.aload(3)
.ldc("sales")
.invokevirtual(stringClass, "equals", MethodTypeDesc.of(ClassDesc.of("Z"), stringClass))
.ifeq(notSales)
.dload(1)
.ldc(0.35)
.dmul()
.dreturn()
我們來詳細看一下上述邏輯的每一行:
- 我們首先開始使用
aload(3)
將role
方法參數載入到引用中。值得注意的是,aload()
的參數是方法參數中變數的槽號,其中long
和double
佔用兩個槽。因此,第一個baseSalary
參數位於插槽 1,而role
位於插槽 3。 - 然後,我們使用
ldc()
將sales
常數儲存在操作數堆疊中,以供後續操作使用。 - 之後,我們對堆疊中的最後一個操作數呼叫
invokevirtual()
,這些運算元是常數“sales”
和role
參數。此外,我們呼叫儲存在stringClass
變數中的String
類別的equals()
方法來比較運算元。ClassDesc.of(Z)
部分錶示我們期望布林值作為該方法呼叫的回傳類型。 - 然後我們呼叫
ifeq()
並傳遞notSales
標籤變數。這意味著只有當invokevirtual()
的前一個布林結果回傳true
時,建構器的後續指令才會運作。否則,程式應該跳到我們稍後定義的notSales
綁定。 - 最後,如果
ifReq()
的條件回傳true
,我們將使用dload(1).
載入baseSalary
參數。然後,我們將常數0.35
載入到操作數堆疊並使用dmul()
將儲存的運算元相乘。最後,我們使用d
return()
傳回該值
第一部分涵蓋了我們要產生的方法的第一個if
語句。因此,為了產生第二個if
語句,我們可以在dreturn()
呼叫之後向codeBuilder()
加入更多方法呼叫:
.labelBinding(notSales)
.aload(3)
.ldc("engineer")
.invokevirtual(stringClass, "equals", MethodTypeDesc.of(ClassDesc.of("Z"), stringClass))
.ifeq(notEngineer)
.dload(1)
.ldc(0.25)
.dmul()
.dreturn()
如果ifeq(notSales)
表達式回傳false
,則labelBinding(notSales)
運行。其他操作與我們先前介紹的處理第一個if
語句的操作類似。
最後,我們可以添加最後一部分來覆蓋預設值返回:
.labelBinding(notEngineer)
.dload(1)
.ldc(0.15)
.dmul()
.dreturn();
標籤分支也會發生同樣的事情,但現在是notEngineer
標籤。如果ifeq(notEngineer)
傳回false
,則最後一部分運行。
最後,為了完成我們的generate()
方法,我們需要定義ClassFile
物件並將其寫入.class
檔案:
var classBuilder = ClassFile.of()
.build(ClassDesc.of("EmployeeSalaryCalculator"),
cb - > cb.withMethod("calculateAnnualBonus", MethodTypeDesc.of(CD_double, CD_double, CD_String),
AccessFlag.PUBLIC.mask(),
calculateAnnualBonusBuilder));
Files.write(Path.of("EmployeeSalaryCalculator.class"), classBuilder);
我們使用ClassFile.of().build()
來實例化一個類別檔案建構器,並向其傳遞了兩個參數。第一個是包裝在ClassDesc.of()
呼叫中的類別名稱。第二個是ClassBuilder
用戶,它使用所需的方法來產生類別。為此,我們使用withMethod()
傳遞方法名稱、方法簽章、存取標誌和先前定義的方法程式碼建構器。
值得注意的是,我們將方法簽名定義為MethodTypeDesc.
of(CD_double, CD_double, CD_String)
,這表示產生的方法傳回一個由第一個參數定義的double
,並接收一個double
和一個String
參數。
然後,我們使用Files
編寫器將儲存在classBuilder
變數中的位元組陣列寫入檔案。
4. 將一個類別文件轉換為另一個類別文件
現在,假設我們要將一個類別文件的所有內容複製到另一個類別文件中。我們可以透過使用不同的轉換來實現這一點:
public static void transform() throws IOException {
var basePath = Files.readAllBytes(Path.of("EmployeeSalaryCalculator.class"));
CodeTransform codeTransform = ClassFileBuilder::accept;
MethodTransform methodTransform = MethodTransform.transformingCode(codeTransform);
ClassTransform classTransform = ClassTransform.transformingMethods(methodTransform);
ClassFile classFile = ClassFile.of();
byte[] transformedClass = classFile.transformClass(classFile.parse(basePath), classTransform);
Files.write(Path.of("TransformedEmployeeSalaryCalculator.class"), transformedClass);
}
在上面的範例中,我們首先讀取在上一節中建立的類別檔案EmployeeSalaryCalculator
。
然後,我們定義一個CodeTransform
,它接受原始類別中定義的所有CodeElements
。此外,我們使用codeTransform
建立MethodTransform
,並使用methodTransform
建立ClassTransform
。這樣的組合使得變壓器很容易推廣和重複用於不同的目的。
可以使用更明確的 lambda 表達式來定義更多客製化的程式碼和方法轉換。例如,我們可以使用 lambda 表達式定義自訂MethodTransform
,該表達式僅接受具有特定名稱的方法:
MethodTransform methodTransform = (methodBuilder, methodElement) - > {
if (methodElement.header().name().stringValue().equals("calculateAnnualBonus")) {
methodBuilder.withCode(codeBuilder - > {
for (var codeElement: methodElement.code()) {
codeBuilder.accept(codeElement);
}
});
}
};
在上面的範例中,我們先使用header() and name()
方法來檢查方法名稱是否等於文字calculateAnnualBonus
。如果是這樣,我們將使用methodBuilder
來建立一個方法,該方法具有來自原始類別的methodElement
的精確指令。
5. 結論
在本文中,我們研究了從頭開始建立類別以及使用 Class File API 將內容從一個類別複製到另一個類別的詳細資訊。
我們研究如何利用各種建構器、轉換器和元素在運行時創建和轉換類別的範例。
與往常一樣,原始碼可在 GitHub 上取得。