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.
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 --gui2.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.CatchProcessorThe 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.ClassProcessorThe 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
