Lombok 的 @Data 到底怎么工作的?Java 注解处理器实战全链路拆解

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)CLASSRUNTIME 也可以被处理器读取,但如果只为编译期使用,SOURCE 最合适------编译后注解被丢弃,减少 class 文件体积。

Q5: 编译期怎么获取泛型类型信息?

TypeMirrorTypes 工具。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 就能做到大多数事情。

相关推荐
小羊没烦恼!2 小时前
Memory 记忆设计讨论:为什么 Agent Memory 不能只靠向量数据库?
java·开发语言·windows·算法·c#
天远API2 小时前
零信任架构实战:基于天远单人婚姻查询构建自动化联合贷款审计网关
java·人工智能·架构·自动化
九皇叔叔2 小时前
从本地事务到分布式事务
java·分布式·分布式事务·cap·base
黑马程序员毕设2 小时前
基于微信小程序的节目活动报名与管理系统设计与实现
java·开发语言·spring boot·后端·电脑
小坏讲微服务2 小时前
Spring AI 高频面试题20道
java·spring·ai·agent·springai
Bs_MoneyMagnet2 小时前
基于springboot+vue的会议室预约管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·毕业设计·计算机毕业设计
sunshine22 girl2 小时前
Java学习 java原理
java·开发语言·学习
Bs_MoneyMagnet2 小时前
基于springboot+vue的茶铺管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·毕业设计·计算机毕业设计
她说..2 小时前
RabbitMQ 使用场景详解
java·spring boot·后端·spring·rabbitmq·java-rabbitmq