1. 介绍
本教程将展示如何使用Spoon库来解析、分析和转换Java源代码。
2. Spoon概述
处理大型代码库时,我们常需要根据特定目的解析它们。典型场景包括:
- 生成聚合报告
- 查找特定类的使用情况,包括通过复杂继承链的间接使用
- 发现潜在安全漏洞
- 自动化重构
这个列表还能继续扩展,但所有任务都遵循一个共同模式:首先扫描现有代码并构建内部表示;其次使用访问者模式或查询机制定位目标元素;最后生成所需输出。
Spoon库专注于前两个步骤,让我们能专注于产生结果。
✅ 简单粗暴的对比:虽然基于文本的shell或Python脚本能解决部分用例,但这种方法缺乏对代码的深度理解,限制了分析能力。而Spoon会创建完整的内存模型,支持多种遍历方式。
⚠️ 关键优势:Spoon底层使用Eclipse JDT编译器解析代码,生成包含类、方法、语句甚至注释的"高保真"模型。它还能处理语法无效的代码且不关心缺失依赖,这对分析数百个遗留代码仓库特别有用。
3. 使用Spoon
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类,生成包含public/private/protected方法数量的报告。
SpoonAPI
接口是主要入口点,通过Launcher
获取实例:
SpoonAPI spoon = new Launcher();
通过addInputResource()
指定源码位置:
spoon.addInputResource("some/directory/SomeClass.java");
此方法接受单个类或目录路径(递归解析所有Java文件),可多次调用以同时解析多个仓库。
使用buildModel()
创建包含所有代码信息的CtModel
实例:
CtModel model = spoon.buildModel();
CtModel
类似XML处理中的Document
类,是元素树的根节点。元素可以是类、方法、包、变量声明甚至语句。
通过filterChildren()
和forEach()
组合获取方法统计:
MethodSummary report = new MethodSummary();
model.filterChildren((el) -> el instanceof CtClass<?>)
.forEach((CtClass<?> clazz) -> processMethods(report, clazz));
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();
}
});
}
测试代码(传入简单类)验证统计准确性:
@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 {
// 语法错误
pluvic void brokenMethod() {}
// 语法错误
protected void protectedMethod() thraws Exception {}
// 有效方法
public void publicMethod() {}
}
public/protected/private方法统计依然正确。在processMethods()
中打断点会发现,无效方法也会被表示为CtMethod
传递给forEach()
。
3.3. 转换代码
CtModel
实例直接支持转换。只需调用CtElement
派生对象的修改方法,例如用setSimpleName()
重命名方法:
CtMethod method = ...
method.setSimpleName("newname");
示例:为每个类添加版权声明Javadoc注释:
CtModel model = // ... 模型创建逻辑省略
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();
生成代码会在类声明前包含注释块:
// ... 包和导入声明省略
/**
* Copyright(c) 2023 etc
*/
public class SpoonClassToTest {
// ... 类代码省略
}
3.4. 使用处理器
前文的代码检查和修改是临时性的:获取模型后直接操作。Spoon支持更结构化的遍历方式——使用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
。只需实现process()
方法——Spoon会在处理阶段为每个匹配元素调用它。
通过SpoonAPI的addProcessor()
注册处理器:
spoon.addProcessor(new AddCopyrightProcessor());
运行方式不变,但顶层代码无需显式调用处理逻辑:
spoon.addInputResource("src/test/resources/spoon/SpoonClassToTest.java");
spoon.setSourceOutputDirectory("./target/spoon-processed");
spoon.buildModel();
spoon.process();
spoon.prettyprint();
这与Spoon命令行工具的执行逻辑几乎一致。
3.5. 调整Spoon环境
Spoon提供多个可配置选项,默认值通常合理。主要选项包括:
- 启用/禁用严格语法检查
- Java合规级别
- 源文件编码
- 日志设置
- 源码输出位置
- Java输出写入器实现
通过getEnvironment()
获取Environment
实例后修改选项。例如用制表符替代空格:
spoon.getEnvironment().useTabulations(true);
另一个实用场景是替换默认代码生成器。Spoon提供SniperJavaPrettyPrinter
,它能最大程度保留原始代码格式,仅修改处理器变更的部分。通过setPrettyPrinterCreator()
替换:
spoon.getEnvironment().setPrettyPrinterCreator(() -> new SniperJavaPrettyPrinter(spoon.getEnvironment()));
4. 结论
本文展示了如何使用Spoon库分析和修改Java源代码。完整代码可在GitHub获取。