背景
遇到一个有趣的问题。项目是一个SpringBoot单jar服务(MySQL + 微信小程序 / 管理后台双端)。刚完成一次大版本升级:Spring Boot 4.1.2 + Java 27(本地与线上镜像均为 Corretto 27.0.1),Lombok用的是SrpringBoot 依赖管理给出的最新版1.18.48。升级后mvn clean test 一路绿灯、45个测试全过,唯独编译阶段多出4行WARNING。它们不是业务代码的问题,全部来自Lombok——这篇把它拆开讲清楚。
结论先行:这是Lombok用
sun.misc.Unsafe绕过Java模块系统访问检查的副作用,官方明确表态暂不修复;当前最干净的规避是给构建JVM加一行--sun-misc-unsafe-memory-access=allow。实测--add-opens消不了这个警告。
一、现象
mvn clean test 编译阶段输出:
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by lombok.permit.Permit
WARNING: Please consider reporting this to the maintainers of class lombok.permit.Permit
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
特征:
只在JDK 24+ 出现(旧JDK无此告警机制);
每个JVM进程只打一次,且点名的是
objectFieldOffset。JVM对Unsafe 内存访问方法是「首次调用统一告警」:谁第一个踩线就点名谁。Lombok里第一个踩线的是Permit类的静态初始化块——UNSAFE.objectFieldOffset(f)拿AccessibleObject.override字段的内存偏移,警告在这一刻打出来。此后每次编译中setAccessible走的UNSAFE.putBoolean(...)同样在用Unsafe裸写内存,但JVM已经报过、不再重复(哪行代码触发,见第二节的源码);出现在compile 阶段:Lombok是注解处理器,跑在Maven进程内的javac里;
不影响编译产物和运行期:Lombok是
provided作用的编译期工具,Unsafe只在构建时用。
二、原因:用内存偏移量伪造反射权限
Lombok不是普通的注解处理器——它是寄生在编译器内部的AST改写器(@Getter之类直接改语法树)。要做到这一点,它必须调用javac / ECJ的一堆内部API(com.sun.tools.javac.*、org.eclipse.jdt.internal.*)。链条如下:
JDK 9模块系统之后,
jdk.compiler没有open这些包,对它们的成员setAccessible(true)会触发访问检查并抛InaccessibleObjectException——除非构建JVM显式加--add-opens;Lombok不想要求用户加flag,于是反其道而行:
setAccessible(true)的本质只是把AccessibleObject内部的override字段置true(置位后反射调用跳过访问检查)。那我就绕过setAccessible,直接改这个字段;改它本身也需要越权,于是用Unsafe的
objectFieldOffset(拿字段内存偏移)+putBoolean(按偏移裸写)。
private static final sun.misc.Unsafe UNSAFE =
(sun.misc.Unsafe) reflectiveStaticFieldAccess(sun.misc.Unsafe.class, "theUnsafe");
public static <T extends AccessibleObject> T setAccessible(T accessor) {
if (INIT_ERROR == null) {
UNSAFE.putBoolean(accessor, ACCESSIBLE_OVERRIDE_FIELD_OFFSET, true); // 越权置位
} else {
accessor.setAccessible(true); // Unsafe 拿不到时才回退普通路径
}
return accessor;
}
两个细节值得品味:
Unsafe优先,回退在后——只要Unsafe可用就永远走第一分支。这是后文「add-opens 消不了警告」的根因;
拿不到
override字段时,它用同内存布局的假类兜底估算偏移:class Fake { boolean override; Object accessCheckCache; }。作者自己都注释了 “The below seems very risky”。
对应回第一节的「只报一次」:告警发生在静态块的objectFieldOffset 那一行(每个JVM一次);而 setAccessible里的putBoolean才是真正的日常工作——Lombok每拿到一个javac / ECJ内部的Method / Field / Constructor(一次编译要拿几十个)就调一次setAccessible,也就是按偏移把override裸写为true几十次。这些调用全部发生在告警之后,所以静默。
而告警本身来自JDK的退出计划:JDK 23(JEP 471) 将Unsafe内存访问方法标记为未来弃用;**JDK 24(JEP 498)**起默认在首次调用时打印这4行运行时警告。
三、为什么官方不修
issue脉络非常清晰:#3852(2025-03关闭,关闭者是 Lombok作者rzwitserloot)→ #3907(dup)→ #4046(后续跟踪)。
关键原话:
“We’re aware, we won’t forget.” … “the solution isn’t set in stone yet: having a ticket for it feels like more a burden than a useful thing” —— rzwitserloot
“Lombok does not need sun.misc.Unsafe, we only use it to make it way easier to setup a lombok build… You have to add a bunch of --add-exports and --add-opens flags if we do this without additional changes.” —— Rawi01
开发者的抱怨集中在两点:
一是修起来并不难,难的是修完没法向用户交代:去掉Unsafe意味着每个用户的构建脚本都要加一串--add-exports / --add-opens,而Lombok从2009年起的核心承诺就是「扔个jar进classpath,别的都不用管」。
二是问题比4行警告大得多:真正的课题是「如何在模块系统彻底关闭后门后继续寄生在编译器内部」,方案至今没有定论,作者觉得挂着一个解决不了的issue反而是负担,干脆关掉。issue以「已完成」状态关闭但实际没修,社区对此有意见,这才补开了#4046 继续跟踪。
说白了:这是产品决策,不是技术偷懒。Lombok能修,代价是「扔个jar就能用」这个核心卖点没了。Unsafe是它的零配置基座,官方计划等JDK真正开始deny / 移除时,再切换为「新JDK上跳过Unsafe + 要求用户自己加flag」。
四、难点在哪
没有合法替代API。模块系统的封装没有「自查自开」的口子:MethodHandle、
privateLookupIn、VarHandle全都未开放。Unsafe是唯一的万能钥匙,而它正被JDK有计划地收走——替代品只有「把 flag 塞给用户」;flag要加对JVM,拓扑千差万别。javac默认跑在Maven / Gradle守护进程内,标志得绕道编译插件的fork配置或
org.gradle.jvmargs才能进到正确的JVM;Eclipse/IDEA构建又是各自的JVM。同一个方案要在各种构建情况下都成立,文档与支持成本极高;外部 deadline 一直没定。JEP 498只说deny默认在「JDK 26 or later」、移除分两波进行。截至JDK 27(2026-10 实测默认仍是warn),主动改造缺乏硬时限,观望是理性选择。
五、暂时性规避
方案 A:--sun-misc-unsafe-memory-access=allow(推荐)
语义是「允许使用、不打警告」,行为零变化。注意这个标志是JVM选项而非编译选项,所以不能直接写进 <compilerArgs>——正确姿势是让Maven把javac fork成独立进程,再用-J前缀把选项转交给那个 JVM,写进pom即可随项目走:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<fork>true</fork>
<compilerArgs>
<arg>-J--sun-misc-unsafe-memory-access=allow</arg>
</compilerArgs>
<!-- annotationProcessorPaths 等原有配置不受影响 -->
</configuration>
</plugin>
实测(JDK 27 + 357 个源文件)两种写法对比:
fork=true+-J--sun-misc-unsafe-memory-access=allow:构建成功,警告归零。代价是每次编译多起一个 javac 子进程,对构建速度几乎无感;不加
fork、裸写<arg>--sun-misc-unsafe-memory-access=allow</arg>:直接构建失败,javac 报错误: 无效的标记——此时javac还跑在Maven进程内,<compilerArgs>只能喂给 javac 自己,它不认识这个 JVM 选项。Gradle等价写法:
tasks.withType(JavaCompile).configureEach { options.forkOptions.jvmArgs << '--sun-misc-unsafe-memory-access=allow' }有效期提醒:该选项是临时方案,JEP 498 阶段 3(默认 deny)起
allow可能不再可用。
方案 B:--add-opens —— 实测无效,别白费劲
不少文章说「加--add-opens jdk.compiler/... 就好」。实测(JDK 27 + Lombok 1.18.48):把 javac的10个包全部open,警告原样输出4行。原因见第二节——Permit.setAccessible是Unsafe优先,setAccessible只是它拿不到 Unsafe 时的回退,add-opens 根本影响不到主路径。它真正的用武之地是未来Lombok被迫切换后的那套配置,现在加了无害但也无用。
方案 C:保持跟踪
Lombok 版本随 JDK 走(1.18.48 = JDK 27 支持),盯住 #4046。某天某版 JDK 默认 deny 时,Unsafe 路径会直接抛异常——届时Lombok会发布要求add-opens的适配版本,升级并按提示补flag即可。