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()將儲存的運算元相乘。最後,我們使用dreturn()傳回該值
第一部分涵蓋了我們要產生的方法的第一個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 上取得。