class lombok.javac.apt.LombokProcessor这行报错,几乎是每个 SpringBoot 项目在升级 JDK 或者换机器之后都会撞上的一道墙。表现很典型:IDEA 里点运行一切正常,Getter/Setter 全都认,偏偏mvn clean package一敲下去就红,日志里翻滚着 LombokProcessor、jdk.compiler、IllegalAccessError 之类的字眼,定位半小时找不到头绪。这篇东西就是把我这几年处理 SpringBoot + Maven + Lombok 打包失败的整套思路摊开讲清楚:三类报错怎么区分、Lombok 为什么非要碰 javac 的内部结构、JDK 与 Lombok 的版本对应关系到底怎么算、几种修法分别适合什么场景,以及在改不动版本的前提下怎么用编译器参数先让流水线跑起来。内容偏实战,面向的是手上有具体项目、需要一个能直接抄的 pom 配置的人,也照顾刚接触 Maven 依赖管理和注解处理机制的读者。
1. 先把报错认清楚:LombokProcessor 失败的三种面孔
1.1 三种典型堆栈,对应三个完全不同的问题
很多人在搜索引擎里看到别人贴的解决方案,照着改了一遍发现没用,根本原因是把三种不同的错误当成同一种了。LombokProcessor只是类名,异常类型和括号里的那句话才是真正的线索。
形态一:IllegalAccessError,模块没导出包。完整堆栈通常是这样的:
java.lang.IllegalAccessError: class lombok.javac.apt.LombokProcessor (in unnamed module @0x5f6a1c8b) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing to unnamed module @0x5f6a1c8b这是 JDK 9 之后引入模块系统、JDK 16 默认强封装(JEP 396)带来的直接后果。Lombok 老版本要去拿 javac 的内部类,模块系统说“不给”,于是抛 IllegalAccessError。它跟 Lombok 版本、JDK 版本强相关,跟你的业务代码一行关系都没有。
形态二:NoSuchFieldError,字段被 JDK 改掉了。长这样:
java.lang.NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field 'com.sun.tools.javac.tree.JCTree qualid'这个几乎可以断定是 JDK 21 撞上了 1.18.30 之前的 Lombok。JDK 21 把JCImport.qualid这个字段的类型和层级动了,而 Lombok 正是靠直接读写 AST 节点字段来做代码改写的,字段一没,异常就来了。这种错误的特点是编译到一半才崩,前面的语法检查都过了。
形态三:编译说“不支持你用的编译器”。信息很直白:
java: You aren't using a compiler supported by lombok, so lombok will not work and has been disabled再往下翻大概率能看到业务代码里满屏的cannot find symbol: method getName()。这条不是环境不兼容,而是注解处理器根本没被加载起来:要么 IDEA 的 Annotation Processing 没勾上,要么项目用的是 Eclipse 编译器(ecj)而 Lombok 只认 javac,要么annotationProcessorPaths里漏了 Lombok,导致-processorpath上找不到处理器。
还有一种变体是ClassNotFoundException: lombok.javac.apt.LombokProcessor,通常出现在显式配置了annotationProcessorPaths、但 Lombok 的依赖被写成了scope=provided而处理器路径里又没有正确引入的场景,本质也是处理器没上路径。
1.2 为什么总是“打包”挂而“运行”不挂
这是最迷惑人的地方。IDEA 的运行按钮走的是 IDEA 自己的编译流程(JPS 构建进程),它有自己的一套 VM 参数、自己的注解处理器扫描逻辑,甚至用的是 JetBrains Runtime 里那个和项目JAVA_HOME不一样的 JDK。而mvn clean package走的是 Maven 进程,mvn -v显示的那个 Java 版本才是真正决定成败的东西。
我遇到过最典型的一次:开发同学 IDEA 里配的项目 SDK 是 JDK 17,但系统环境变量JAVA_HOME指向的是早年装的一个 JDK 8,mvn package于是拿着 JDK 8 去加载一个为 JDK 17 准备的 Lombok 版本的字节码,或者反过来,老 Lombok 跑在新 javac 上。所以第一条诊断永远不是看 pom,而是:
mvn -v java -version echo $JAVA_HOME三个输出对齐之后,再谈后面的配置。这点时间花得绝对值。
1.3 一分钟初判表
| 现象关键词 | 最可能原因 | 首选动作 |
|---|---|---|
| IllegalAccessError + module jdk.compiler does not export | JDK 16+ 强封装,Lombok 太老 | 升 Lombok 到 1.18.22 以上 |
| NoSuchFieldError + JCImport qualid | JDK 21+ 与 Lombok 1.18.30 以下 | 升 Lombok 到 1.18.32 以上 |
| You aren't using a compiler supported by lombok | 注解处理器未启用 | 查 IDEA AP 开关与 processorpath |
| ClassNotFoundException: LocombokProcessor | 处理器路径缺失或 scope 写错 | 补 annotationProcessorPaths |
| 编译过了但运行缺 getter | 处理器被静默禁用,代码没被改写 | 用mvn -X compile看处理器日志 |
注意:不要一上来就怀疑业务代码或者 SpringBoot 版本。Lombok 的报错 99% 是环境与版本匹配问题,改代码是白费力气。
2. 根因拆解:Lombok 到底对 javac 干了什么
2.1 它不是编译器插件,而是寄生在注解处理器里
很多人以为 Lombok 是一个“编译期插件”,其实从技术定义上讲,它是一个标准的注解处理器(javax.annotation.processing.Processor接口实现),通过 jar 包里META-INF/services/javax.annotation.processing.Processor这个文件被 javac 自动发现。javac 在编译过程中扫描到@Data、@Getter这类注解,就会把 AST(抽象语法树)交给处理器,Lombok 直接在这棵树上“加节点”——给你的类插进去 getter、setter、构造器、equals、hashCode。
问题就出在这:标准注解处理器 API 只能生成新的源文件,不允许修改已有的类结构。Lombok 想要的是后者,所以它绕开了公开 API,直接去 importcom.sun.tools.javac.tree.JCTree、com.sun.tools.javac.processing.JavacProcessingEnvironment这些内部实现类。这些类从来没承诺过兼容性,包名里的com.sun.*就是“你自己负责”的意思。
用生活化的话讲:标准做法是“你去前台填个表,工作人员帮你把文件放进去”;Lombok 的做法是“翻窗户进档案室自己改”。档案室平时不锁门,一切都好;一旦加了门禁、换了锁芯,翻窗的人就摔下来了。
2.2 JDK 16 那道门禁是怎么落下来的
JDK 9 引入 JPMS 模块系统之后,jdk.compiler模块里那些com.sun.tools.javac.*的包默认是“不导出”状态。但为了给生态留缓冲期,JDK 9 到 15 都允许通过--illegal-access=permit这种宽松策略放行,默认还开着,所以大家感觉不到疼。
JDK 16 把默认值改成了deny,门禁彻底落下。于是所有依赖反射、依赖内部包的库都开始报警,Lombok 是重灾区之一。Lombok 官方的应对方式是:在新版本的实现里内置了一套自己的绕过机制,让处理器在被加载时能拿到必要的访问权限,从 1.18.22 起在 JDK 17 上基本可以开箱即用。具体实现手法官方文档着墨不多,从业界的普遍理解是通过内部类加载配合运行时权限调整,把这道门禁在自身范围内打开一个缺口。
这也解释了一件事:为什么同样是 JDK 17,有人升级 Lombok 就好了,有人死活不行——因为后者项目里实际生效的 Lombok 版本并不是他以为的那个。
2.3 版本矩阵:JDK、Lombok、编译插件三角关系
下面这张表是我自己踩坑之后整理的“能用下限”,不是官方声明,而是实际跑通过的经验值。生产项目请以官方 changelog 为准,但用它做初判非常快:
| JDK 版本 | Lombok 最低可用 | maven-compiler-plugin 建议 | 备注 |
|---|---|---|---|
| 8 | 1.16.20 | 3.8.1 | 基本无坑,配置随便写 |
| 11 | 1.18.10 | 3.8.1 | 稳定期组合 |
| 17 | 1.18.22 | 3.11.0 | Boot 3.x 主流组合 |
| 21 | 1.18.30 | 3.11.0 及以上 | JCImport 字段变更分水岭 |
| 22 | 1.18.32 | 3.13.0 | 建议显式指定处理器路径 |
| 23 | 1.18.34 | 3.13.0 | 新项目建议直接上最新 |
三个变量里,最容易失控的是maven-compiler-plugin。它决定了 javac 怎么被调用、-processorpath怎么拼、是否 fork 出独立进程。Spring Boot 2.x 的spring-boot-starter-parent里锁的是 3.8.1,Spring Boot 3.x 锁的是 3.11.0。如果你是从 2.x 升 3.x 的过程中卡住的,一半的原因可能就在这儿——父 pom 帮你锁的插件版本,未必适合你现在的 JDK。
2.4 依赖树里藏着两个 Lombok
这是我认为最隐蔽的一类问题,值得单独拎出来讲。现象是:你明明在dependencyManagement里写了 Lombok 1.18.32,编译还是报老版本的错。原因是某个第三方库(尤其是国内一些工具库、老版本的分页插件、老版本的代码生成器)把 Lombok 作为compile依赖打进自己的 pom 里了,传递依赖带进来一个 1.18.12,Maven 的就近原则在这个场景下未必按你想的走。
一条命令搞定排查:
mvn dependency:tree -Dincludes=org.projectlombok:lombok如果输出里出现两行甚至更多,说明依赖树不干净。这时候光加dependencyManagement可能不够,得在引入方加exclusions,或者干脆统一用第 4 节的annotationProcessorPaths方案,从根上把 classpath 和 processorpath 分开。
实操心得:
mvn dependency:tree一定要加-Dincludes过滤,不然一个中型项目能刷出两千行,眼睛看花也找不到关键那一行。
3. 修法一:把 Lombok 升到与 JDK 匹配的版本
3.1 最小改动方案
如果你的项目结构简单、依赖干净,最快的一条路就是在pom.xml的properties里覆盖父 pom 定义的版本号。Spring Boot 的 parent 里定义了lombok.version这个属性,直接覆盖它是官方支持的写法,不用改dependencyManagement:
<properties> <java.version>17</java.version> <lombok.version>1.18.32</lombok.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies>注意这里没有写<version>,因为spring-boot-starter-parent已经在dependencyManagement里管好了;覆盖属性就等于改了它管的那一版。如果你的项目没有继承 Spring Boot parent(比如用的是spring-boot-dependencies的 import 方式),那lombok.version这个属性是不生效的,得老老实实在dependencyManagement里写死版本。
为什么要用provided而不是compile?因为 Lombok 只在编译期起作用,运行时它的注解(如@Data)保留策略是SOURCE,class 文件里除了那一两个@Generated标记之外什么都不剩。打进最终的可执行 jar 纯属浪费体积,还会在某些静态扫描工具里引发无谓的告警。
3.2 用 dependencyManagement 压住传递依赖
当依赖树里出现多个 Lombok 时,加一段显式的dependencyManagement是必要的。它的作用是“凡是在这棵依赖树里出现的 lombok,不管谁带的,统一用这个版本”。
<dependencyManagement> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.32</version> <scope>provided</scope> </dependency> </dependencies> </dependencyManagement>要提醒的是:dependencyManagement只影响传递依赖的版本选择,不影响注解处理器的搜索路径。如果报错来自-processorpath,那再怎么写dependencyManagement都没用,得看下一节。
3.3 本地仓库和 IDE 缓存要一起清
改完版本号直接跑mvn package,偶尔还是会拿到旧 jar。原因是本地仓库里的~/.m2/repository/org/projectlombok/lombok/下同时存在新旧两个目录,Maven 有时因为时间戳与_remote.repositories记录不一致而选错。暴力但有效:
rm -rf ~/.m2/repository/org/projectlombok/lombok mvn -U clean package -DskipTests-U是强制检查快照与更新,能顺带把镜像里的元数据刷新一遍。IDEA 侧还要做两件事:File → Invalidate Caches / Restart,以及确认Settings → Build, Execution, Deployment → Compiler → Annotation Processors里“Enable annotation processing”是勾上的,并且选择的是Obtain processors from project classpath。如果这里选了Processor path但路径是空的,就会出现前面说的形态三。
4. 修法二:显式声明 annotationProcessorPaths(长期解)
4.1 spring-boot-starter-parent 管不住处理器路径
为什么我不推荐只靠升级版本解决问题?因为它太依赖环境。换一台机器、换一个 CI 镜像、某个同事本地仓库里有个老 jar,问题就可能复发。真正稳的做法是:显式告诉 javac 去哪里找注解处理器,让-processorpath与项目 classpath 彻底解耦。
逻辑很简单:一旦你在maven-compiler-plugin里配了annotationProcessorPaths,javac 就只认这个列表里的 jar,项目 classpath 上那些乱七八糟传递进来的 Lombok 版本自动失联。这就把“外部依赖污染”这条风险通道堵死了。
需要留意的是:Spring Boot 的 parent 只帮忙锁定了插件版本,并不会替你配annotationProcessorPaths。也就是说这一块必须自己写,好处是显式、可控,坏处是漏配了就会变成形态三那个“编译器不受支持”的报错。
4.2 完整配置片段与逐项说明
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> <encoding>UTF-8</encoding> <parameters>true</parameters> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok-mapstruct-binding</artifactId> <version>0.2.0</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>几个关键点值得展开:
<parameters>true</parameters>是为了给方法参数保留参数名信息。Spring MVC 的@RequestParam、MyBatis 的多参数映射、@PathVariable在没写名字的时候都要靠它,JDK 8 上不写也能跑是因为 Spring 自己做了字节码解析,但新版本 Spring 建议显式开启。
source和target我建议改用<release>17</release>,它的效果比 source/target 更严格——会同时校验你调用的 API 是不是真的存在于目标版本里,避免本地用 JDK 21 编译、扔到只有 17 的生产机器上炸掉。
处理器顺序方面,Lombok 应该排在 MapStruct 前面,因为 MapStruct 生成的XxxMapperImpl需要读被 Lombok 改写之后的 getter/setter。lombok-mapstruct-binding这个桥接包的存在意义就是保证两者在同一个注解处理轮次里按正确顺序执行,缺了它经常出现 Mapper 里getName()编译不过的情况。
4.3 多模块项目里的统一管理
多模块是另一个高发场景。父 pom 配好了,子模块自己又写了一个maven-compiler-plugin覆盖掉,或者某个模块压根没继承到配置,就会出现“主模块能打包、工具模块打包失败”的现象。
正确姿势是在父 pom 的<pluginManagement>里统一声明,子模块按需<plugin>引用(或者干脆什么都不写,让 Maven 从 pluginManagement 里取):
<build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <!-- 同上 --> </configuration> </plugin> </plugins> </pluginManagement> </build>子模块如果确实需要不同配置(比如某个模块是 Java 8 的 SDK 客户端),可以在自己的 pom 里重新声明<configuration>,Maven 会做配置合并而不是替换,大部分情况下能正常工作。
4.4 一个容易踩的坑:processorpath 里版本与依赖版本不一致
我见过最离谱的一次排查:pom 里 Lombok 依赖写 1.18.32,annotationProcessorPaths里写的是 1.18.20,两个版本共存。编译时 javac 用 1.18.20 去解析 AST,运行时报没有对应字段,报错信息里那句qualid让人以为是 JDK 版本问题,绕了一大圈。
解决办法就是别名抽成属性,两处都引用同一个变量:
<properties> <lombok.version>1.18.32</lombok.version> </properties>只要${lombok.version}出现的地方都统一,就不可能不一致。这个习惯我在所有项目里都保留了,成本几乎为零,收益是省下无数排查时间。
5. 修法三:升级不了时的 add-opens 兜底方案
5.1 需要放开的包清单
有些场景确实升不了 Lombok:公司有统一的内部构件库版本管控,或者项目上锁了依赖评审流程,一个版本变更要走两周。这种时候可以用 JVM 参数把模块门禁临时打开,让老 Lombok 能正常访问 javac 内部类。
需要放开的包主要集中在jdk.compiler模块下,我在实际配置里通常写全这几条:
--add-opens jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED原则上报错里提到哪个包就放开哪个,但 javac 的反射链路经常是连环的,放开一个又冒出下一个,不如一次性写全。生产环境不建议这么做,真正的长期方案还是升级版本。
5.2 Maven 场景下的放置位置
关键要理解一点:--add-opens是JVM 参数,不是 javac 参数。所以放在<compilerArgs>里是无效的,那会被当成 javac 的选项传进去,javac 会报“unrecognized option”。
默认情况下maven-compiler-plugin是fork=false,javac 就跑在 Maven 自己的 JVM 里。所以参数要加到 Maven 进程上。两种写法:
第一种是项目根目录建.mvn/jvm.config,一行一个参数,不需要开头带-J:
--add-opens jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED这个文件会被 Maven 自动读取并交给 JVM,随项目走,不用改每个人的环境变量,团队协作时最省事。
第二种是MAVEN_OPTS环境变量,适合临时验证:
export MAVEN_OPTS="--add-opens jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED" mvn clean package -DskipTests如果你把编译插件改成了fork=true(有些构建为了隔离内存会这么做),那参数得放到插件的jvmArgs里。jvmArgs这个配置项在maven-compiler-plugin3.11.0 之后才比较完善,旧版本只能用MAVEN_OPTS。
5.3 IDEA 编译进程的 VM 参数
IDEA 侧的编译进程是独立的,.mvn/jvm.config对它没有任何作用。需要在Settings → Build, Execution, Deployment → Compiler的Shared build process VM options里填:
-Djps.track.ap.dependencies=false --add-opens jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED-Djps.track.ap.dependencies=false这一条是拿来治“IDEA 编译结果和 Maven 不一致”的老毛病的,关闭注解处理器依赖追踪可以避免很多 XxxImpl 找不到的诡异情况。代价是增量编译的准确性会下降一点,改完注解处理器相关代码后最好手动 Rebuild 一次。
注意:这些参数是止疼药不是解药。它会让你的构建在有 JDK 安全策略收紧、或者换更新 JDK 时再次失效。上线前把版本升级的账还掉。
6. 不同技术栈组合下的落地配置
6.1 JDK 8 与 Spring Boot 2.x
这套组合是老项目的主力,坑也最少。Lombok 用 1.18.24 或者 1.18.28 都行,不需要任何annotationProcessorPaths、也不需要--add-opens,因为 JDK 8 根本没有模块系统。配置只需要最简单的依赖声明:
<properties> <java.version>1.8</java.version> <lombok.version>1.18.24</lombok.version> </properties>这里唯一要警惕的是“JDK 21 编译、JDK 8 运行”的错配。高版本 JDK 用-source 8 -target 8编译时会打警告,生成的字节码也可能引用新 API。最保险的还是把构建 JDK 和运行 JDK 拉到同一个大版本上,或者至少用<release>8</release>让编译器帮你把关。
6.2 JDK 17 与 Spring Boot 3.x
这是目前新建项目的主流。spring-boot-starter-parent3.2.x 里默认的 Lombok 版本是 1.18.30,maven-compiler-plugin是 3.11.0。理论上什么都不改就能跑起来。
但实际项目里我依然建议加上annotationProcessorPaths,理由是 Boot 3 的项目通常还会引入 MapStruct、QueryDSL、Spring Configuration Processor 这些处理器,它们的顺序和版本一旦交给 classpath 自动扫描,很容易在某个同事的机器上出现顺序错乱。全部写死之后,构建结果是可复现的:
<properties> <java.version>17</java.version> <lombok.version>1.18.32</lombok.version> <mapstruct.version>1.5.5.Final</mapstruct.version> </properties>顺便提一句spring-boot-configuration-processor,很多人在用@ConfigurationProperties写配置类,加上它之后 IDE 里写application.yml会有自动提示,代价是编译期多一个处理器。如果它和 Lombok 在同一份annotationProcessorPaths里,记得顺带把 Lombok 放前面。
6.3 JDK 21 与更新的版本
JDK 21 是分水岭。前文提到的JCImport.qualid字段变更让 1.18.30 以前的 Lombok 全部阵亡,而且报错信息指向的是 JDK 内部类字段,非常难联想。上到 JDK 21 之后,把 Lombok 直接推到 1.18.34 是比较稳妥的做法。
另外 JDK 21 引入了虚拟线程和分代 ZGC,这些跟编译期无关,但有一个细节值得注意:--release 21配合 Lombok 的代码生成时,某些注解(比如@SneakyThrows)生成的代码会用Unsafe相关的操作,在开启-Xlint:all的情况下会有大量告警。这是正常的,不用处理,也别为了消警告去关掉整个 lint。
如果项目上用了 JPMS(模块化,有module-info.java),那就是另一个维度的复杂度了。Lombok 在模块化项目里的处理一直不算顺畅,我的建议是:业务项目不要上 JPMS,用 classpath 模式就好,收益远小于维护成本。
7. 排查实录:六个真实坑位与速查表
7.1 命令行和 IDEA 结果不一致
最常见的坑。表现是 IDEA 编译通过、mvn失败,或者反过来。根因基本是两边 JDK 不一样。除了前面说的mvn -v和java -version,还有一个容易漏:IDEA 的Settings → Build Tools → Maven → Runner里的JRE选项。如果它设成了“Use Project JDK”之外的固定 JDK,命令行和 IDEA 就会各走各的路。
我的习惯是把这个选项显式设成和JAVA_HOME一致,然后 Maven 的pom.xml里用<java.version>把语言级别也写清楚。三处对齐之后再排查其他问题。
7.2 只在 CI 流水线上失败
本地好好的,流水线红。这类问题多半是镜像里的 JDK 变了:CI 的 base image 是maven:3.9-eclipse-temurin-17,某次别人把它改成了maven:3.9-eclipse-temurin-21,编译就炸了。
处理方式有两条:一是把 CI 用的 JDK 版本在流水线配置里钉死,不要用latest或者只写大版本;二是在pom.xml里加 maven-enforcer-plugin 做版本校验,让不符合要求的 JDK 直接在构建早期就报错,而不是等到编译到一半。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <executions> <execution> <id>enforce-java</id> <goals><goal>enforce</goal></goals> <configuration> <rules> <requireJavaVersion> <version>[17,22)</version> </requireJavaVersion> </rules> </configuration> </execution> </executions> </plugin>这个小插件帮我拦下过至少三次“某台机器装了新 JDK 导致构建行为变化”的事故。
7.3 多模块里只有个别模块失败
如果是common模块失败、web模块成功,通常是因为common没有继承父 pom 的插件配置,或者它自己的build段落把父配置覆盖了。用mvn help:effective-pom -pl common能看到合并之后真正生效的配置,比翻三层 pom 快得多。
7.4 编译过了但运行时报找不到方法
前面提过,这是处理器被静默禁用的信号。有一个快速验证方法:编译后去看target/classes下的 class 文件,用javap反编译看看 getter 在不在。
javap -p target/classes/com/example/demo/User.class如果没有看到getName、setName,说明 Lombok 根本没工作。这时候不要怀疑注解写法,直接回去查处理器路径和 IDEA 的 AP 开关。
7.5 常见问题速查表
| 症状 | 高频原因 | 验证命令 | 修复动作 |
|---|---|---|---|
| IllegalAccessError 访问 jdk.compiler | Lombok 老 + JDK 16+ | mvn -v | 升 Lombok 或加 add-opens |
| NoSuchFieldError JCImport.qualid | JDK 21 + Lombok < 1.18.30 | mvn dependency:tree -Dincludes=org.projectlombok:lombok | 升到 1.18.32+ |
| 编译器不受支持提示 | AP 未启用 / ecj 编译器 | IDEA AP 设置 | 启用 javac 与注解处理 |
| 找不到 XxxMapperImpl | MapStruct 处理器缺失 | 查看 target/generated-sources | 补 processorpath |
| 编译成功但 getter 缺失 | 处理器被禁用 | javap -p | 修 processorpath |
| 本地成功 CI 失败 | JDK 版本漂移 | mvn -v在 CI 日志里 | 钉死镜像 + enforcer |
| 依赖树出现两个 Lombok | 传递依赖污染 | dependency:tree -Dincludes | exclusions 或统一 processorpath |
7.6 一个调试手法:让 javac 把处理器吐出来
实在找不到线索时,打开 Maven 的调试输出,javac 的参数会原样打印出来:
mvn -X clean compile 2>&1 | grep -i "processorpath"你能直接看到-processorpath上到底挂了哪些 jar、版本号是多少。这个输出比任何推断都可靠。我一般把这个作为排查的终结手段——只要看到了-processorpath的真实内容,问题基本就锁定了。
8. 治不好就绕开:三条替代路线
8.1 delombok:把注解“展开”成真实代码
delombok是 Lombok 官方提供的能力,作用是把@Data这类注解展开成手写的 getter/setter 源码,生成到target/generated-sources下。这样编译阶段就不再需要注解处理器了,所有版本兼容问题一次性消失。
配置方式是在build里加lombok-maven-plugin,绑定到generate-sources阶段。生成的源码是可读的、可调试的,堆栈信息里的行号也能对上真实代码,这在排查线上问题时其实是加分项。代价是每次构建多花几秒,以及 IDE 里需要把生成目录标记为源码根。
8.2 换成 record 或者手写
如果项目用的是 JDK 17+,把简单的 DTO 换成record是最省事的做法。record天生带构造器、访问器、equals、hashCode、toString,覆盖了@Data八成的使用场景。缺点是不能有可变字段、不能继承,对实体类这种需要和 ORM 框架配合的场景不太合适。
对实体类,我的建议是手写。用 IDEA 的Alt+Insert批量生成访问器,一次成本换长期稳定。一个中型项目里真正需要手写访问器的类可能就几十个,投入产出比没那么差。
8.3 保留 Lombok 但限制使用范围
也有一条中间路线:保留 Lombok,但禁用掉那些最依赖内部 API 的注解。@Data、@Getter、@Setter属于基础能力,路径稳定;而@SneakyThrows、@Cleanup、@Synchronized这些涉及字节码指令注入的注解,在 JDK 大版本更新时更容易出问题。团队里定一条规范,只允许用基础注解,能把风险降一大截。
| 替代方案 | 适用场景 | 迁移成本 | 遗留风险 |
|---|---|---|---|
| delombok | 老项目、需保留注解写法 | 低 | 构建时间增加 |
| record | 不可变 DTO、值对象 | 中 | 不适用于 ORM 实体 |
| 手写访问器 | 实体类、核心领域模型 | 高 | 无 |
| 限制注解范围 | 长期维护的中大型项目 | 低 | 仍有版本依赖 |
我个人在最近两个项目里选了 delombok 加限制注解范围的组合:日常开发仍然写@Data,构建时展开成真实代码,这样既保留了开发体验,又把版本风险挡在了编译期之外。踩过几次 JDK 升级的坑之后,我越来越倾向于把“魔法”控制在构建阶段,而不是留给运行时去猜。如果你的项目现在还在用@Data加一堆@SneakyThrows,又刚好卡在这个打包报错上,那不妨趁这次机会把注解清单收敛一下,长远看比单纯改个版本号值多了。至于版本选择,我的个人经验是:JDK 和 Lombok 的版本跨越不要一次超过一个大版本,比如从 JDK 17 直接跳到 JDK 21 时,先把 Lombok 升到 1.18.34,跑通clean package再动别的,出问题时排查面会小得多。