资讯动态

JDK 17反射异常InaccessibleObjectException详解与解决方案

发布时间:2026/9/9 12:14:38 来源:尧图企业网站定制
最近把一个服务从JDK 8直接升到JDK 17启动第一分钟就被java.lang.reflect.InaccessibleObjectException拍脸。我猜你现在搜到这十有八九也是被这一串异常信息折腾得够呛。这篇就把这个异常彻底讲透它为什么会冒出来、怎么从堆栈里快速定位、用哪几种方式能压下去最后再配上几个我实际踩过的案例。JDK 17本身不是问题真正的问题是很多人还在用JDK 8时代的老框架和老写法反射依然在背后访问JDK内部类到了JDK 17这个动作就不被允许了。这个异常最适合正在做JDK版本升级、或者新项目启动就遇到反射报错的同学看完至少能自己解决八成场景。1. 异常本质解析JDK 17的强封装到底是怎么回事1.1 从一段最典型的异常堆栈说起先看一个我在Spring Boot项目里经常碰到的原始报错java.lang.reflect.InaccessibleObjectException: Unable to make field private static final sun.misc.Unsafe theUnsafe accessible: module jdk.unsupported does not opens sun.misc to unnamed module 0x7f5f2b5e at java.base/java.lang.reflect.Field.setAccessible(Field.java:693) at io.netty.util.internal.PlatformDependent0.clinit(PlatformDependent0.java:89) at io.netty.util.internal.PlatformDependent.clinit(PlatformDependent.java:165) ...注意看第一行异常类的全限定名java.lang.reflect.InaccessibleObjectException。它本身是RuntimeException的子类专门在反射操作Field、Method、Constructor的setAccessible(true)时被抛出来表示“你想访问的那个对象不属于你”。很多人第一次看到这个异常会以为是自己代码里setAccessible(true)写错了其实不是。就算你反射的目标代码完全合法只要目标是JDK内部包里的非public成员JDK 17就直接拒绝。1.2 根本原因JDK模块系统下的强封装JDK 9开始引入模块化架构原本一个大而全的rt.jar被拆成了java.base、java.sql、java.xml等一堆模块。模块系统有一个核心规则每个模块可以声明哪些包对外“开放”opens。只有开放的包才允许其他模块通过反射去访问其中的非public成员。JDK 8时代反射几乎想干嘛就干嘛能直接透过setAccessible(true)读到很多JDK内部的私有字段。JDK 9到JDK 16之间还有一个过渡期很多内部包虽然默认不开放但JVM给了--permit-illegal-access这类宽松参数或者只是打印警告。到了JDK 17强封装成为默认且不可绕过非法访问直接抛异常留给你用参数解锁的空间仅限于手动指定的--add-opens。我一般给不太理解模块化的同事打个比方JDK内部类像小区里的业主楼层以前JDK 8任何快递员都能直接上楼反射随意访问JDK 9开始小区装了门禁但保安还会偶尔放人进去并提醒一下告警JDK 17是直接把门禁全面启用没在访客名单里没有配--add-opens的人一概不许进。1.3 哪些框架和场景最容易触发不是我危言耸听只要你的项目满足以下几个条件之一遇到这个异常的概率非常高Spring Boot 2.x 项目直接跑在JDK 17上尤其是用到CGLIB代理、Configuration类代理时Netty框架低版本例如4.1.x早期版本启动时访问sun.misc.Unsafe使用Lombok的旧版本编译期或运行期反射访问JDK内部类用到了Hibernate、MapStruct、ByteBuddy、Mockito等字节码/反射库自己的代码里用getDeclaredField去拿java.base模块里的内部字段我把常见的求助现场整理成了一张表方便你对号入座常见触发方被访问的模块典型内部包典型触发点Spring Boot 2.x / CGLIBjava.basejava.lang, java.lang.reflect动态代理生成类Netty 4.1.xjdk.unsupportedsun.miscUnsafe、DirectBuffer相关ByteBuddy / Mockitojava.basejava.lang, java.util子类生成与MockHibernate 5.xjava.basejava.lang, java.util字节码增强、懒加载代理自研工具类java.basesun.nio.ch, jdk.internal.misc反射强行访问内部实现看到自己项目的影子也别慌这个异常本质上不是代码逻辑错误而是“运行环境与依赖不兼容”的提示。换句话说它告诉你你的JDK和你的依赖之间需要一次版本对齐或者需要给JVM开一扇门。2. 空降现场快速定位异常根源的实操方法2.1 别再只看异常第一行核心在Caused by和完整堆栈遇到这个异常很多人习惯把第一行贴到搜索引擎但我建议你先看完整的堆栈尤其是Caused by那一行和最初的触发入口。解释清楚是什么操作触发的比背下异常信息有用得多。比如下面这段Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.ClassLoader.defineClass accessible: module java.base does not opens java.lang to unnamed module 0x6d6f8f94 at java.base/java.lang.reflect.Method.setAccessible(Method.java:693) at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:398) ...这句话其实已经把所有信息都抛给你了Unable to make ... accessible具体是哪个字段或方法没权限module java.base宿主模块是谁does not opens java.lang缺少对哪个包的开放to unnamed module谁想访问通常就是你的应用本身未命名模块这时候你已经清楚知道要打开什么门了java.base模块里的java.lang包。对应的JVM参数就是--add-opens java.base/java.langALL-UNNAMED这个公式几乎适用所有情况把报错里“module X does not opens Y”原样搬过来就行。2.2 常见的JVM参数组合速查表一次性只加一个参数太被动实际项目里要开的门常常不止一扇。我把自己在多个项目里用过的参数组合整理成了表格遇到异常直接挑需要的加异常信息中出现的模块/包需要添加的参数java.base does not opens java.lang--add-opens java.base/java.langALL-UNNAMEDjava.base does not opens java.util--add-opens java.base/java.utilALL-UNNAMEDjava.base does not opens java.net--add-opens java.base/java.netALL-UNNAMEDjava.base does not opens java.text--add-opens java.base/java.textALL-UNNAMEDjava.base does not opens java.time--add-opens java.base/java.timeALL-UNNAMEDjava.base does not opens java.lang.reflect--add-opens java.base/java.lang.reflectALL-UNNAMEDjava.base does not opens sun.nio.ch--add-opens java.base/sun.nio.chALL-UNNAMEDjdk.unsupported does not opens sun.misc--add-opens jdk.unsupported/sun.miscALL-UNNAMED这里ALL-UNNAMED指的是“未命名模块”说白了就是你的classpath上的所有普通代码。只要你不是自己写了什么模块化module-info.java的项目基本都是这个值。2.3 判断到底是开发环境还是生产环境出的问题同一个异常在不同环境下排查侧重点不一样。开发环境里IDEA跑Spring Boot异常经常出现在启动前几秒而且堆栈里通常带着CGLIB或ByteBuddy字样这时候直接在IDE的VM options里加参数最快。生产环境如果是用java -jar启动的服务报错堆栈往往会带上部署脚本里没有的JVM参数缺失信息。这种时候一定要先确认JAVA_OPTS有没有正确传递到进程。我遇到过很多次Dockerfile里写了ENV JAVA_OPTS--add-opens ...但启动命令没有引用这个变量等于白写。# 错误示范 CMD [java, -jar, app.jar] # 正确示范 CMD [sh, -c, java $JAVA_OPTS -jar app.jar]排查生产环境时还有一个比较狠但高效的命令用jcmd或jinfo看当前进程实际生效的JVM参数确认你加的--add-opens到底有没有进入运行时。3. 三种解决套路升级依赖、JVM参数、代码改造3.1 首选方案把依赖升级到支持JDK 17的版本说到底InaccessibleObjectException是“旧依赖在新JDK上水土不服”的典型症状所以治本思路是升级依赖而不是给JVM开一堆后门。举个例子Spring Boot 2.x在JDK 17上支持得比较勉强Spring Boot 3.0直接要求JDK 17整体适配就顺畅很多。如果你的项目能升级优先升依赖。具体版本我列一下实际可用的最低版本大家心里有个底Spring Boot建议至少2.7.9更好的是直接Spring Boot 3.xNetty4.1.68开始兼容JDK 17推荐4.1.86ByteBuddy1.12.0推荐1.12.9Lombok1.18.22Mockito4.x推荐4.8.0Hibernate5.6.x升级之后原先很多反射访问JDK内部类的逻辑被官方重写或屏蔽不需要你再手动开放。这里有一个实在的建议升级依赖后务必把单元测试跑一遍因为有些依赖换了实现方式行为有细微差别特别是框架底层涉及Unsafe的地方。3.2 兜底方案用--add-opens参数给JVM开一扇门如果项目暂时不能升依赖或者升级成本太高退而求其次就是在JVM层面给受影响的模块包手动开放。这个方案我几乎每天都在用足够应付绝大多数场景。先看命令行方式。假设你的应用是自己写的直接用--add-opens完整命令行长这样java \ --add-opens java.base/java.langALL-UNNAMED \ --add-opens java.base/java.utilALL-UNNAMED \ --add-opens java.base/java.netALL-UNNAMED \ --add-opens java.base/sun.nio.chALL-UNNAMED \ --add-opens jdk.unsupported/sun.miscALL-UNNAMED \ -jar app.jar如果你用的是IDEA打开Run/Debug Configurations在VM options里输入同样的参数即可。注意IDEA里填这串参数时一定不要加java前缀直接写--add-opens开头的参数就行这个细节有同事栽过。Docker环境里建议用环境变量统一传这样Dockerfile和启动脚本解耦ENV JAVA_TOOL_OPTIONS--add-opens java.base/java.langALL-UNNAMED --add-opens java.base/java.utilALL-UNNAMEDJAVA_TOOL_OPTIONS这个环境变量的好处是JVM启动时会自动读取它不管启动命令怎么写都能生效特别适合部署平台统一注入。但也正因为它是全局的如果一台机器上跑多个Java应用会影响所有应用所以生产环境我通常只在应用专用镜像里设置。3.3 别把--add-opens和--add-exports搞混做JDK模块化排障时经常还会遇到另一个相似异常提示的是Package 包名 not exported而不是does not opens。这时候要用的参数是--add-exports不是--add-opens。我简单说一下两个参数的本质区别--add-exports让某个包可以被其他模块在编译期引用解决的是“看不见”的问题--add-opens让某个包可以被其他模块在运行期反射访问解决的是“反射访问不到”的问题因为InaccessibleObjectException是运行期反射导致的所以绝大多数情况下你需要的是--add-opens。只有在你看到ClassNotFoundException或者Package x not exported这类编译/加载期错误时才考虑--add-exports。这个点很多人会绕进去我特意写出来提醒一下。还有一点要注意JDK 17里--permit-illegal-access参数已经被移除了网上搜到旧文章让你加这个的直接忽略加了JVM会报“Unrecognized option”反而影响启动。4. 实战案例三个典型场景的完整排查过程4.1 Spring Boot 2.7在JDK 17启动即崩我维护的一个内部管理系统从JDK 8升到JDK 17启动不到三秒就抛异常。完整堆栈关键部分如下Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.ClassLoader.defineClass accessible: module java.base does not opens java.lang to unnamed module 0x1b7d6c3c at java.base/java.lang.reflect.Method.setAccessible(Method.java:693) at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:398) at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:319) ...定位过程其实很快异常发生在Spring的CGLIB动态代理生成代理类时它反射调用了ClassLoader.defineClass这个受保护方法。JDK 17强封装下这段反射路径被禁了。我的处理方式分两步。第一步先试升级Spring Boot从2.7.2升到2.7.9结果发现还是有类似问题因为CGLIB在2.7.x里依然是默认的代理实现只是部分修复了。第二步加参数兜底java --add-opens java.base/java.langALL-UNNAMED -jar app.jar启动恢复正常。不过这里要提醒一句Spring Boot 2.7加这一个参数不一定够运行过程中如果又冒出别的异常比如java.lang.reflect.InaccessibleObjectException: Unable to make field ... accessible就再照着第2章的速查表补即可。4.2 ARM Linux服务器上Netty离线安装后的报错有段时间我在一台ARM架构的Linux服务器上离线安装了JDK 17然后部署一个Netty服务。离线安装本身倒没踩坑真正难受的是服务一启动Netty内部访问sun.misc.Unsafe的代码直接触发了同样的异常java.lang.reflect.InaccessibleObjectException: Unable to make field private static final sun.misc.Unsafe theUnsafe accessible: module jdk.unsupported does not opens sun.misc to unnamed module 0x8f3b52a at java.base/java.lang.reflect.Field.setAccessible(Field.java:693) at io.netty.util.internal.PlatformDependent0.clinit(PlatformDependent0.java:89)因为Netty在初始化时会尝试通过反射获取Unsafe实例用于直接内存分配等操作。我参照速查表在启动脚本里加入了JAVA_OPTS$JAVA_OPTS --add-opens jdk.unsupported/sun.miscALL-UNNAMED java $JAVA_OPTS -jar netty-server.jar对就是这个参数Netty顺利跑起来了。之后我又顺手在JAVA_TOOL_OPTIONS里加了同一个参数这样不管谁用哪个脚本启动都会自动带上省得那天同事换了个启动方式又把异常带出来。4.3 自己写的反射代码碰了JDK内部包有的项目里同事为了拿Unsafe或者其他内部类会写类似这样的代码Field f sun.misc.Unsafe.class.getDeclaredField(theUnsafe); f.setAccessible(true); Unsafe unsafe (Unsafe) f.get(null);在JDK 8下没问题JDK 17一跑立刻炸。这个场景我给出的建议很直接如果是为了写框架底层或者学习验证可以临时加--add-opens jdk.unsupported/sun.miscALL-UNNAMED但如果是正式业务代码强烈建议改用公开API或者通过MethodHandles来操作。为什么因为JDK官方已经把很多内部实现关闭了今天你加参数能开下次升级JDK可能连sun.misc这个包都换到别处代码直接废掉。对于想拿Unsafe做高性能操作的场景可以看看jdk.internal.misc.Unsafe但那个包的访问门槛更高也需要--add-opens我一般不建议碰。业务代码就老实点用ByteBuffer.allocateDirect()这类标准API性能差不了多少但维护成本低得多。5. 常见问题速查表与避坑心得5.1 典型问题排查表问题原因解决方案加了--add-opens还是报错参数位置不对或没传到JVM用java -jar时参数必须放在-jar之前或在JAVA_TOOL_OPTIONS里设置IDEA里配置了VM options但没生效配置在错误的运行配置上确认当前启动类是哪个配置直接在目标配置里修改同一个异常在测试环境好了生产又出部署脚本没把JAVA_OPTS传进启动命令用jps -lvm或jinfo看实际JVM参数报错说Package xxx not exported需要的是--add-exports而不是--add-opens换成--add-exports对应模块包升级依赖后应用行为变化新版本框架重写了底层反射逻辑跑一遍集成测试重点验证代理、序列化、缓存相关功能JDK 17启动时提示Unrecognized option: --permit-illegal-access旧参数已移除删掉该参数改用--add-opens5.2 我自己的几个避坑习惯第一遇到InaccessibleObjectException我第一件事不是搜索而是复制完整堆栈到本地文件里用grep把“module”和“opens”关键字全部提取出来。这两个关键字后面就是你要加的包名比对着屏幕猜效率高得多。第二能用--add-opens解决的问题我不会为了图稳定去给所有内部包都开一遍。比如只报java.lang就只开java.lang不要一上来就全开十个参数否则线上出了问题排查范围更大。第三也是我个人最大的体会这个异常本质上是“时代切换”的信号——你的代码和依赖还在上一个时代。临时开参数是给项目争取改造时间不是终极方案。有条件的话还是安排一次依赖升级专项把支持JDK 17的新版本框架、库统一拉齐。最后分享一个小技巧如果你经常要在不同JDK版本间切换测试可以在~/.bashrc或~/.zshrc里把常用的--add-opens组合定义成一个变量比如export JAVA17_OPTS--add-opens java.base/java.langALL-UNNAMED --add-opens ...每次启动直接java $JAVA17_OPTS -jar xxx.jar省得来回复制。我那几个天天折腾JDK版本的项目基本都靠这个习惯省下了不少时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价