Unlock Java Code Analysis with Spoon: Real‑World Spring Boot 3 Cases

This article introduces the open‑source Spoon library for Java source‑code analysis and transformation, demonstrates how to integrate it with Spring Boot 3, and provides step‑by‑step examples—including visual AST inspection, empty‑catch detection, architecture rule validation, field usage checks, reflection replacement, and automatic code modifications such as adding fields, constructors, logging, and null‑checks.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Unlock Java Code Analysis with Spoon: Real‑World Spring Boot 3 Cases

1. Introduction

Spoon is a powerful open‑source Java library (maintained by Inria and part of the OW2 consortium) that can parse Java source files into a well‑structured abstract syntax tree (AST), offering APIs for analysis, rewriting, transformation, and even compilation. It supports modern Java versions up to Java 20.

2. Practical Cases

2.1 Adding the Maven Dependency

<dependency>
  <groupId>fr.inria.gforge.spoon</groupId>
  <artifactId>spoon-core</artifactId>
  <version>11.2.1</version>
</dependency>

The chosen version works with Java 20, but you can also use it with earlier language levels.

2.2 Quick Start – Visualising the AST

Given a simple class:

public class MyClass {
  public void eat() {
    System.err.println("eat...");
  }
  public static void say() {}
  public static void main(String[] args) {
    System.err.println("MyClass...");
  }
}

Running Spoon with the GUI flags displays the AST graphically:

-i src/main/java/com/pack/MyClass.java --gui

2.3 Detecting Empty catch Blocks

Spoon processes code through processors that visit each AST element. The following processor reports any catch clause without statements:

public class CatchProcessor extends AbstractProcessor<CtCatch> {
  @Override
  public void process(CtCatch element) {
    Environment env = getFactory().getEnvironment();
    env.setLevel("DEBUG");
    if (element.getBody().getStatements().size() == 0) {
      env.report(this, Level.WARN, element, "empty catch clause");
    }
  }
}

Run it with:

-i src/main/java/com/pack/spoon -p com.pack.spoon.CatchProcessor

The execution prints the locations of all empty catch blocks (see screenshot).

2.4 Validating Architecture Rules – Javadoc on Public Methods

Architecture rules (constraints that cannot be expressed directly in code) can be encoded as AST checks. The example below scans a project for public methods lacking a sufficiently long Javadoc comment:

