简介这份资源围绕 Lombok 的实现原理展开面向希望深入理解注解处理器与编译期字节码增强机制的 Java 开发者尤其适合已能熟练使用 Data、Getter 等注解、想进一步探究其底层运作方式的中高级工程师。压缩包共 40 个文件约 88KB包含 9 个 java 源码、10 个 class 字节码、5 个 lst 与 1 个 processor 处理器文件另有 xml、iml、properties 等工程配置以及 dmn、bpmn 流程定义和 jar 依赖整体呈现一个多模块的注解处理实验工程。资源从注解处理器、预处理器元数据、ASM 字节码操作到 IDE 集成与自定义处理器逐层拆解并讨论了 final 字段、第三方库兼容性等使用限制。已有 665 人学习读者可借此对照源码理解 Lombok 如何在编译阶段插入方法、如何借助 ASM 改写字节码并掌握自定义注解处理器的实现思路为排查编译期问题与扩展注解能力提供参考。1. 从一行 Data 说起这个项目到底在折腾什么你有没有想过一个问题为什么在实体类上写一个Data编译完的 class 文件里就凭空多出了 getter、setter、equals、hashCode、toString源码里明明没有这些方法IDE 里却能点出来反射调用也能拿到。很多人用了好几年 Lombok把它当成“IDE 插件”或者“语法糖”但真到面试被问“Lombok 是怎么改字节码的”或者线上遇到 Lombok 和编译工具链打架时就说不清了。这份资源就是冲着这个黑匣子去的——它是一套用代码讲清 Lombok 实现原理的工程核心围绕注解处理器Annotation Processor和字节码操作两条线展开。适合两类人一类是天天用 Lombok 但想搞明白它底层怎么跑的开发者另一类是想自己写注解处理器、做编译期代码生成的工程师。读完你至少能自己动手写一个简化版的Getter知道 AST 在哪一步被改、class 文件在哪一步被写出来。2. 原理拆解注解处理器怎么在编译期“无中生有”2.1 先分清 Lombok 的两个身份很多人把 Lombok 理解成一个 IDE 插件这个认知只对了一半。Lombok 实际上同时扮演两个角色一个是编译期的注解处理器javac 的Processor另一个是 IDE 侧的插件让编辑器不报错、能补全。真正干活的是前者IDE 插件只是为了让你的编辑器别飘红。编译期这条链路是这样的javac 在解析完源码、生成抽象语法树AST之后会进入注解处理轮次annotation processing round。Lombok 注册的处理器在这个阶段被调用它拿到的是已经解析好的 AST 节点然后直接往这些节点上“挂”方法——注意不是生成新的源文件而是直接修改当前正在编译的 AST。这就是为什么 Lombok 不需要你手动再编译一次生成的文件它是在同一轮编译里把方法塞进去的。这里有个关键点标准的注解处理器继承AbstractProcessor只能生成新文件不能改已有的类。Lombok 能改是因为它用了内部 APIcom.sun.tools.javac包下的东西直接操作 AST。这也是为什么 Lombok 对 JDK 版本这么敏感——JDK 9 模块化之后这些内部包默认不开放得加--add-opens或者--add-exports才能跑。2.2 一个最小注解处理器的骨架要理解 Lombok先得能写出一个最普通的注解处理器。下面这段代码是一个能跑的最小骨架它不干别的只是在编译期打印出被注解的元素名字// 最小注解处理器继承 AbstractProcessor注册支持的注解 SupportedAnnotationTypes(com.example.demo.MyAnnotation) SupportedSourceVersion(SourceVersion.RELEASE_8) public class SimpleProcessor extends AbstractProcessor { Override public boolean process(Set? extends TypeElement annotations, RoundEnvironment roundEnv) { // roundEnv 里能拿到所有被 MyAnnotation 标记的元素 for (Element element : roundEnv.getElementsAnnotatedWith( processingEnv.getElementUtils().getTypeElement(com.example.demo.MyAnnotation))) { // 这里只是打印真正的 Lombok 会在这里改 AST System.out.println(被注解的元素: element.getSimpleName()); } return true; // 返回 true 表示这个注解已被处理不再往下传 } }逻辑说明process方法是注解处理器的入口javac 每一轮注解处理都会调它。roundEnv.getElementsAnnotatedWith返回所有被目标注解标记的元素元素可能是类、方法、字段。参数annotations是这一轮要处理的注解集合roundEnv是当前轮次的环境能拿到根元素和工具类。关键参数SupportedAnnotationTypes声明这个处理器关心哪些注解写全限定名SupportedSourceVersion声明支持的源码版本写低了会有警告。返回值true表示“这个注解我处理了别的处理器别再处理”返回false则继续传递。这个骨架跑起来需要在META-INF/services/javax.annotation.processing.Processor文件里注册处理器全类名否则 javac 根本不知道它的存在。Lombok 也是这么注册的只不过它注册的处理器内部逻辑复杂得多。2.3 Lombok 改 AST 的核心动作Lombok 的处理器拿到 AST 节点后核心动作是往类节点上添加方法节点。简化后的逻辑大致是这样// 伪代码Lombok 内部往类节点挂 getter 的简化逻辑 public boolean process(Set? extends TypeElement annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(Getter.class)) { if (element.getKind() ElementKind.FIELD) { VariableElement field (VariableElement) element; TypeElement clazz (TypeElement) field.getEnclosingElement(); // 拿到 javac 内部的类声明节点 JavacNode classNode JavacAST.of(clazz); // 构造 getter 方法名get 首字母大写字段名 String methodName get capitalize(field.getSimpleName().toString()); // 用 MethodDeclaration 构造方法节点挂到类节点上 classNode.addMethod(methodName, Modifier.PUBLIC, field.asType(), return this. field.getSimpleName() ;); } } return true; }逻辑说明这段是简化后的伪代码真实 Lombok 用的是JavacTreeMaker和JavacNode这套封装。核心思路是遍历被Getter标记的字段找到它所属的类然后构造一个方法声明节点方法体是return this.字段名最后把这个方法节点加到类节点的方法列表里。javac 后续生成字节码时会把这个新加的方法一起编译进去。参数说明field.asType()拿到字段的类型作为 getter 的返回类型Modifier.PUBLIC是方法修饰符方法体字符串会被解析成 AST 表达式。真实实现里还要处理布尔类型is前缀、静态字段、访问权限等边界情况。提示Lombok 改 AST 用的是 javac 内部 API不同 JDK 版本内部结构会变所以 Lombok 每个大版本都要适配新 JDK。这也是为什么升级 JDK 时经常要同步升 Lombok。3. 跑起来从零编译一个简化版 Lombok3.1 环境准备和项目结构要复现这套原理不需要真的去读 Lombok 全部源码可以自己搭一个简化工程。环境要求JDK 8 或 11JDK 8 对内部 API 限制少适合入门Maven 3.6。项目结构建议这样组织lombok-demo/ ├── pom.xml ├── src/main/java/ │ ├── com/example/annotation/ │ │ └── MyGetter.java # 自定义注解 │ ├── com/example/processor/ │ │ └── MyGetterProcessor.java # 注解处理器 │ └── com/example/app/ │ └── User.java # 使用注解的实体类 └── src/main/resources/ └── META-INF/services/ └── javax.annotation.processing.Processorpom.xml里关键是把处理器和业务代码分开编译处理器本身不能依赖 Lombok否则会循环。常见做法是用两个 module或者用maven-compiler-plugin的annotationProcessorPaths配置。3.2 自定义注解和处理器实现先定义注解// MyGetter.java自定义注解对标 Lombok 的 Getter Target(ElementType.FIELD) // 只能标在字段上 Retention(RetentionPolicy.SOURCE) // 只在源码期保留编译后消失 public interface MyGetter { }Retention(SOURCE)是关键Lombok 的注解也是 SOURCE 级别编译完就没了所以运行时反射拿不到注解但方法已经生成好了。处理器实现// MyGetterProcessor.java处理 MyGetter生成 getter 方法 SupportedAnnotationTypes(com.example.annotation.MyGetter) SupportedSourceVersion(SourceVersion.RELEASE_8) public class MyGetterProcessor extends AbstractProcessor { Override public boolean process(Set? extends TypeElement annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(MyGetter.class)) { if (element.getKind() ! ElementKind.FIELD) continue; VariableElement field (VariableElement) element; TypeElement clazz (TypeElement) field.getEnclosingElement(); String fieldName field.getSimpleName().toString(); String methodName get Character.toUpperCase(fieldName.charAt(0)) fieldName.substring(1); // 用 JavaPoet 或直接拼字符串生成方法源码 String methodCode public field.asType() methodName () { return this. fieldName ; }; try { // 把生成的方法写进当前类 JavaFileObject file processingEnv.getFiler() .createSourceFile(clazz.getQualifiedName() Getter); try (Writer writer file.openWriter()) { writer.write(package processingEnv.getElementUtils() .getPackageOf(clazz) ;\n); writer.write(public class clazz.getSimpleName() Getter {\n methodCode \n}); } } catch (IOException e) { processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, 生成失败: e.getMessage()); } } return true; } }逻辑说明这段代码演示的是标准注解处理器的做法——生成一个新文件。注意它和 Lombok 的区别Lombok 是直接改原类的 AST而标准处理器只能生成新文件。这里生成一个UserGetter类里面放着 getter 方法。虽然效果不如 Lombok 优雅但能帮你理解注解处理器的基本流程。参数说明processingEnv.getFiler()是生成文件的工具createSourceFile创建新的源文件processingEnv.getMessager()用来在编译期报错或警告比System.out更规范IDE 能直接定位到出错的行。注册文件META-INF/services/javax.annotation.processing.Processor内容就一行com.example.processor.MyGetterProcessor3.3 编译验证和结果查看在User.java里用上注解// User.java使用自定义注解 public class User { MyGetter private String name; MyGetter private int age; }执行mvn clean compile编译完成后去target/classes/com/example/app/目录下看会多出一个UserGetter.class。用javap -p UserGetter.class反编译能看到生成的方法javap -p target/classes/com/example/app/UserGetter.class # 输出里能看到 public java.lang.String getName(); 和 public int getAge();这一步跑通你就理解了注解处理器“生成代码”的基本套路。接下来要理解 Lombok 的“改 AST”套路需要引入tools.jarJDK 8或者用--add-exports开放内部包JDK 9然后调用com.sun.tools.javac.tree.TreeMaker去构造方法节点。这部分代码量较大资源里给了完整的可运行版本建议直接跑一遍再对照改。注意JDK 9 以上跑内部 API 需要加 JVM 参数比如--add-exports jdk.compiler/com.sun.tools.javac.treeALL-UNNAMED否则会报IllegalAccessError。这是新手最容易翻车的地方。4. 避坑排查Lombok 和注解处理器常见的五个坑4.1 编译报错“找不到符号”但 IDE 不飘红现象命令行mvn compile报cannot find symbol: method getName()但 IDE 里代码是正常的点进去也能看到方法。原因IDE 装了 Lombok 插件它自己模拟了一套方法补全但命令行编译时注解处理器没生效。常见于 Maven 配置里annotationProcessorPaths没写或者 Lombok 版本和 JDK 版本不匹配。解决检查pom.xml里maven-compiler-plugin是否配置了annotationProcessorPaths指向 Lombok确认 Lombok 版本支持当前 JDK比如 JDK 17 需要 Lombok 1.18.22 以上。命令行执行mvn dependency:tree看 Lombok 有没有被正确引入。4.2 JDK 升级后 Lombok 直接罢工现象项目从 JDK 8 升到 JDK 17编译报java.lang.NoSuchFieldError或者IllegalAccessError指向com.sun.tools.javac包。原因Lombok 依赖 javac 内部 APIJDK 9 模块化后这些包默认不导出JDK 版本越高内部结构变化越大老版本 Lombok 找不到对应的字段或方法。解决升级 Lombok 到支持目标 JDK 的版本这是最省事的做法。如果因为依赖锁定不能升就得在maven-compiler-plugin里加--add-exports和--add-opens参数把jdk.compiler相关包开放出来。血泪经验是升级 JDK 前先查 Lombok 的兼容性矩阵别等编译挂了才回头找。4.3 注解处理器生成了文件但没被编译现象自定义处理器跑完target/generated-sources下能看到生成的文件但最终 class 里没有对应的方法。原因生成的文件没有被加入编译源路径。Maven 默认会把target/generated-sources/annotations加进去但如果你手动改了sourceDirectory或者用了非标准目录就可能漏掉。解决检查pom.xml里有没有build-helper-maven-plugin把生成目录加进 source root或者确认处理器用的是createSourceFile而不是createResource。生成源文件用createSourceFile生成资源用createResource用错了就不会被编译。4.4 处理器里抛异常导致整个编译失败现象编译时突然报一堆java.lang.NullPointerException堆栈指向处理器内部业务代码一行没改。原因处理器在process方法里没做空判断比如element.getEnclosingElement()返回 null 时直接强转或者processingEnv.getFiler()重复创建同名文件抛FilerException。解决处理器里所有从 AST 拿到的元素都要判空生成文件前先检查是否已存在或者用createSourceFile时捕获FilerException并忽略。更规范的做法是用Messager报错而不是直接抛异常这样 IDE 能定位到具体代码行。4.5 增量编译时处理器不触发现象改了实体类重新编译发现生成的代码没更新还是旧的。原因Maven 增量编译判断源文件没变就不重新跑注解处理器但处理器依赖的注解或配置变了它检测不到。解决执行mvn clean compile强制全量编译。如果项目大、全量慢可以在maven-compiler-plugin里配置useIncrementalCompilationfalse或者用mvn compile -Dmaven.compiler.useIncrementalCompilationfalse。我一般会在 CI 里强制 clean本地开发才用增量。5. 二次开发把原理变成自己的代码生成器理解了 Lombok 的 AST 操作之后最有价值的二次开发方向是写自己的编译期代码生成器。比如你团队里有一套重复的 DTO 转换逻辑每次加字段都要手动改 Converter这种活完全可以交给注解处理器。改造思路分三步。第一步定义注解比如AutoConverter标在 DTO 类上声明目标类型。第二步写处理器在process里拿到被注解的类读取它的字段列表生成一个XxxConverter类里面是字段到字段的赋值代码。第三步注册处理器并配置到构建流程里。这里给一个生成 Converter 的核心片段// 读取源类字段生成转换方法 TypeElement source (TypeElement) element; List? extends Element fields source.getEnclosedElements().stream() .filter(e - e.getKind() ElementKind.FIELD) .collect(Collectors.toList()); StringBuilder body new StringBuilder(); body.append(public static ).append(targetType).append( convert() .append(source.getSimpleName()).append( src) {\n); body.append( ).append(targetType).append( dst new ).append(targetType).append(();\n); for (Element f : fields) { String name f.getSimpleName().toString(); // 生成 dst.setXxx(src.getXxx()) 这样的赋值 body.append( dst.set).append(capitalize(name)) .append((src.get).append(capitalize(name)).append(());\n); } body.append( return dst;\n});逻辑说明这段代码遍历源类的所有字段为每个字段生成一行 setter 调用。capitalize是首字母大写的工具方法。生成的类是一个静态工具类调用方直接XxxConverter.convert(src)就能完成转换。参数说明targetType是目标类的全限定名可以从注解的属性里读字段过滤要排除静态字段和serialVersionUID这类特殊字段否则生成的代码会编译不过。验证方法写一个单元测试构造源对象调用生成的 Converter断言目标对象的字段值一致。更彻底的做法是用反射遍历所有字段确保没有遗漏。我一般会加一个测试用例字段数量对不上就 fail这样加字段忘了改注解能立刻发现。改造时有个技巧别一上来就追求支持所有类型。先支持基本类型和 String跑通了再逐步加 List、Map、嵌套对象。嵌套对象的转换可以递归生成但要注意循环引用加个已处理集合去重。另外生成的代码要加Generated注解javax.annotation.Generated标明是自动生成的方便后续排查问题时区分。从那以后我每次写注解处理器都会先写一个最小可运行版本确认process被调用了、文件生成了、编译通过了再往上加逻辑。这个习惯帮我省了无数个对着空输出发呆的下午。希望帮到你。本文还有配套的精品资源点击获取