Backend Development 17 min read

Understanding Java Annotation Processing (APT) and Implementing a Custom Lombok‑like Processor

This article explains the principles behind Java annotation processing tools such as APT, compares APT with AOP and Java agents, and provides a step‑by‑step guide to creating a custom Lombok‑style annotation processor that generates getters, setters, and toString methods at compile time.

政采云技术
政采云技术
政采云技术
Understanding Java Annotation Processing (APT) and Implementing a Custom Lombok‑like Processor

1 Background

In Java development we often rely on frameworks like Lombok, MapStruct, or MyBatis‑Plus that generate methods through a few annotations; understanding the underlying mechanism of these tools is therefore essential.

2 Bytecode Generation Principle

2.1 APT (Annotation Processing Tool) Processor

Based on the JSR‑269 (Pluggable Annotation Processing API) specification, APT provides a plug‑in interface for annotation processing. Since Java 6 it can process source code during compilation, allowing plugins to read, modify, or add any element in the abstract syntax tree (AST).

When an annotation processor modifies the AST, the Java compiler (javac) restarts the parsing and symbol‑table filling phases until the modifications are complete, after which compilation proceeds.

2.2 AbstractProcessor Usage

Creating an annotation processor involves the following steps:

Define an annotation class, e.g., @Data .

Implement a class that extends AbstractProcessor , the core of APT.

Write the code that generates the desired bytecode.

Configure the Service Provider Interface (SPI) by creating a file META-INF/services/javax.annotation.processing.Processor that lists the processor implementation.

2.3 APT, AOP, JavaAgent – Advantages and Disadvantages

In everyday development, AOP is not the only way to implement cross‑cutting concerns; APT can also be used, supporting static and private methods with higher stability and broader coverage.

2.4 Lombok Principle

1. APT (Annotation Processing Tool) 2. javac API processes the AST (abstract syntax tree)

The overall principle is illustrated in the following diagram:

To analyse Lombok’s implementation, start from the Processor and AnnotationProcessor classes, then examine the lombok.javac.JavacAnnotationHandler methods.

3 Implementing Your Own Lombok

3.1 Creating the @Data Annotation

@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
public @interface Data {
}

The @Data annotation is only available at compile time and cannot be accessed at runtime.

2.2 Custom Annotation Processor

By implementing the Processor interface you can create a custom processor; the example below extends AbstractProcessor and overrides the abstract process method.

2.2.1 APT Simple Introduction

@SupportedAnnotationTypes({"com.nicky.lombok.annotation.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DataProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) { }
    @Override
    public boolean process(Set
annotations, RoundEnvironment roundEnv) { }
}

@SupportedAnnotationTypes declares which annotations the processor handles; @SupportedSourceVersion specifies the Java version.

If you do not use these annotations, you can still override the corresponding methods:

Set
getSupportedAnnotationTypes();
SourceVersion getSupportedSourceVersion();

init method – used to initialise context information.

Mainly for initialising context and other information.

process method – contains the business logic for handling the annotations.

Specific method‑level processing logic.

2.2.2 Concrete Implementation

Override init method

/** Abstract syntax tree */
private JavacTrees trees;
/** AST */
private TreeMaker treeMaker;
/** Identifier */
private Names names;
/** Logger */
private Messager messager;
private Filer filer;
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    this.trees = JavacTrees.instance(processingEnv);
    Context context = ((JavacProcessingEnvironment)processingEnv).getContext();
    this.treeMaker = TreeMaker.instance(context);
    messager = processingEnvironment.getMessager();
    this.names = Names.instance(context);
    filer = processingEnvironment.getFiler();
}

Key member variables:

JavacTrees – current Java syntax‑tree instance.

TreeMaker – creates or modifies AST nodes.

Names – utility for handling identifiers.

Messager – prints compiler messages.

Filer – handles file generation.

Note: Using AST manipulation requires the local tools.jar package.

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

Override process method

@Override
public boolean process(Set
annotations, RoundEnvironment roundEnv) {
    Set
annotation = roundEnv.getElementsAnnotatedWith(Data.class);
    annotation.stream()
        .map(element -> trees.getTree(element))
        .forEach(tree -> tree.accept(new TreeTranslator() {
            @Override
            public void visitClassDef(JCClassDecl jcClass) {
                // filter fields
                Map
treeMap = jcClass.defs.stream()
                    .filter(k -> k.getKind().equals(Tree.Kind.VARIABLE))
                    .map(t -> (JCVariableDecl) t)
                    .collect(Collectors.toMap(JCVariableDecl::getName, Function.identity()));
                // generate getter, setter, toString
                treeMap.forEach((k, jcVariable) -> {
                    messager.printMessage(Diagnostic.Kind.NOTE, String.format("fields:%s", k));
                    try {
                        jcClass.defs = jcClass.defs.prepend(generateGetterMethod(jcVariable));
                        jcClass.defs = jcClass.defs.prepend(generateSetterMethod(jcVariable));
                    } catch (Exception e) {
                        messager.printMessage(Diagnostic.Kind.ERROR, Throwables.getStackTraceAsString(e));
                    }
                });
                jcClass.defs = jcClass.defs.prepend(generateToStringBuilderMethod());
                super.visitClassDef(jcClass);
            }
            @Override
            public void visitMethodDef(JCMethodDecl jcMethod) {
                messager.printMessage(Diagnostic.Kind.NOTE, jcMethod.toString());
                if ("getTest".equals(jcMethod.getName().toString())) {
                    result = treeMaker.MethodDef(jcMethod.getModifiers(), getNameFromString("testMethod"), jcMethod.restype,
                        jcMethod.getTypeParameters(), jcMethod.getParameters(), jcMethod.getThrows(),
                        jcMethod.getBody(), jcMethod.defaultValue);
                }
                super.visitMethodDef(jcMethod);
            }
        }));
    return true;
}

