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.
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
staticNameis set, a private constructor is created and a public static factory method is generated. The
accessattribute 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.Loggerfield named
loginto 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/
政采云技术
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.
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.