尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Lombok原理揭秘:javac注解处理器如何修改AST生成代码
简介Java 开发中注解处理器是一类特殊的编译期工具它基于 JSR 269 规范在 javac 解析源码生成抽象语法树AST后介入编译流程通过 SPI 机制被自动发现并执行。与反射在运行时影响程序行为不同注解处理器能在字节码生成前直接修改 AST从而在编译期完成代码注入兼顾运行效率与代码简洁性。这一机制广泛应用于框架开发、编译期校验和代码生成等场景。Lombok 正是典型代表它利用 javac 注解处理链路和 TreeMaker API在编译阶段为标注了 Getter、Setter 等注解的类插入方法省去手写样板代码。深入理解该原理不仅有助于排查“程序包 lombok 不存在”“反射拿不到注解”等工程问题也能为自定义编译期处理器提供坚实的技术基础。1. Lombok 的原理真相它不是运行时魔法而是编译期拦截器如果你到现在还以为 lombok 软件/插件是靠反射在运行时给类补方法那可以先把结论反过来记Lombok 是 javac 走编译流程时的一个注解处理器它利用 JDK 提供的注解处理机制在源码还没变成字节码之前直接修改抽象语法树。也就是说Getter 在源码里是给编译器看的记号真正干活的是后面那个处理器。这篇笔记会把 Lombok 从注解定义、处理器装配到树改造的逻辑拆开并给出一个可以跑通的自定义处理器。适合想搞清楚“Lombok 生成的代码到底在哪生成”的工程师也适合打算自己写注解处理器的开发者。2. Java 编译器的注解处理链路javac 在哪一步对 AST 动手2.1 注解处理器的装配SPI 与服务发现Java 6 之后引入 JSR 269 注解处理规范核心接口是javax.annotation.processing.Processor。javac 在编译时会通过 ServiceLoader 机制读取 classpath 或 processorpath 下的META-INF/services/javax.annotation.processing.Processor文件把这个文件里写的全限定类名加载进来。Lombok 的 jar 里自带这条 SPI 配置所以你只要把 lombok 放进依赖里javac 就会自动发现它Java 源码里不需要任何 import 语句。process()方法的返回值是一个需要理解的细节。返回true表示“这批注解我已经消费掉了后续处理器不能再碰”返回false表示“我只是看了一眼你们继续处理”。实际写处理器时这地方特别容易误用我一般会返回false让没被消费的注解继续往后传递。2.2 编译阶段与处理器触发时机javac 编译不是一步到位的。解析阶段先把 .java 文件变成 JCTree 语法树然后是输入阶段判断有没有新源文件要读进来注解处理阶段启动轮次 round每个 round 会给每个处理器一次process()调用当某一轮没有产生新源文件时处理阶段结束最后才做语义分析和字节码生成。Lombok 的插入点就是注解处理阶段因为在这个阶段树已经建好并且还可以改。编译阶段树的状态注解处理器能做什么解析语法树刚生成类型未绑定可以读取元素不适合改动注解处理可能多轮树可读可写可追加新文件在这里插入方法、字段、类分析与生成树基本冻结无法再改变结构javac 可能跑多轮。RoundEnvironment.processingOver()返回true的那一轮表示整个处理阶段即将结束这一轮不要再做实质操作。这个时机判断在自定义注解处理器里是一个典型的翻车点。2.3 javac 的关键参数processor、processorpath、proc命令行里要让自定义处理器生效常见做法是这样的javac \ -processor com.example.processor.GetterProcessor \ -processorpath ./processor-jars \ -d ./out \ ./src/**/*.java各参数的含义分别是-processor指定处理器全类名多个处理器用逗号分隔-processorpath指定处理器本身所在的 jar 或 class 目录路径比把处理器扔进 classpath 更可控-d是字节码输出目录。调试阶段还会用到-proc:none跳过注解处理以及-proc:only只跑注解处理器不生成最终字节码。IDE 里的 “Enable annotation processing” 选项本质就是把这一组参数写进编译器的配置里。2.4 多轮次机制rootElements 与 getElementsAnnotatedWith 的取舍多轮次带来的一个隐藏问题是每个 round 里能看到的东西不一样。roundEnv.getRootElements()返回当前轮次所有顶层元素roundEnv.getElementsAnnotatedWith(某个注解)返回的是“当前轮次里可见的”被该注解标记的元素。如果某个注解出现在上一轮处理器生成的新文件里它可能只在下一轮才可见。所以很多自定义处理器宁可先遍历 rootElements再递归查成员而不是直接依赖getElementsAnnotatedWith。这个取舍在 Lombok 源码里也很明显Lombok 的 handler 拿到的是一棵完整的编译单元树而不是零散的注解元素。理解了这个差异再去读 Lombok 源码就能少走不少弯路。3. 拆解 Lombok 核心源码注解、Handler 与 JavacAST 的协作方式3.1 源码分层annotation、javac、eclipse、bytecodeLombok 源码里核心模块按平台分了几块。目录/包职责lombok.annotation注解定义比如 Getter、Setter、Builderlombok.core公共基础设施含配置加载、SPI 装配lombok.javacjavac 平台实现包含 JavacAST、JavacHandlerlombok.eclipseEclipse 平台的对应实现lombok.bytecode对 .class 的后期修补例如 SneakyThrows 的异常表注解本身其实只是一个带Target和Retention的标记接口。比如Getter标注为Retention(SOURCE)意思是它只在编译期有意义不会进入 class 字节码。很多人运行期用反射去拿 Lombok 的注解拿不到原因就在这里不是 Lombok 失效了而是它本来就设计成不留存。3.2 JavacAST 与 JavacNode树的封装Lombok 在 javac 平台没有直接操作裸的 JCTree而是包了一层 JavacAST。JavacAST 持有一棵JCTree.JCCompilationUnit并把树里的每个元素映射成 JavacNode。JavacNode 包装了 JCTree 节点的类型、位置、元素对象以及父子关系。为什么要自己包一层因为 Lombok 同时要适配 Eclipse 编译器和 javac 两套 AST 体系抽象出 JavacAST 后上层 handler 写一套逻辑就能在两个平台复用。handler 的调度流程是这样的Lombok 通过注解名定位到对应 handler比如看到 Builder 就找到 HandleBuilder。handler 拿到注解对应的 JavacNode 后通过 node 取得 JCTree再用 TreeMaker 构造新节点插入现有语法树。JavacHandlerUtil 里提供了一堆现成方法比如判断某个方法是否已存在、查找字段、检查注解是否重复标注等。这些工具方法避免了每个 handler 都自己写树的遍历逻辑。3.3 TreeMaker 改树的常规套路TreeMaker 是 javac 内部用来生成语法树节点的入口。Lombok 在 handler 里最常见的操作序列是找到类 JCTree创建方法定义节点创建方法体里的表达式节点然后把新方法追加进类的成员列表。这里有一个容易忽略的点方法定义的构造参数非常多包括访问标志、方法名、参数列表、返回值、throws 子句、方法体等。任何一个字段传错后续分析阶段就会报“找不到符号”。Lombok 对 handler 的封装做了很多容错。例如加方法之前会先检查该类是否已有同名方法避免重复生成。这个看似不起眼的防御逻辑在大型工程里能挡掉一大批冲突问题。3.4 一个生成 getter 的树改造路径拿最简单的 getter 生成来说处理逻辑大致是解析字段名首字母大写得到方法名确定返回类型构造方法体和 return 语句插入类的成员列表。其中 boolean 字段要生成 isXxx 而不是 getXxx这个分支在 Lombok 源码里就是一个很不起眼的 if 判断但它决定了生成方法是 get 前缀还是 is 前缀。很多人自定义处理器时忘了这个分支生成的代码风格就乱了。还有一点值得注意Lombok 对字段的访问权限、static 修饰符、泛型类型都有一整套处理规则。如果字段是ListString返回类型必须保留泛型信息不能只取裸类型List。这要求 handler 操作树节点时复制完整类型表达式而不是简单的字符串拼接。3.5 为什么不能直接用 Filer 生成一个新类有读者会问注解处理器里不是可以用 Filer 生成新源文件吗Lombok 为什么不这么做答案是Filer 生成的是独立类不是给原类塞方法。Lombok 的用法是在现有类字段上加注解调用方写的代码是new User().getName()User 类必须本身就带着 getName 方法。只有直接修改原类的 AST才能让生成的 getter 出现在调用方预期的类里。这也是 Lombok 实现原理与普通注解处理器最大的分界点。4. 仿写精简版 Getter 处理器从零跑通全流程4.1 工程结构与准备工作仿写一个简化版注解处理器先列一下文件结构。文件作用com/example/Getter.java注解定义com/example/processor/GetterProcessor.java注解处理器主体META-INF/services/javax.annotation.processing.ProcessorSPI 配置让 javac 能发现处理器demo/User.java验证代码JDK 版本建议在 8 到 17 之间。javac 默认会扫描 processorpath 下的所有 SPI 配置所以只要处理器所在目录被-processorpath指到即可不需要额外注册。4.2 注解定义package com.example; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.FIELD) Retention(RetentionPolicy.SOURCE) public interface Getter { }这里把Target限定为 FIELD表示该注解只能标注在字段上。Retention(SOURCE)表示它不会进入字节码。如果有人把它标到类上处理器里要加一个类型判断做保护否则后面的类型强转会直接抛异常。4.3 处理器核心逻辑标准 API 版本下面是处理器主体。这个版本只用标准 API不依赖 javac 内部类所以可移植性最好。package com.example.processor; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.tools.JavaFileObject; import java.io.IOException; import java.io.Writer; import java.util.Set; SupportedAnnotationTypes(com.example.Getter) SupportedSourceVersion(SourceVersion.RELEASE_8) public class GetterProcessor extends AbstractProcessor { Override public boolean process(Set? extends TypeElement annotations, RoundEnvironment roundEnv) { // 最后一轮不再做任何事避免重复生成 if (roundEnv.processingOver()) { return false; } TypeElement getterAnno processingEnv.getElementUtils().getTypeElement(com.example.Getter); if (getterAnno null) { return false; } for (Element element : roundEnv.getElementsAnnotatedWith(getterAnno)) { if (element.getKind() ! ElementKind.FIELD) { continue; } String fieldName element.getSimpleName().toString(); String typeName element.asType().toString(); // 约定俗成原生 boolean 生成 isXxx其他类型生成 getXxx String methodName typeName.equals(boolean) ? is capitalize(fieldName) : get capitalize(fieldName); generateHelperClass(element, fieldName, typeName, methodName); } return false; } private void generateHelperClass(Element field, String fieldName, String typeName, String methodName) { Element enclosing field.getEnclosingElement(); if (enclosing.getKind() ! ElementKind.CLASS) { return; } String pkg processingEnv.getElementUtils() .getPackageOf(enclosing).getQualifiedName().toString(); String className enclosing.getSimpleName().toString() _Getter; String qualifiedName pkg.isEmpty() ? className : pkg . className; String src String.format( package %s; class %s { public %s %s() { return this.%s; } }, pkg, className, typeName, methodName, fieldName); try { JavaFileObject jfo processingEnv.getFiler().createSourceFile(qualifiedName); try (Writer w jfo.openWriter()) { w.write(src); } } catch (IOException e) { processingEnv.getMessager().printMessage( Kind.ERROR, 生成 getter 失败: e.getMessage(), field); } } private String capitalize(String s) { if (s null || s.isEmpty()) { return s; } return Character.toUpperCase(s.charAt(0)) s.substring(1); } }这段代码里有几个关键点。第一process()里的状态检查processingOver()为 true 时直接返回避免最后一轮把生成逻辑再跑一遍。第二通过getElementsAnnotatedWith精确拿到标了 Getter 的字段省去手动递归遍历成员的过程。第三用 Filer 创建新源文件这是标准 API 的写法。注意这里生成的是独立辅助类演示的是注解处理器链路能跑通跟 Lombok 直接改原类 AST 不同。想给原类补方法看下一小节的 TreeMaker 版本。4.4 贴近 Lombok 的 TreeMaker 改造下面这段代码展示的是给原类直接插入 getter 方法的关键片段逻辑上更接近 Lombok 真实做法。// 需要通过强转拿到 JavacProcessingEnvironment 和 JavacTrees JavacProcessingEnvironment env (JavacProcessingEnvironment) processingEnv; JavacTrees trees JavacTrees.instance(env); TreePath path trees.getPath(field); JCTree.JCClassDecl targetClass (JCTree.JCClassDecl) path.getLeaf(); TreeMaker make TreeMaker.instance(env.getContext()); Names names Names.instance(env.getContext()); JCTree.JCMethodDecl getterMethod make.MethodDef( make.Modifiers(Flags.PUBLIC), names.fromString(methodName), returnTypeExpr, List.nil(), List.nil(), List.nil(), make.Block(0, List.of( make.Return( make.Select( make.Ident(names.fromString(this)), names.fromString(fieldName))) )), null ); // 把新方法追加到类成员列表的末尾 targetClass.defs targetClass.defs.append(getterMethod);这段代码的逻辑是把字段元素转成树路径拿到它所属的类节点用 TreeMaker 构造一个 public 方法定义方法体直接返回this.字段名最后把方法挂到类节点上。MethodDef的第二个参数是方法名第三个是返回类型表达式后面的List.nil()分别是泛型参数、形参、异常声明。任何一项没对齐后续编译阶段就会出符号错误。这里用的com.sun.tools.javac.*是 JDK 内部 API。JDK 9 之后模块系统把它们封在 jdk.compiler 模块里编译和运行的时候需要加--add-exports参数。这也是社区里很多注解处理器项目在跨 JDK 版本升级时最头疼的问题Lombok 自身也在持续适配这块变化。4.5 编译与验证命令javac \ --add-exports jdk.compiler/com.sun.tools.javac.treeALL-UNNAMED \ --add-exports jdk.compiler/com.sun.tools.javac.codeALL-UNNAMED \ --add-exports jdk.compiler/com.sun.tools.javac.utilALL-UNNAMED \ -processor com.example.processor.GetterProcessor \ -processorpath ./classes \ -d ./out \ src/com/example/demo/User.java--add-exports的作用是允许当前编译任务访问 JDK 内部 API。Lombok 官方在发布 jar 时把这部分细节封装好了所以用户不需要自己面对这些参数。但你自己仿写时这组参数就是绕不开的前置条件。如果编译阶段看到 IllegalAccessError先检查是不是漏了 exports。5. 避坑记录Lombok 在工程里的典型翻车现场5.1 程序包 lombok 不存在现象命令行执行 maven 编译时报“程序包 lombok 不存在”但 IDE 里编译正常。原因IDE 通常默认开启了注解处理命令行构建却可能没有把 lombok 挂到注解处理器链路上。如果 lombok 依赖只配了 provided scope且没有在 maven-compiler-plugin 里声明 processor path命令行构建就找不到它。解决在 maven-compiler-plugin 里用annotationProcessorPaths显式声明plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId configuration annotationProcessorPaths path groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version用你当前依赖的版本号/version /path /annotationProcessorPaths /configuration /plugin这个配置把 lombok 从普通依赖里剥离出来单独作为注解处理器参与编译类路径里就不会再出现“程序包不存在”的问题。5.2 IDE 正常、命令行编译后没有 getter现象IDEA 里编译运行一切正常但用脚本 javac 编译后反编译 class 发现没有 getter 方法运行期调用直接 NoSuchMethodError。原因IDEA 默认从 classpath 里扫描所有处理器命令行则要看-processorpath是否正确指向 lombok 所在位置。另一个常见原因是 spi 配置文件没被打进处理器 jarjavac 根本发现不了处理器。解决先用javac -processorpath显式指定 jar再用javap -p检查 class 内容如果还是没有就解压 lombok jar 确认META-INF/services/javax.annotation.processing.Processor文件里确实包含了处理器类名。5.3 仿写处理器时生成的方法报“找不到符号”现象自定义注解处理器跑完生成的 class 里能看到方法但编译过程中报“找不到符号”。原因最常见的是MethodDef里的返回类型表达式引用不正确或者方法插入后跟已有方法产生冲突。还有一种是构造方法体时用了字符串拼接类型名而真正需要的是编译单元里同一个类型系统的符号引用。解决先用javac -Xprint打印树结构对照 Lombok 的 JavacHandlerUtil 看它怎么构造节点。重点检查方法签名里的 JCExpression 是否来自同一个编译单元不能从其他上下文直接拿一个裸的字符串。5.4 多轮次处理器导致重复生成现象自定义处理器跑完后编译报“attempting to duplicate class definition”或者类里出现两个一模一样的方法。原因处理器没有判断roundEnv.processingOver()最后一轮又把生成逻辑执行了一遍。另一个可能是在 process 方法里返回了 true导致本应传递给他人的注解被自己重复消费。解决process 方法最前面先判断 processingOvertrue 就直接返回 false业务处理结束后也返回 false让注解继续传递。这样既避免重复生成也避免误伤其他处理器。5.5 反射拿不到 Lombok 注解现象运行期通过field.getAnnotation(Getter.class)判断字段是否存在 Getter结果全是 null。原因Lombok 的注解基本都是Retention(SOURCE)编译后不会写进 class 文件的注解区。运行期反射从 class 字节码里找注解自然找不到。解决如果运行期真的需要感知某个注解存在不要复用 Lombok 的注解自己定义一个Retention(RUNTIME)的注解然后在编译期处理器里把 Lombok 注解的信息转写到运行时注解上或者生成一份常量索引类。这个思路也是很多框架做编译期元数据收集的常见做法。6. 进阶核对用三个验证手段确认生成代码确实存在装配一个自定义 Lombok 风格处理器之后光看编译通过还不够怎么确认生成代码存在也值得核一遍。第一个手段是-XD-printflat它会打印经过完整注解处理流程后的源码生成的中间文件里带_前缀。命令行里试一下javac -XD-printflat -processorpath ./processor-jars -d ./flat ./src/**/*.java在输出目录里找到 User.java 对应的 flat 文件里面能看到处理器生成的 getter 方法是否被并入了原类。这个手段对调试 TreeMaker 插入逻辑特别有用能直观看到树改造的结果。第二个手段是 javap 反编译字节码javap -p -c out/com/example/demo/User.class-p显示所有成员-c显示每个方法的字节码。如果看到对应的 getXxx 方法说明生成逻辑真的跑通了如果方法缺失回去检查处理器是否被加载或者 SPI 配置是否写对。第三个手段是在处理器里主动打日志用processingEnv.getMessager().printMessage输出每个字段的处理过程再配合javac -verbose查看注解处理的轮次日志。这个手段能确认处理器进入了几轮、每轮看到了哪些元素对排查多轮次重复生成问题很有效。有一段时间我在调试一个仿写处理器总在“方法生成了但调用不到”的怪圈里打转后来发现是 javac 多轮次把旧树覆盖了。从那以后我每次写完这类处理器都会强制走一遍三连验证flat 文件看合并结果、javap 看字节码、再对照 Lombok 的 JavacHandler 实现复查逻辑几乎不再遇到玄学问题。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

基于OpenCV contrib的人脸关键点检测与美颜算法实现

基于OpenCV contrib的人脸关键点检测与美颜算法实现

简介:一套以OpenCV为基础的人脸美颜算法资料,围绕美白、扩眼、瘦脸三种典型人像美化需求展开,适合有一定图像处理基础、希望快速上手真实项目的开发者。包内共85个文件,压缩后约520MB,组成上兼顾课件、源码与依赖&…

📅 2026/10/20 2:29:18
PotPlayer+LAV+madVR:本地视频解码渲染链路与画质调校指南

PotPlayer+LAV+madVR:本地视频解码渲染链路与画质调校指南

简介:面向追求高清画质的PC播放用户,特别是动漫爱好者,系统讲解Potplayer搭配LAV Filters与madVR三大组件实现高品质视频播放的完整配置方案。资源为1个doc文档,约1.8MB,内容涵盖从软件下载安装、外部滤镜添加、解码器…

📅 2026/10/20 2:24:18
降完AI率重复率又高了怎么办?降重降AI一起做,知网AI率降到3.8%!

降完AI率重复率又高了怎么办?降重降AI一起做,知网AI率降到3.8%!

降完AI率重复率又高了怎么办?降重降AI一起做,知网AI率降到3.8%! 降完AI率重复率又高了,这大概是今年毕业季最让人崩溃的一种情况。前两天刚把AIGC报告上的红色段落一句句改顺,心想总算能交了,转头一查重&a…

📅 2026/10/20 2:24:18
MORE NEWS

更多资讯

📰

context-mode实战:构建上下文感知的桌面自动化效率工具

context-mode:把“我现在在干嘛”变成系统行为的实战设计先说个我自己的真实感受。同时维护四五个项目的时候,每天最累的不是写代码,而是频繁切换状态。前一分钟还在看生产环境的日志,后一分钟切回编辑器写需求,再过一…

📰

基于潜在时间图的递归时空图神经网络:原理、实现与实战

1. 问题背景与核心思路:时空预测为什么需要“潜在时间图”前阵子我接手了一个城市传感器流量预测项目,输入是分布在城市不同位置的传感器每小时回传的数值,目标是预测未来 12 个时刻的变化。这类问题我们一般称为“时空预测”。我这次用的方案…

📰

本地部署AI Agent:多模型配置与Skill安装避坑实战

最近我基本把自己泡在一套开源智能体框架里。这东西能本地部署、能接多路大模型、还能用 Skill 把模型从“聊天机器”变成“能干活的执行器”。折腾完之后,我最想说的不是“它有多好用”,而是“文档里没写的坑到底在哪”。这篇就把多模型配置&#xff08…

📰

AI应用落地中的Skills:从提示词到可复用能力封装

最近一两年做AI应用落地,有个词出现频率越来越高:skills。很多人第一次听到它,以为是那种"Excel技能清单""职场软技能列表"之类的东西,结果一问才知道,圈里讨论的完全是另一回事。我最初接触这个概…

📰

Windows 11 优化完整教程:用 Windows11 开源项目快速完成 7 项系统调优

Windows 11 优化完整教程:用 Windows11 开源项目快速完成 7 项系统调优 【免费下载链接】windows11 🌎 Windows 11 Settings, Tweaks, Scripts 项目地址: https://gitcode.com/GitHub_Trending/wi/windows11 Windows11 是一个免费的开源项目&…

📰

法律AI融资背后的渠道逻辑:收银机与等保服务商为何下注

说实话,我是在一个很平常的工作日上午刷到这则融资消息的。某法律AI团队上线刚两个月,宣布完成A轮融资,具体金额没披露,但投资方名单里排头的不是任何一家VC,一个是做智能收银机的厂商,另一个是做等保测评的…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