@Test
public void testDocumentation() throws Exception {
  SpoonAPI spoon = new Launcher();
  spoon.addInputResource("src/main/java/com/pack/spoon2");
  spoon.buildModel();
  List<String> notDocumented = new ArrayList<>();
  for (CtMethod<?> method : spoon.getModel().getElements(new TypeFilter<>(CtMethod.class))) {
    if (method.hasModifier(ModifierKind.PUBLIC) && method.getTopDefinitions().size() == 0) {
      if (method.getDocComment().length() < 20) {
        notDocumented.add(method.getParent(CtType.class).getQualifiedName() + "#" + method.getSignature());
      }
    }
  }
  if (!notDocumented.isEmpty()) {
    System.err.printf("%s public methods should have proper Javadoc:%n%s", notDocumented.size(), StringUtils.join(notDocumented, "
"));
  }
}

Running the test produces:

1 public methods should have proper Javadoc:
com.pack.spoon2.OrderService#createOrder(com.pack.spoon2.Order)

2.5 Checking Unused Private Fields

The following test verifies that every private field (except serialization IDs) is read at least once:

@Test
public void testUnusedPrivateField() throws Exception {
  // 1. Build the model
  SpoonAPI spoon = new Launcher();
  spoon.addInputResource("src/main/java/com/pack/spoon2");
  CtModel model = spoon.buildModel();
  // 2. Collect all private fields
  List<CtField<?>> fields = model.getElements(new TypeFilter<>(CtField.class));
  fields.removeIf(v -> !v.isPrivate());
  fields.removeIf(v -> v.getSimpleName().equals("serialVersionUID"));
  // 3. Find all field‑read accesses
  List<CtFieldRead<?>> reads = model.getElements(new TypeFilter<>(CtFieldRead.class));
  reads.removeIf(v -> v.getVariable().getFieldDeclaration() == null);
  Set<CtField<?>> readSet = reads.stream()
      .map(CtFieldRead::getVariable)
      .map(CtVariableReference::getFieldDeclaration)
      .collect(Collectors.toSet());
  // 4. Determine fields that were never read
  fields.removeAll(readSet);
  System.err.printf("These fields are never used: %s%n", fields);
}

Output example:

These fields are never used: [private java.lang.String key = "pack";]

2.6 Replacing the Reflection API

Spoon can act as a substitute for Java reflection. Instead of Class<?>, you work with CtClass objects:

CtType<MyClass> ct = new TypeFactory().get(MyClass.class);
System.out.println(ct.getSimpleName());

Meta‑model introspection is also possible:

Set<CtType<?>> all = Metamodel.getAllMetamodelInterfaces();
for (CtType<?> t : all) {
  System.out.println(t.getMethods());
}

2.7 Code Transformation – Adding a Field and Constructor

The processor below creates a List<Date> dates field and a matching constructor, then injects them into the target class:

public class ClassProcessor extends AbstractProcessor<CtClass<?>> {
  @Override
  public void process(CtClass<?> ctClass) {
    // Create field type references
    CtTypeReference<Date> dateRef = getFactory().Code().createCtTypeReference(Date.class);
    CtTypeReference<List<Date>> listRef = getFactory().Code().createCtTypeReference(List.class);
    listRef.addActualTypeArgument(dateRef);
    // Create field
    CtField<List<Date>> datesField = getFactory().Core().createField();
    datesField.setType(listRef);
    datesField.addModifier(ModifierKind.PRIVATE);
    datesField.setSimpleName("dates");
    // Create constructor with parameter
    CtCodeSnippetStatement stmt = getFactory().Code().createCodeSnippetStatement("this.dates = dates");
    CtBlock<?> ctorBody = getFactory().Code().createCtBlock(stmt);
    CtParameter<List<Date>> param = getFactory().Core().createParameter();
    param.setType(listRef);
    param.setSimpleName("dates");
    CtConstructor<?> ctor = getFactory().Core().createConstructor();
    ctor.setBody(ctorBody);
    ctor.setParameters(Collections.singletonList(param));
    ctor.addModifier(ModifierKind.PUBLIC);
    // Apply changes
    ctClass.addField(datesField);
    ctClass.addConstructor(ctor);
  }
}

Run with:

-i src/main/java/com/pack/Order.java -p com.pack.ClassProcessor

The generated source (shown in the screenshot) now contains the new field and constructor.

2.8 Adding Logging Statements to Methods

This processor inserts a log line at the beginning of every method:

public class LogProcessor extends AbstractProcessor<CtExecutable<?>> {
  @Override
  public void process(CtExecutable<?> element) {
    CtCodeSnippetStatement snippet = getFactory().Core().createCodeSnippetStatement();
    String value = String.format(
        "System.out.println(\"Enter in the method %s from class %s\")",
        element.getSimpleName(),
        element.getParent(CtClass.class).getSimpleName());
    snippet.setValue(value);
    if (element.getBody() != null) {
      element.getBody().insertBegin(snippet);
    }
  }
}

After applying it, each method starts with a line such as:

System.out.println("Enter in the method eat from class MyClass");

2.9 Adding Non‑Null Checks for Object Parameters

public class NotNullCheckProcessor extends AbstractProcessor<CtParameter<?>> {
  @Override
  public boolean isToBeProcessed(CtParameter<?> element) {
    return !element.getType().isPrimitive();
  }
  @Override
  public void process(CtParameter<?> element) {
    CtCodeSnippetStatement snippet = getFactory().Core().createCodeSnippetStatement();
    String value = String.format(
        "if (%s == null) throw new IllegalArgumentException(\"非法参数\")",
        element.getSimpleName());
    snippet.setValue(value);
    CtExecutable<?> method = element.getParent(CtExecutable.class);
    if (method.getBody() != null) {
      method.getBody().insertBegin(snippet);
    }
  }
}

Running the processor on the example class produces methods that first validate their object arguments, e.g.:

if (order == null) throw new IllegalArgumentException("非法参数");

3. Conclusion

Spoon provides a versatile platform for static analysis, architecture‑rule enforcement, and automated source‑code transformation. Its AST‑centric API makes it suitable for integrating custom checks into continuous‑integration pipelines, replacing reflection in many scenarios, and performing large‑scale refactorings with minimal manual effort.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaASTBackend Developmentstatic analysiscode transformationSpoon
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.