Lombok注解底层原理

张开发
2026/4/8 3:18:44 15 分钟阅读

分享文章

Lombok注解底层原理
Lombok 注解原理——编译期注解处理的完整讲解一、从一个现象说起Lombok 的魔法你一定见过这样的代码DatapublicclassUser{privateStringname;privateintage;}就这么几行什么方法都没写。但你在别的地方却可以直接这样调用UserusernewUser();user.setName(张三);user.setAge(18);System.out.println(user.getName());System.out.println(user.toString());System.out.println(user.equals(anotherUser));getter、setter、toString、equals、hashCode甚至全参构造方法——你明明没写但它们确实存在而且能正常调用。这就是 Lombok 的魔法。你第一反应可能是是不是和 Spring 的注解一样在运行时通过反射和动态代理做的不是。完全不是。Lombok 的实现机制和运行时注解处理截然不同。上一次我们讲的注解原理是运行时通过反射获取注解JVM 用动态代理创建注解实例。那是一种运行时注解处理机制。而 Lombok 采用的是编译期注解处理。也就是说Lombok 的所有工作在你按下编译按钮的那一刻就全部完成了。等到程序运行时Lombok 的注解已经不存在了生成的.class文件中直接就有完整的getter、setter等方法的字节码。运行时没有任何额外开销没有反射没有动态代理什么都没有。这到底是怎么做到的接下来我们一步步拆解。二、要理解 Lombok必须先理解 Java 编译的过程Lombok 的秘密藏在 Java 编译过程中。所以我们需要先了解一个.java文件是怎么变成.class文件的2.1 编译不是一步到位的很多初学者以为编译就是源码进去字节码出来是一个黑盒操作。但实际上Java 编译器javac的编译过程分为多个阶段。简化来看核心阶段如下.java 源码文件 │ ▼ 【阶段一】词法分析 语法分析 │ 将源代码文本解析成一棵抽象语法树AST ▼ 【阶段二】注解处理Annotation Processing │ 调用注册的注解处理器可以读取甚至修改 AST ▼ 【阶段三】语义分析 │ 检查类型、变量、方法调用是否合法 ▼ 【阶段四】字节码生成 │ 将最终的 AST 转换成 .class 字节码文件 ▼ .class 字节码文件注意阶段二——注解处理。这个阶段是编译器专门留出来的一个插口允许外部程序介入编译过程。Lombok 正是通过这个插口来施展它的魔法的。但在深入阶段二之前我们需要先理解阶段一产生的东西——抽象语法树AST。三、什么是抽象语法树AST3.1 源码在编译器眼中的样子你写的 Java 源代码在你眼中是一行行的文字。但在编译器眼中它需要被转换成一种结构化的数据表示才能被程序化地处理。这种结构化的表示就是抽象语法树Abstract Syntax Tree简称 AST。什么意思呢我们来看一个具体的例子。假设你写了这样一个简单的类publicclassUser{privateStringname;publicStringgetName(){returnname;}}编译器在阶段一词法分析 语法分析处理完之后会在内存中构建出一棵树形结构大致如下ClassDeclaration类声明节点 ├── 修饰符: public ├── 类名: User ├── 字段列表 │ └── FieldDeclaration字段声明节点 │ ├── 修饰符: private │ ├── 类型: String │ └── 名称: name └── 方法列表 └── MethodDeclaration方法声明节点 ├── 修饰符: public ├── 返回类型: String ├── 方法名: getName ├── 参数列表: (空) └── 方法体 └── ReturnStatementreturn 语句节点 └── 引用: name这就是 AST。它把你写的源代码按照语法规则拆解成了一棵由各种节点组成的树。每个类是一个节点每个字段是一个节点每个方法是一个节点方法里的每条语句是一个节点甚至每个表达式也是一个节点这棵树完整地、结构化地描述了你源代码的全部信息。3.2 AST 的关键特性它是可以被修改的这是理解 Lombok 原理的关键。AST 不是一个只读的东西。它在内存中就是一堆 Java 对象组成的树形结构。既然是对象就可以被操作——你可以往树上添加新节点也可以删除或修改已有的节点。比如你可以在方法列表下面插入一个新的MethodDeclaration节点这就相当于给这个类新增了一个方法。而 Lombok 做的事情就是在编译过程中往 AST 上插入新的方法节点。编译器在后续阶段处理 AST 并生成字节码时它不知道也不关心这些节点是程序员原本写的还是被 Lombok 插入的——它只管把 AST 上的所有节点都翻译成字节码。这就是为什么最终的.class文件中会出现你没写过的getter、setter等方法。四、什么是 APT注解处理工具4.1 APT 的定位APT 的全称是Annotation Processing Tool即注解处理工具。它是 Java 编译器提供的一种机制允许开发者编写注解处理器Annotation Processor在编译阶段参与到编译过程中。还记得前面编译流程中的阶段二吗那个阶段就是 APT 发挥作用的地方。4.2 APT 的工作流程当javac编译源代码时到达注解处理阶段它会做以下事情发现注解处理器编译器会扫描项目中注册的所有注解处理器。注解处理器通过 Java 的 SPIService Provider Interface机制注册具体来说就是在META-INF/services/javax.annotation.processing.Processor文件中声明处理器的全限定类名。匹配注解每个注解处理器都会声明自己关心哪些注解。编译器根据源码中出现的注解将对应的处理器匹配上。调用注解处理器编译器把相关的 AST 信息传递给注解处理器注解处理器执行自己的逻辑。可能的多轮处理如果注解处理器生成了新的源文件编译器会重新解析这些新文件再次进入注解处理阶段。这个过程可能重复多轮直到没有新的文件生成为止。4.3 标准 APT 的能力边界这里有一个非常重要的细节标准的 APT 规范只允许注解处理器读取AST 信息和生成新的源文件不允许修改已有的 AST。也就是说按照 Java 官方的设计注解处理器可以做的事情是读取源码中的注解信息基于这些信息生成新的.java文件比如生成一个辅助类但不能修改已有类的 AST例如Google 的 AutoValue 框架就是标准 APT 的典型用法——它读取你写的注解然后生成一个新的实现类文件。4.4 Lombok 的越界操作Lombok 不走寻常路。它没有遵循标准 APT 的只读规则。Lombok 直接使用了javac编译器的内部 APIcom.sun.tools.javac包下的类获取到了 AST 的底层数据结构然后直接修改了已有类的 AST。这就是为什么 Lombok 经常被称为一种hack或黑魔法——它利用了编译器的非公开内部接口做了标准规范不允许的事情。这也是为什么 Lombok 有时会在 JDK 版本升级后出现兼容性问题——因为它依赖的是javac的内部实现细节而这些内部 API 不在 Java 的公开规范内Oracle 随时可能改动它们。五、Lombok 的完整工作流程现在我们把所有知识组装起来看看当你编译一个使用了Data注解的类时Lombok 到底做了什么。5.1 准备阶段Lombok 注册自己的注解处理器当你在项目中引入 Lombok 的依赖比如在 Maven 或 Gradle 中添加 LombokLombok 的 jar 包中包含了一个文件META-INF/services/javax.annotation.processing.Processor这个文件的内容大致是lombok.launch.AnnotationProcessorHider$AnnotationProcessor这就是告诉 Java 编译器“嘿我这里有一个注解处理器编译的时候请调用我。”5.2 第一步编译器解析源码生成 AST假设你的源代码是DatapublicclassUser{privateStringname;privateintage;}编译器首先进行词法分析和语法分析将这段源码转换成 ASTClassDeclaration ├── 注解列表 │ └── Data ├── 修饰符: public ├── 类名: User ├── 字段列表 │ ├── FieldDeclaration │ │ ├── 修饰符: private │ │ ├── 类型: String │ │ └── 名称: name │ └── FieldDeclaration │ ├── 修饰符: private │ ├── 类型: int │ └── 名称: age └── 方法列表 └── (空因为你没有写任何方法)此时AST 中只有两个字段没有任何方法。5.3 第二步编译器进入注解处理阶段调用 Lombok 的注解处理器编译器发现源码中有注解Data于是检查已注册的注解处理器找到了 Lombok 的处理器。编译器将 AST 传递给 Lombok 的处理器。5.4 第三步Lombok 的处理器解析 AST找到目标Lombok 的注解处理器拿到 AST 后开始遍历这棵树。它找到了User类的ClassDeclaration节点发现这个节点上标注了Data注解。Lombok 知道Data是一个组合注解相当于同时使用了以下注解Getter为所有字段生成 getter 方法Setter为所有非 final 字段生成 setter 方法ToString生成 toString 方法EqualsAndHashCode生成 equals 和 hashCode 方法RequiredArgsConstructor生成包含 final 字段和 NonNull 字段的构造方法5.5 第四步Lombok 直接修改 AST插入新的方法节点接下来就是 Lombok 的核心操作。Lombok 使用javac的内部 API直接在 AST 上执行修改操作。生成 getter 方法Lombok 遍历User类的字段列表发现有nameString 类型和ageint 类型两个字段。于是它在 AST 的方法列表中插入两个新的MethodDeclaration节点MethodDeclaration (新插入) ├── 修饰符: public ├── 返回类型: String ├── 方法名: getName ├── 参数列表: (空) └── 方法体 └── ReturnStatement └── 引用: this.name MethodDeclaration (新插入) ├── 修饰符: public ├── 返回类型: int ├── 方法名: getAge ├── 参数列表: (空) └── 方法体 └── ReturnStatement └── 引用: this.age生成 setter 方法同样插入两个 setter 方法的节点MethodDeclaration (新插入) ├── 修饰符: public ├── 返回类型: void ├── 方法名: setName ├── 参数列表: (String name) └── 方法体 └── AssignmentStatement └── this.name name MethodDeclaration (新插入) ├── 修饰符: public ├── 返回类型: void ├── 方法名: setAge ├── 参数列表: (int age) └── 方法体 └── AssignmentStatement └── this.age age生成 toString 方法MethodDeclaration (新插入) ├── 修饰符: public ├── 返回类型: String ├── 方法名: toString ├── 参数列表: (空) └── 方法体 └── ReturnStatement └── 字符串拼接: User(name this.name , age this.age )生成 equals 和 hashCode 方法类似地Lombok 按照规范的算法生成完整的equals()和hashCode()方法节点并插入 AST。处理完之后AST 变成了这样ClassDeclaration ├── 注解列表 │ └── Data ├── 修饰符: public ├── 类名: User ├── 字段列表 │ ├── FieldDeclaration (name) │ └── FieldDeclaration (age) └── 方法列表 ├── MethodDeclaration: getName() ← Lombok 插入 ├── MethodDeclaration: getAge() ← Lombok 插入 ├── MethodDeclaration: setName(String) ← Lombok 插入 ├── MethodDeclaration: setAge(int) ← Lombok 插入 ├── MethodDeclaration: toString() ← Lombok 插入 ├── MethodDeclaration: equals(Object) ← Lombok 插入 └── MethodDeclaration: hashCode() ← Lombok 插入原来空空如也的方法列表现在被 Lombok 填满了七个方法。5.6 第五步编译器继续后续流程生成字节码注解处理阶段结束后编译器拿着这棵被修改过的 AST继续往下走——语义分析、字节码生成。对编译器来说它完全不知道这些方法是 Lombok 插入的还是程序员自己写的。AST 上有什么它就编译什么。最终生成的User.class文件中getName()、setName()、toString()等方法的字节码全部都在。5.7 运行阶段Lombok 已经完全退场到了程序运行的时候JVM 加载的是编译完成的.class文件。这个文件中完整包含了所有 getter、setter、toString 等方法的字节码Data注解本身已经不存在了因为 Lombok 注解的Retention通常是SOURCE编译后就被丢弃了没有任何反射没有任何动态代理没有任何运行时开销Lombok 在运行时的存在感是零。你甚至可以在编译完成后把 Lombok 的 jar 包从运行时依赖中移除程序照样正常运行。六、验证反编译 Lombok 生成的 .class 文件你可以用反编译工具比如javap或 IDEA 的反编译功能查看 Lombok 处理后的.class文件。你会看到类似这样的代码publicclassUser{privateStringname;privateintage;publicStringgetName(){returnthis.name;}publicintgetAge(){returnthis.age;}publicvoidsetName(Stringname){this.namename;}publicvoidsetAge(intage){this.ageage;}publicStringtoString(){returnUser(namethis.name, agethis.age);}publicbooleanequals(Objecto){// 完整的 equals 实现...}publicinthashCode(){// 完整的 hashCode 实现...}}这些方法在源码中不存在但在字节码中完整存在。这就是 Lombok 在编译期偷偷插入的结果。七、Lombok 与 Spring 注解的本质区别现在我们可以把两种注解处理机制做一个清晰的对比了7.1 处理时机不同维度Spring 注解如 ControllerLombok 注解如 Data处理时机运行时编译时注解何时被读取程序启动后Spring 通过反射读取编译器编译源码时Lombok 的注解处理器读取处理完成后注解仍然存在于 .class 文件中注解已经从 .class 文件中消失7.2 实现技术不同维度Spring 注解Lombok 注解核心技术反射 动态代理APT AST 修改注解的 RetentionRUNTIME必须保留到运行时SOURCE只在源码阶段存在运行时开销有反射和代理有性能开销无编译期已完成所有工作7.3 工作方式不同维度Spring 注解Lombok 注解怎么产生效果运行时读取注解信息动态改变程序行为如注入依赖、创建代理编译时直接修改源码的 AST在字节码中生成实际的方法结果是什么注解信息被框架解释执行.class 文件中直接包含了新增的方法字节码7.4 用通俗的话说Spring 的注解就像是一个标签贴在类或方法上。程序运行时Spring 框架看到这个标签就按照标签的含义去做相应的事情注册 Bean、注入依赖等。标签本身不会改变类的代码。Lombok 的注解更像是一个指令告诉编译器请在这里帮我补充以下代码。编译器执行完指令后代码就真的被补充进去了指令本身也就完成使命、被丢弃了。最终的 .class 文件看起来就像是你自己手写了所有方法一样。八、Lombok 为什么会有争议理解了 Lombok 的原理之后你就能理解为什么 Lombok 在 Java 社区中一直存在争议了。8.1 争议一使用了非公开的内部 API正如前面所说标准的 APT 规范不允许修改已有类的 AST。Lombok 是通过调用javac的内部 APIcom.sun.tools.javac.tree等包下的类来实现 AST 修改的。这些内部 API不属于 Java 的公开标准没有稳定性保证可能在 JDK 版本升级时发生变化这就是为什么每次 JDK 发布新版本Lombok 团队都需要跟进适配有时候会出现升级了 JDKLombok 就不能用了的情况。8.2 争议二IDE 需要额外插件支持因为 Lombok 生成的方法不存在于源码中IDE如 IntelliJ IDEA、Eclipse在做代码分析时默认找不到这些方法会报红线错误。为了解决这个问题你需要安装 Lombok 的 IDE 插件。这个插件会模拟 Lombok 的行为让 IDE 知道虽然源码中没有这些方法但编译后会有。8.3 争议三调试和代码可读性因为 getter、setter 等方法不存在于源码中当你在调试时想设断点到某个 setter 方法里你可能找不到对应的源码行。同样新加入项目的开发者如果不了解 Lombok看到一个只有字段定义的类就能调用各种方法可能会困惑。8.4 尽管如此Lombok 仍然广泛使用上述争议并没有阻止 Lombok 的普及。因为它带来的便利性确实很大——减少了大量重复的样板代码让类的定义更加简洁。在实际的企业项目中Lombok 的使用率非常高。九、补充其他基于标准 APT 的注解处理框架Lombok 是一种非标准的 APT 使用方式因为它修改了已有的 AST。在 Java 生态中还有很多框架是按照标准 APT 规范工作的——它们不修改已有代码而是生成新的源文件。比如MapStruct一个对象映射框架。你定义一个 Mapper 接口用注解标注映射规则编译时 MapStruct 的注解处理器自动生成这个接口的实现类。Dagger 2一个依赖注入框架。编译时根据注解生成依赖注入的代码避免了运行时反射的开销。Google AutoValue编译时根据注解生成不可变值对象的实现类。这些框架和 Lombok 的共同点是都在编译期处理注解都没有运行时开销。不同点是它们生成新文件而不修改已有文件走的是标准 APT 的合规路线。十、最终总结让我们把整个讲解浓缩为一个完整的结论Lombok 使用的是编译期注解处理原理和运行时反射完全不同。它的核心机制是APT注解处理工具 修改 AST抽象语法树编译阶段介入Java 编译器在编译.java文件时会调用注解处理器。Lombok 通过 SPI 机制注册了自己的注解处理器。解析 AST编译器先将源码解析成抽象语法树AST。Lombok 的注解处理器拿到 AST 后遍历树节点找到标注了Data、Getter等注解的类。修改 ASTLombok 利用javac的内部 API直接在语法树上插入 getter、setter、toString、equals 等方法节点。这一步是标准 APT 不允许的越界操作。生成字节码修改后的 AST 继续走正常的编译流程。编译器不区分哪些节点是原有的、哪些是 Lombok 插入的统一生成字节码。最终的.class文件中就完整包含了这些方法。所以 Lombok 的注解在.class文件中已经不存在了它只在编译期起作用运行时没有任何开销。而 Spring 的注解如Controller、Autowired是运行时注解处理注解保留到运行时Spring 启动后通过反射读取注解信息再执行相应的逻辑如创建 Bean、注入依赖。Lombok 是编译期注解处理的典型应用和 Spring 那种运行时反射处理注解是两种完全不同的机制。

更多文章