在 Java 中使用 Spoon 分析、生成和轉換代碼
一、簡介
在本教程中,我們將展示如何使用 Spoon 庫來解析、分析和轉換 Java 源代碼。
2.勺子概述
在處理大型代碼庫時,出於給定目的需要消化它們的情況並不少見。例子包括:
- 生成匯總報告
- 查找給定類的用法,包括通過複雜繼承鏈的間接用法
- 發現潛在漏洞
- 自動重構
這個列表可以繼續下去,但是所有這些任務都有一個共同的模式。首先,他們要求我們掃描現有代碼並為其構建內部表示。其次,我們將使用訪問者模式或查詢機制來查找我們感興趣的元素。最後,我們將生成所需的輸出。
Spoon 庫專注於前兩個步驟,因此我們可以專注於生成所需的結果。
當然,一個簡單的基於文本的 shell 或 Python 管道可以完成某些用例的工作。然而,這種方法缺乏對掃描代碼的更深入理解,因此限制了我們可以進行的分析類型。
另一方面,Spoon 創建了一個完整的代碼庫內存模型,允許它以多種方式遍歷它。在底層,Spoon 使用 Eclipse 的 JDT 編譯器來解析源代碼,從而產生了一個“高保真”模型,它不僅包括類、方法等,還包括所有語句和註釋。
此外,Spoon 可以處理語法無效的代碼並且不關心缺失的依賴項,如果您必須挖掘數百個 git 存儲庫的遺留代碼,這很好。
3.使用勺子
3.1. Maven 依賴
要在我們的項目中使用 Spoon 庫,我們需要將其添加為依賴項:
<dependency>
<groupId>fr.inria.gforge.spoon</groupId>
<artifactId>spoon-core</artifactId>
<version>10.3.0</version>
</dependency>
最新版本可在Maven Central上獲得。
請注意,從版本 10 開始,Spoon 需要 Java 11 或更高版本才能運行。無論如何,它可以從 Java 源代碼解析和創建模型,最高版本為 16(截至撰寫本文時)。
3.2.解析代碼
讓我們從一個簡單的例子開始。我們將使用 Spoon 來解析單個 Java 類,並創建一個包含公共、私有和受保護方法計數的報告。
SpoonAPI
接口充當使用該庫的主要入口點。獲得此接口的具體實現的標準方法是創建一個新的Launcher
實例:
SpoonAPI spoon = new Launcher();
接下來,我們將使用addInputResource()
通知我們要分析的源代碼的位置:
spoon.addInputResource("some/directory/SomeClass.java");
此方法接受單個類或目錄的路徑。在後一種情況下,所有 Java 文件都將被遞歸解析。該方法可以多次調用。例如,如果我們想同時解析來自多個存儲庫的代碼,就會出現這種情況。
現在,我們將使用buildModel()
來創建CtModel
實例,該實例包含有關所有已處理代碼的信息:
CtModel model = spoon.buildModel();
考慮CtModel
類的一種方式是,它在 XML 處理中扮演著類似於Document
類的角色:它是樹的根,從中可以到達所有其他元素。在我們的例子中,元素可以是類、方法、包變量聲明,甚至是語句。
CtModel
的方法允許我們找到給定類型的元素並使用訪問者模式樣式的回調遍歷它。在我們的例子中,我們將使用這兩種方法來獲取方法計數:
MethodSummary report = new MethodSummary();
model.filterChildren((el) -> el instanceof CtClass<?>)
.forEach((CtClass<?> clazz) -> processMethods(report, clazz));
首先,我們使用filterChildren()
返回一個僅匹配模型中CtClass
元素的CtQuery
實例。接下來,我們使用forEach().
參數是一個 lambda 函數,它調用processMethods()
以使用類似的模式評估類的方法:
private void processMethods(MethodSummary report, CtClass<?> ctClass) {
ctClass.filterChildren((c) -> c instanceof CtMethod<?> )
.forEach((CtMethod<?> m) -> {
if (m.isPublic()) {
report.addPublicMethod();
}
else if ( m.isPrivate()) {
report.addPrivateMethod();
}
else if ( m.isProtected()) {
report.addProtectedMethod();
}
else {
report.addPackagePrivateMethod();
}
});
}
在這裡,根元素是正在分析的類,我們將遍歷每個CtMethod
,根據其可見性更新報告計數器。
為了測試此代碼,我們將向其傳遞一個簡單的類(在線提供)並驗證我們是否為每個方法可見性獲得正確的計數:
@Test
public void whenGenerateReport_thenSuccess() {
ClassReporter reporter = new ClassReporter();
MethodSummary report = reporter.generateMethodSummaryReport("src/test/resources/spoon/SpoonClassToTest.java");
assertThat(report).isNotNull();
assertThat(report.getPackagePrivateMethodCount()).isEqualTo(1);
assertThat(report.getPublicMethodCount()).isEqualTo(1);
assertThat(report.getPrivateMethodCount()).isEqualTo(1);
}
如果解析的類有語法錯誤,此代碼也有效。例如,給定這個語法無效的類:
public class BrokenClass {
// Syntax error
pluvic void brokenMethod() {}
// Syntax error
protected void protectedMethod() thraws Exception {}
// Valid method
public void publicMethod() {}
}
我們仍然可以得到 public、protected 和 private 方法的正確答案。至於損壞的方法,內部表示會嘗試獲取盡可能多的信息。如果我們在processMethods(),
我們將能夠看到forEach()
最終將收到一個包含有關無效方法信息的CtMethod
。
3.3.轉換代碼
我們從buildModel()
獲得的CtModel
實例直接支持轉換。我們所要做的就是使用任何CtElement
派生對像中可用的增變器方法。例如,我們可以重命名一個由CtMethod
表示的方法,只需使用setSimpleName()
即可:
CtMethod method = ...
method.setSimpleName("newname");
讓我們寫一個簡單的例子,在每個類中添加一個帶有版權聲明的標準 Javadoc 註釋:
CtModel model = // ... model creation logic omitted
model.filterChildren((el) -> el instanceof CtClass<?>)
.forEach((CtClass<?> cl) -> {
CtComment comment = cl.getFactory()
.createComment("Copyright(c) 2023 etc", CommentType.JAVADOC);
cl.addComment(comment);
});
模型修改發生在傳遞給forEach
lambda 內部。我們從當前元素使用getFactory()
並使用它來創建一個新的CtComment
,它代表一個“分離的”元素。然後,我們使用addComment()
將此評論添加到類中。
該模式與更改其他代碼方面相同。我們可以通過首先創建相應的CtElement
然後使用其中一個可用的修改器將其插入適當的位置來添加任何語言。
完成轉換後,我們使用setOutputDirectory()
和prettyprint()
將模型寫回文件系統:
spoon.setSourceOutputDirectory("./target");
spoon.prettyprint();
生成的代碼現在將在類聲明之前包含一個註釋塊:
// ... package and import declarations omitted
/**
* Copyright(c) 2023 etc
*/
public class SpoonClassToTest {
// ... class code omitted
}
3.4.使用處理器
在前面的示例中,代碼檢查和修改以一種特別的方式發生:我們得到一個模型實例並開始擺弄它。 Spoon 支持一種更結構化的方式來使用Processor
遍歷代碼。
這種方法的主要優點是它易於組合,並允許主要處理序列與分析/轉換代碼隔離開來。讓我們通過將版權示例重寫為Processor
來在實踐中展示這種方法:
public class AddCopyrightProcessor extends AbstractProcessor<CtClass<?>> {
@Override
public void process(CtClass<?> clazz) {
CtComment comment = getFactory().createComment("Copyright(c) 2023 etc", CommentType.JAVADOC);
clazz.addComment(comment);
}
}
Processor
接口有幾個方法,但 Spoon 提供了一個方便的基類,我們可以擴展它: AbstractProcessor
。這個類實現了 Spoon 所需的一切,但我們仍然必須實現一個方法: process().
Spoon 將在模型處理階段為模型中的每個匹配元素調用此方法。
現在,我們必須使用 SpoonAPI 中提供的addProcessor()
方法通知 Spoon 我們的處理器:
spoon.addProcessor(new AddCopyrightProcessor());
最後,我們可以像以前一樣運行 Spoon。然而,這一次,頂層代碼不必顯式調用處理代碼:
spoon.addInputResource("src/test/resources/spoon/SpoonClassToTest.java");
spoon.setSourceOutputDirectory("./target/spoon-processed");
spoon.buildModel();
spoon.process();
spoon.prettyprint();
事實上,這段代碼幾乎與 Spoon 在命令行中使用的代碼相同。
3.5.調音勺的Environment
Spoon 有許多處理選項,我們可以調整它們以滿足我們的需要。開箱即用,這些選項採用合理的默認值,因此通常我們可以保持不變。這是這些選項的簡要列表:
- 啟用/禁用嚴格的語法檢查
- Java 合規級別
- 源文件編碼
- 日誌設置
- 源代碼輸出位置
- Java 輸出編寫器實現
要更改這些選項中的任何一個,我們首先使用getEnvironment()
來訪問 Spoon 的Environment
,然後使用它來修改我們想要自定義的選項。例如,這就是我們在生成的文件中使用製表符而不是空格的方式:
spoon.getEnvironment().useTabulations(true);
另一個有趣的用例是替換默認的 Java 代碼生成器。 Spoon 附帶了另一種替代方法,它在生成輸出時嘗試盡可能多地保留原始代碼,稱為SniperJavaPrettyPrinter
。
這個生成器的主要優點是它生成的代碼與原始代碼相比,僅在處理器進行更改的地方有所不同。為了替換默認生成器,我們使用setPrettyPrintGenerator(),
它為 Spoon 將使用的PrettyPrinter
獲取一個Supplier
:
spoon.getEnvironment().setPrettyPrinterCreator(() -> new SniperJavaPrettyPrinter(spoon.getEnvironment()));
4。結論
在本文中,我們展示瞭如何使用 Spoon 庫來分析和修改 Java 源代碼。
像往常一樣,完整的代碼可以在 GitHub 上找到。