Backend Development 21 min read

Understanding Lombok: Introduction, Compilation Principles, and a Manual @Getter Implementation

This article introduces Lombok, explains how it leverages Java's annotation‑processing (JSR‑269) and abstract syntax tree (AST) modifications to generate boilerplate code at compile time, and provides a step‑by‑step guide to manually implement a @Getter annotation processor with full Maven project examples.

政采云技术
政采云技术
政采云技术
Understanding Lombok: Introduction, Compilation Principles, and a Manual @Getter Implementation

1. Introduction to Lombok

Project Lombok is a Java library that automatically inserts boilerplate code during compilation, allowing developers to avoid writing getters, setters, equals, hashCode, and other repetitive methods by simply adding annotations.

1.1 What Lombok Is

Official description: Lombok is a Java library that enhances the Java compiler and IDEs by generating common code constructs at compile time.

1.2 Lombok Annotation Features

Annotation

Function

@Getter

@Setter

Generates getter and setter methods for fields or the whole class; default access level is public.

@NoArgsConstructor

@AllArgsConstructor

@RequiredArgsConstructor

Generates a no‑arg, all‑arg, or required‑args constructor. When

staticName

is set, a private constructor is created and a public static factory method is generated. The

access

attribute controls visibility (default public).

@ToString

Generates a

toString()

method. Options include printing field names, excluding fields, selecting specific fields, and including super‑class fields.

@Data

Combines @ToString, @EqualsAndHashCode, @Getter, @Setter, and @RequiredArgsConstructor.

@Builder

Creates a builder pattern implementation for the class, constructor, or method.

@Slf4j

Injects a static

org.slf4j.Logger

field named

log

into the class.

@SneakyThrows

Wraps method bodies with try‑catch blocks that re‑throw checked exceptions as unchecked, suppressing compiler warnings.

@Value

Creates an immutable value object: all fields are

final

, only getters are generated, and no setters are provided.

@Accessor

Generates custom accessor methods with configurable names, modifiers, and parameters.

@Cleanup

Automatically calls

close()

on resources at the end of the scope to prevent leaks.

2. Lombok Implementation Principles

2.1 Java Class Compilation Process

To understand Lombok, we first look at the standard Java compilation pipeline.

Define a PersonDTO.java class.

public class PersonDTO {
  // name
  private String name;
}

Javac parses the source file and builds an Abstract Syntax Tree (AST).

During compilation, the JSR‑269 annotation‑processing API is invoked.

The processor can modify the AST.

Javac then generates bytecode from the (potentially) modified AST.

The following diagram (omitted) illustrates the flow.

AST (Abstract Syntax Tree) is a tree representation of Java source code that maps syntactic elements to nodes, making it easier for tools to analyze and transform code.

For deeper study, see the JavaParser project on GitHub.

2.2 Introduction to JSR‑269

JSR‑269, the "Pluggable Annotation Processing API," is a Java specification introduced in Java 6 that allows developers to create custom annotation processors that run at compile time, inspecting, analyzing, and generating code.

Typical applications include:

Servlet/JAX‑RS code generation.

Spring Boot custom annotations (e.g., @Controller, @Service).

Hibernate JPA annotation handling.

Lombok’s own annotation processor that generates getters, setters, etc.

MapStruct object‑mapping code generation.

How to Implement a Custom Annotation Processor

Declare a custom annotation (e.g., @GetterTest ).

Implement Processor (or extend AbstractProcessor ) and override the process method.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author zcy1
 */
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface GetterTest {}
import com.google.auto.service.AutoService;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

/**
 * @author zcy1
 */
@AutoService(Processor.class) // register via Google auto‑service
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("GetterTest")
public class GetterProcessor extends AbstractProcessor {
    private Messager messager;
    private JavacTrees trees;
    private TreeMaker treeMaker;
    private Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public boolean process(Set
annotations, RoundEnvironment roundEnv) {
        Set
elements = roundEnv.getElementsAnnotatedWith(GetterTest.class);
        elements.forEach(e -> {
            JCTree tree = trees.getTree(e);
            tree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List
vars = List.nil();
                    for (JCTree jcTree : jcClassDecl.defs) {
                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
                            vars = vars.append((JCTree.JCVariableDecl) jcTree);
                        }
                    }
                    vars.forEach(v -> {
                        messager.printMessage(Diagnostic.Kind.NOTE, v.getName() + " has been processed");
                        jcClassDecl.defs = jcClassDecl.defs.prepend(createGetterMethod(v));
                    });
                    super.visitClassDef(jcClassDecl);
                }
            });
        });
        return true;
    }

    private JCTree.JCMethodDecl createGetterMethod(JCTree.JCVariableDecl var) {
        ListBuffer
stmts = new ListBuffer<>();
        // this.name = name;
        JCTree.JCExpressionStatement assign = makeAssignment(
            treeMaker.Select(treeMaker.Ident(names.fromString("this")), var.getName()),
            treeMaker.Ident(var.getName())
        );
        stmts.append(assign);
        JCTree.JCBlock block = treeMaker.Block(0, stmts.toList());
        // parameter (String name)
        JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), var.getName(), var.vartype, null);
        List