The above logic implements getter, setter, and toString generation.

Getter Method Implementation

private JCMethodDecl generateGetterMethod(JCVariableDecl jcVariable) {
    JCModifiers jcModifiers = treeMaker.Modifiers(Flags.PUBLIC);
    Name methodName = handleMethodSignature(jcVariable.getName(), "get");
    ListBuffer
jcStatements = new ListBuffer<>();
    jcStatements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(getNameFromString("this")), jcVariable.getName())));
    JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());
    JCExpression returnType = jcVariable.vartype;
    return treeMaker.MethodDef(jcModifiers, methodName, returnType, List.nil(), List.nil(), List.nil(), jcBlock, null);
}

Setter Method Implementation

private JCMethodDecl generateSetterMethod(JCVariableDecl jcVariable) throws ReflectiveOperationException {
    JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
    Name variableName = jcVariable.getName();
    Name methodName = handleMethodSignature(variableName, "set");
    ListBuffer
jcStatements = new ListBuffer<>();
    jcStatements.append(treeMaker.Exec(treeMaker.Assign(
        treeMaker.Select(treeMaker.Ident(getNameFromString("this")), variableName),
        treeMaker.Ident(variableName))));
    JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());
    JCExpression returnType = treeMaker.Type((Type)(Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance()));
    JCVariableDecl variableDecl = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER, List.nil()), jcVariable.name, jcVariable.vartype, null);
    List
parameters = List.of(variableDecl);
    return treeMaker.MethodDef(modifiers, methodName, returnType, List.nil(), parameters, List.nil(), jcBlock, null);
}

toString Method Implementation

private JCMethodDecl generateToStringBuilderMethod() {
    JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
    Name methodName = getNameFromString("toString");
    JCExpressionStatement statement = treeMaker.Exec(treeMaker.Apply(List.of(memberAccess("java.lang.Object")),
        memberAccess("com.nicky.lombok.adapter.AdapterFactory.builderStyleAdapter"),
        List.of(treeMaker.Ident(getNameFromString("this"))));
    ListBuffer
jcStatements = new ListBuffer<>();
    jcStatements.append(treeMaker.Return(statement.getExpression()));
    JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());
    JCExpression returnType = memberAccess("java.lang.String");
    return treeMaker.MethodDef(modifiers, methodName, returnType, List.nil(), List.nil(), List.nil(), jcBlock, null);
}

private JCExpression memberAccess(String components) {
    String[] componentArray = components.split("\\.");
    JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0]));
    for (int i = 1; i < componentArray.length; i++) {
        expr = treeMaker.Select(expr, getNameFromString(componentArray[i]));
    }
    return expr;
}

private Name handleMethodSignature(Name name, String prefix) {
    return names.fromString(prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, name.toString()));
}

private Name getNameFromString(String s) {
    return names.fromString(s);
}

The processor is loaded via SPI; the example uses Google’s auto-service library to generate the service file.

<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service</artifactId>
    <version>1.0-rc4</version>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.google.auto</groupId>
    <artifactId>auto-common</artifactId>
    <version>0.10</version>
    <optional>true</optional>
</dependency>

Annotate the processor class with @AutoService(Processor.class) :

@SupportedAnnotationTypes({"com.nicky.lombok.annotation.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class DataProcessor extends AbstractProcessor {
}

After building the project with mvn clean install , the generated JAR can be added as a local Maven dependency:

<dependency>
    <groupId>com.nicky</groupId>
    <artifactId>lombok-enchance</artifactId>
    <version>1.0.4</version>
</dependency>

Using the custom annotation on a class:

@Data
public class LombokTest {
    private String name;
    private int age;
    public LombokTest(String name) { this.name = name; }
    public static void main(String[] args) {
        LombokTest t = new LombokTest("nicky");
        t.age = 18;
        System.out.println(t.toString());
    }
}

Compilation produces the expected getter, setter, and toString methods, confirming that the processor works as intended.

4 References

Modifying the AST at compile time (https://blog.csdn.net/a_zhenzhen/article/details/86065063#JCTree%E7%9A%84%E4%BB%8B%E7%BB%8D)

tools.jar source documentation (https://searchcode.com/file/40279168/src/com/sun/tools/javac/)

JSR‑269 specification (https://www.jcp.org/en/jsr/detail?id=269)

JavacompilerLombokAnnotationProcessingAPTBytecodeGeneration
政采云技术
Written by

政采云技术

ZCY Technology Team (Zero), based in Hangzhou, is a growth-oriented team passionate about technology and craftsmanship. With around 500 members, we are building comprehensive engineering, project management, and talent development systems. We are committed to innovation and creating a cloud service ecosystem for government and enterprise procurement. We look forward to your joining us.

0 followers
Reader feedback

How this landed with the community

login 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.