Lombok 的 @Data 到底怎么工作的?Java 注解处理器实战全链路拆解
面试常考:AbstractProcessor、RoundEnvironment、编译期代码生成、Lombok 原理
一、从 Lombok 的魔法说起
java
@Data
public class User {
private String name;
private int age;
}
// 编译后自动生成了 getter/setter/equals/hashCode/toString
// 这些代码在源文件里完全看不到,怎么做到的?
Lombok 完全不修改你的源文件,却在编译后生成了 getter、setter、equals 等方法。这是怎么实现的?答案就是注解处理器(Annotation Processor)------Java 编译器提供的编译期代码生成机制。
二、注解处理器的位置
scss
┌──────────────────────────────────────────────────────────┐
│ Java 编译流程 │
│ │
│ .java 源码 │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 词法/语法分析 │ │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ 生成 AST │────▶│ 注解处理器 (多轮) │ │
│ │ (抽象语法树) │◀────│ • 读取注解 │ │
│ └──────┬──────┘ │ • 生成新类/修改AST │ │
│ │ │ • 输出 .java 文件 │ │
│ │ └──────────────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 语义分析 │ ← 新生成的类也会被编译 │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 生成 .class │ │
│ └─────────────┘ │
│ │
│ 关键: 注解处理器在编译期运行,不是运行时 │
│ 处理器可以生成新文件,但不能修改已生成的AST结构 │
│ (Lombok 用了非公开API绕过此限制) │
└──────────────────────────────────────────────────────────┘
注解处理器 vs 运行时注解
| 维度 | 注解处理器 (编译期) | 运行时注解 |
|---|---|---|
| 执行时机 | 编译期 | 运行时 |
| 操作能力 | 生成新类/代码 | 反射读取元数据 |
| 性能影响 | 零运行时开销 | 反射开销 |
| 典型应用 | Lombok, MapStruct, Dagger | Spring, Jackson |
| 依赖 | 编译时 | 运行时 |
三、编写第一个注解处理器
1. 定义注解
java
package com.example.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE) // 只能标注在类上
@Retention(RetentionPolicy.SOURCE) // 源码级保留 (编译后丢弃)
public @interface Builder {
String prefix() default "";
}
2. 实现处理器
java
package com.example.processor;
import com.example.annotation.Builder;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.util.*;
import java.io.*;
@SupportedAnnotationTypes("com.example.annotation.Builder") // 处理的注解
@SupportedSourceVersion(SourceVersion.RELEASE_17) // 支持的 Java 版本
public class BuilderProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
// env 提供工具:
// Filer: 生成文件
// Messager: 报错/警告
// Types: 类型操作
// Elements: 元素操作
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
// 每一轮处理都会调用此方法
// roundEnv.getElementsAnnotatedWith: 获取被注解标注的元素
for (Element element : roundEnv.getElementsAnnotatedWith(Builder.class)) {
if (element.getKind() != ElementKind.CLASS) {
error(element, "@Builder 只能标注在类上");
continue;
}
TypeElement typeElement = (TypeElement) element;
generateBuilder(typeElement);
}
return true; // true: 已处理,其他处理器不再处理
}
private void generateBuilder(TypeElement typeElement) {
String className = typeElement.getSimpleName().toString();
String builderClassName = className + "Builder";
String packageName = processingEnv.getElementUtils()
.getPackageOf(typeElement).getQualifiedName().toString();
// 收集字段
List<VariableElement> fields = new ArrayList<>();
for (Element enclosed : typeElement.getEnclosedElements()) {
if (enclosed.getKind() == ElementKind.FIELD)
fields.add((VariableElement) enclosed);
}
// 生成 Builder 代码
String source = generateSource(packageName, className, builderClassName, fields);
writeFile(packageName, builderClassName, source);
}
private String generateSource(String pkg, String className,
String builderName, List<VariableElement> fields) {
StringBuilder sb = new StringBuilder();
sb.append("package ").append(pkg).append(";\n\n");
sb.append("public class ").append(builderName).append(" {\n");
// 字段
for (VariableElement f : fields) {
sb.append(" private ").append(f.asType()).append(" ")
.append(f.getSimpleName()).append(";\n");
}
sb.append("\n");
// setter 方法 (链式)
for (VariableElement f : fields) {
String name = f.getSimpleName().toString();
String type = f.asType().toString();
sb.append(" public ").append(builderName).append(" ")
.append(name).append("(").append(type).append(" val) {\n")
.append(" this.").append(name).append(" = val;\n")
.append(" return this;\n")
.append(" }\n\n");
}
// build 方法
sb.append(" public ").append(className).append(" build() {\n")
.append(" ").append(className).append(" obj = new ").append(className).append("();\n");
for (VariableElement f : fields) {
String name = f.getSimpleName().toString();
sb.append(" obj.set").append(capitalize(name))
.append("(this.").append(name).append(");\n");
}
sb.append(" return obj;\n")
.append(" }\n")
.append("}\n");
return sb.toString();
}
private void writeFile(String pkg, String name, String content) {
try {
JavaFileObject file = processingEnv.getFiler()
.createSourceFile(pkg + "." + name);
try (Writer w = file.openWriter()) {
w.write(content);
}
} catch (IOException e) {
error(null, "生成文件失败: " + e.getMessage());
}
}
private void error(Element e, String msg) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, e);
}
private String capitalize(String s) {
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
}
3. 注册处理器
在 META-INF/services/javax.annotation.processing.Processor 文件中注册:
com.example.processor.BuilderProcessor
或用 @AutoService 注解(Google AutoService 库)自动生成:
java
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor { ... }
4. 使用
java
@Builder
public class User {
private String name;
private int age;
}
// 编译后自动生成 UserBuilder:
User user = new UserBuilder()
.name("Alice")
.age(30)
.build();
四、AbstractProcessor 核心 API
process 方法:处理器的入口
java
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
// annotations: 本轮要处理的注解类型
// roundEnv: 本轮处理的环境
if (roundEnv.processingOver()) {
// 最后一轮,清理工作
return false;
}
// 获取被 @Builder 标注的所有元素
for (Element e : roundEnv.getElementsAnnotatedWith(Builder.class)) {
// Element 是编译期的 "反射" 模型
// TypeElement → 类/接口
// VariableElement → 字段/参数
// ExecutableElement → 方法/构造器
// PackageElement → 包
}
return true;
}
Element 层级模型
scss
Element 层级 (javax.lang.model.element):
┌──────────────────────────────────────────────────┐
│ │
│ Element (根接口) │
│ ├── PackageElement (包: com.example) │
│ ├── TypeElement (类/接口: User) │
│ │ ├── VariableElement (字段: name) │
│ │ ├── ExecutableElement (方法: getName()) │
│ │ │ ├── VariableElement (参数: p) │
│ │ └── TypeElement (内部类: Inner) │
│ └── TypeParameterElement (泛型参数: T) │
│ │
│ 关键方法: │
│ getEnclosingElement() → 父元素 │
│ getEnclosedElements() → 子元素 │
│ getKind() → 元素类型 │
│ getModifiers() → 修饰符 │
│ getAnnotation(Class) → 读取注解 │
│ asType() → TypeMirror 类型 │
└──────────────────────────────────────────────────┘
Filer:文件生成器
java
// 生成 Java 源文件
JavaFileObject sourceFile = filer.createSourceFile("com.example.GeneratedClass");
try (Writer w = sourceFile.openWriter()) {
w.write("package com.example;\n");
w.write("public class GeneratedClass {}\n");
}
// 生成资源文件
FileObject resource = filer.createResource(
StandardLocation.CLASS_OUTPUT,
"com.example",
"config.properties"
);
try (Writer w = resource.openWriter()) {
w.write("key=value\n");
}
// 生成 class 文件 (用 JavaPoet/JavaParser 更方便)
Messager:编译期报告
java
// 编译期错误/警告 (不影响源文件)
messager.printMessage(Diagnostic.Kind.ERROR, "字段类型不支持", element);
messager.printMessage(Diagnostic.Kind.WARNING, "建议使用包装类", element);
messager.printMessage(Diagnostic.Kind.NOTE, "生成: " + className);
编译期 ERROR 会阻止编译成功,就像 javac 自己报错一样。
五、用 JavaPoet 优雅生成代码
手动拼接字符串生成代码既繁琐又易错。Google 的 JavaPoet 提供了流畅的 API:
java
import com.squareup.javapoet.*;
// 生成 Builder 类
MethodSpec buildMethod = MethodSpec.methodBuilder("build")
.returns(ClassName.get(packageName, className))
.addStatement("$T obj = new $T()",
ClassName.get(packageName, className),
ClassName.get(packageName, className))
.addStatement("return obj")
.build();
TypeSpec builderClass = TypeSpec.classBuilder(builderName)
.addFields(fieldSpecs)
.addMethods(setterMethods)
.addMethod(buildMethod)
.build();
JavaFile file = JavaFile.builder(packageName, builderClass)
.indent(" ")
.build();
file.writeTo(filer); // 直接写入 Filer
JavaPoet 生成对比
java
// 手动拼接
sb.append("public void setName(String name) {\n");
sb.append(" this.name = name;\n");
sb.append(" return this;\n");
sb.append("}\n");
// JavaPoet
MethodSpec.name("setName", String.class)
.addStatement("this.$N = $N", "name", "name")
.addStatement("return this")
.build();
// JavaPoet 格式化占位符:
// $L → 字面量 (literal)
// $S → 字符串 (string, 自动加引号)
// $T → 类型 (type, 自动加 import)
// $N → 名称 (name, 引用已声明的方法/字段)
六、Lombok 原理:修改 AST
标准 API 的限制
标准注解处理器只能生成新文件 ,不能修改已有类的 AST。这意味着标准 API 做"@Data 自动加 getter"这样的事情是做不到的------因为 getter 要加到 User.java 对应的 AST 上。
Lombok 的做法
Lombok 使用了非公开 API,直接操作 javac 内部的 AST:
java
// Lombok 核心处理类 (简化)
public class HandleData {
public void handle(AnnotationValues<Data> annotation, JavacNode node) {
// 1. 获取 javac 内部的 AST 节点
ClassSymbol classSymbol = (ClassSymbol) node.getSymbol();
JCClassDecl classDecl = (JCClassDecl) node.get();
// 2. 直接在 AST 上添加方法 (非公开 API!)
JCMethodDecl getter = createGetter(field);
classDecl.defs = classDecl.defs.append(getter); // 直接修改AST
// 3. javac 继续编译时会看到新方法
}
}
objectivec
Lombok 工作原理:
┌──────────────────────────────────────────────────────┐
│ │
│ javac 编译流程: │
│ 解析 → AST → 注解处理 → 语义分析 → 生成class │
│ ↑ │
│ Lombok 在这里介入: │
│ 1. registerProcessor → 注册到 javac │
│ 2. 读取 @Data 注解 │
│ 3. 获取 javac 内部 AST (JCClassDecl) │
│ 4. 直接修改 AST: 添加 getter/setter/equals... │
│ 5. javac 继续编译时 "看到" 新方法 │
│ │
│ 关键: 使用非公开 API: │
│ com.sun.tools.javac.tree.JCTree │
│ com.sun.tools.javac.code.Symbol │
│ com.sun.tools.javac.processing.JavacProcessingEnvironment │
│ │
│ 风险: │
│ 1. 依赖内部 API,JDK 升级可能失效 │
│ 2. IDE 需要安装 Lombok 插件 │
│ 3. JDK 9+ 模块化限制内部API访问 │
│ 4. 非标准,可能与编译器优化冲突 │
│ │
└──────────────────────────────────────────────────────┘
为什么 Lombok 不用标准 API?
标准 API 只能生成新文件。@Data 要在 User.class 中添加 getter,要么改源文件(不优雅),要么改 AST(Lombok 的做法)。如果用标准 API,只能生成 UserDataExtensions 之类的辅助类,无法替换原始类。
七、注解处理器的实际应用
| 框架 | 用法 | 生成内容 |
|---|---|---|
| Lombok | @Data, @Builder | getter/setter/equals |
| MapStruct | @Mapper | DTO 转换实现类 |
| Dagger | @Inject, @Component | 依赖注入代码 |
| ButterKnife | @BindView | View 绑定代码 |
| Room | @Dao, @Entity | DB 操作实现类 |
| Hibernate JPA Model Gen | @Entity | 元模型类 |
MapStruct 示例
java
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserDTO toDTO(User entity);
User toEntity(UserDTO dto);
}
// 编译时 MapStruct 生成 UserMapperImpl:
public class UserMapperImpl implements UserMapper {
@Override
public UserDTO toDTO(User entity) {
if (entity == null) return null;
UserDTO dto = new UserDTO();
dto.setId(entity.getId());
dto.setName(entity.getName());
return dto;
}
// ...
}
八、面试高频问题速答
Q1: 注解处理器在编译的哪个阶段运行?
javac 解析完源码生成 AST 后,在语义分析之前运行。处理器可能运行多轮:每轮中如果生成了新的源文件,会触发下一轮处理,直到没有新文件生成为止。
Q2: process 方法的返回值什么意思?
返回 true 表示已处理该注解,后续处理器不再处理。返回 false 表示未处理或允许多处理器处理。实践中通常返回 true 以避免被其他处理器重复处理。
Q3: Lombok 为什么不用标准注解处理器 API?
标准 API 只能生成新文件,不能修改已有类的 AST。Lombok 需要直接在 User 类中加 getter/setter,必须修改 javac 内部的 AST 树。这使用了非公开 API(com.sun.tools.javac.tree),有 JDK 升级风险。
Q4: @Retention 对注解处理器有什么影响?
注解处理器需要用 @Retention(RetentionPolicy.SOURCE) 或 CLASS。RUNTIME 也可以被处理器读取,但如果只为编译期使用,SOURCE 最合适------编译后注解被丢弃,减少 class 文件体积。
Q5: 编译期怎么获取泛型类型信息?
用 TypeMirror 和 Types 工具。Element.asType() 返回 TypeMirror,可以判断是否是泛型、获取类型参数等。运行时因为类型擦除做不到,但编译期完整保留。
java
// 编译期获取泛型
TypeMirror fieldType = variableElement.asType();
if (fieldType.getKind() == TypeKind.DECLARED) {
DeclaredType dt = (DeclaredType) fieldType;
// List<String> → List + [String]
List<? extends TypeMirror> args = dt.getTypeArguments();
}
Q6: 注解处理器能修改已有源文件吗?
标准 API 不能。只能生成新文件。Lombok 通过直接操作 javac 内部 AST 绕过了此限制,但这不是标准做法。如果需要修改已有类,考虑用字节码工具(ASM、ByteBuddy)在编译后处理。
九、总结
yaml
注解处理器核心知识:
┌──────────────────────────────────────────────────┐
│ │
│ 机制: javac 编译期钩子,多轮处理 │
│ API: AbstractProcessor + Filer + Messager │
│ 模型: Element (类/字段/方法) + TypeMirror (类型)│
│ 能力: 生成新 .java 文件 (不能修改已有源文件) │
│ │
│ Lombok 特殊: │
│ 使用非公开 API 直接修改 AST │
│ 在已有类中插入 getter/setter 等 │
│ 代价: 依赖 javac 内部 API,JDK 升级风险 │
│ │
│ 标准应用: │
│ MapStruct → DTO 转换 │
│ Dagger → 依赖注入 │
│ Room → DAO 实现 │
│ │
│ 代码生成工具: │
│ JavaPoet: 流畅 API 生成 Java 源码 │
│ AutoService: 自动注册处理器 │
│ │
└──────────────────────────────────────────────────┘
注解处理器是 Java 编译期编程的核心工具。理解了 AbstractProcessor 的处理流程、Element 模型和 Filer 文件生成机制,你就能像 Lombok、MapStruct 那样在编译期生成模板代码,消除重复劳动。Lombok 的 AST 修改是特殊技巧,标准场景下用 Filer + JavaPoet 就能做到大多数事情。