params = List.of(param);
        // return type void
        JCTree.JCExpression retType = treeMaker.Type(new Type.JCVoidType());
        // method name getXxx
        Name methodName = getMethodName(var.getName());
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), methodName, retType, List.nil(), params, List.nil(), block, null);
    }

    /** Convert field name to getter name */
    private Name getMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1));
    }

    private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
        return treeMaker.Exec(treeMaker.Assign(lhs, rhs));
    }
}

2.3 Lombok’s Internal Mechanism

Lombok combines an annotation processor with AST manipulation. During compilation, Lombok’s processor traverses the AST, finds elements annotated with Lombok annotations, and injects the required code (e.g., getters, setters) directly into the tree before bytecode generation.

Key internal classes include AnnotationProcessorHider (which hides Lombok’s processor from the compiler) and individual handler classes such as HandlerGetter that generate specific methods.

class AnnotationProcessorHider {
    public static class AstModificationNotifierData {
        public volatile static boolean lombokInvoked = false;
    }
    public static class AnnotationProcessor extends AbstractProcessor {
        @Override
        public Set
getSupportedOptions() { return instance.getSupportedOptions(); }
        @Override
        public Set
getSupportedAnnotationTypes() { return instance.getSupportedAnnotationTypes(); }
        @Override
        public SourceVersion getSupportedSourceVersion() { return instance.getSupportedSourceVersion(); }
        @Override
        public void init(ProcessingEnvironment processingEnv) {
            disableJava9SillyWarning();
            AstModificationNotifierData.lombokInvoked = true;
            instance.init(processingEnv);
            super.init(processingEnv);
        }
        @Override
        public boolean process(Set
annotations, RoundEnvironment roundEnv) {
            return instance.process(annotations, roundEnv);
        }
    }
}

2.4 Manual Implementation of a @Getter Annotation

2.4.1 Create a Maven Multi‑module Project (getter / getter‑use)

The getter module contains the custom annotation and processor; the getter-use module depends on it and demonstrates usage.

2.4.2 Maven POM for the getter Module

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>demo</artifactId>
    <groupId>com.example</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  <artifactId>getter</artifactId>
  <dependencies>
    <!-- Annotation processing dependency (tools.jar) -->
    <dependency>
      <groupId>com.sun</groupId>
      <artifactId>tools</artifactId>
      <version>1.6.0</version>
      <scope>system</scope>
      <systemPath>${java.home}/../lib/tools.jar</systemPath>
    </dependency>
    <!-- Auto‑service for automatic registration -->
    <dependency>
      <groupId>com.google.auto.service</groupId>
      <artifactId>auto-service</artifactId>
      <version>1.0.1</version>
    </dependency>
  </dependencies>
</project>

2.4.3 Custom @GetterTest Annotation and Processor (shown above)

2.4.4 Using the Annotation in getter‑use Module

The getter-use module adds a dependency on the getter module.

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
  <parent>
    <artifactId>demo</artifactId>
    <groupId>com.example</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  <artifactId>getter-use</artifactId>
  <dependencies>
    <dependency>
      <groupId>com.example</groupId>
      <artifactId>getter</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </dependency>
  </dependencies>
</project>

Applying the annotation:

@GetterTest
public class PersonDTO {
  private String name;
  private Integer age;
}

After compilation, the generated getName() and getAge() methods are visible in the compiled PersonDTO.class file.

3. Summary

The article presented an overview of Lombok, detailed the Java compilation pipeline, explained how JSR‑269 enables Lombok’s annotation processing, and demonstrated a hands‑on implementation of a custom @Getter processor. While Lombok greatly reduces boilerplate, developers should be aware of its drawbacks, such as reduced source‑code visibility and potential issues when combining annotations (e.g., @Data with @Builder).

Problem

Solution

@Data and @Builder together remove the no‑arg constructor

Add @AllArgsConstructor and @NoArgsConstructor manually.

@Builder makes default field values ineffective

Annotate the field with @Builder.Default.

@Data generated toString() does not include super‑class fields

Add @ToString(callSuper = true) on the subclass.

References

Lombok official site: https://projectlombok.org

JavaParser source: https://github.com/javaparser/javaparser

AST deep dive: https://www.freesion.com/article/4068581927/

Javacode generationASTMavenannotation-processingLombok
政采云技术
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.