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获取。


原始标题:Analyze, Generate and Transform Code Using Spoon in Java | Baeldung