JEP 500:通过限制反射来强制实现 Final 字段的严格不可变性
来源: InfoQ - 后端
JEP 500,“让Final名副其实(Prepare to Make Final Mean Final)”已经在JDK 26中完成。该JEP旨在为Java生态系统做好准备,未来将不再允许通过深度反射(deep reflection)修改声明为final的字段,以前,这种操作通常是通过AccessibleObject类中定义的setAccessible()方法实现的。此举标志着一条弃用路径的开始,在未来的JDK版本中,默认行为将会变为抛出IllegalAccessException,从而彻底堵上Java封装模型中长期存在的一个漏洞。
长期以来,尽管final字段被视为不可变性的契约,但Java历史上一直允许通过反射破坏这一契约。这个最初为支持对象序列化而保留的漏洞,被OpenJDK团队称为累积的“完整性债务(integrity debt)”。随着时间的推移,依赖注入、序列化和Mock框架广泛依赖反射修来修改final字段,这迫使JVM的即时编译器(JIT)不得不做出保守性的假设,从而限制了依赖真正不可变性的优化(如常量折叠)。通过恢复final关键字更强的不可变性保障,平台将能启用更激进的优化,并为开发者和架构师提供更可靠的语义,尤其在并发程序中。
在JDK 26的新行为中,尝试通过深度反射修改final字段的代码仍会成功执行(以保持兼容性),但JVM会在每个模块首次发生此类操作时默认输出警告。
考虑如下的样例:
java.lang.reflect.Field f = MyClass.class.getDeclaredField("value");
f.setAccessible(true); // 深度反射
f.set(myInstance, 42); // 修改一个final字段在运行期,这段代码将触发一个警告,指出final字段已被反射修改,如下所示:
WARNING: Final field value in MyClass has been mutated by ...该行为由--illegal-final-field-mutation选项控制,JDK 26中它的默认值为warn。
为管理这一转变,JEP 500引入了多种运行时模式,与近年来Java版本中其他访问控制机制的做法类似:
--illegal-final-field-mutation=warn(默认值)
继续支持反射,但是会发出一个警告
--illegal-final-field-mutation=deny
当尝试进行反射变更时,会抛出IllegalAccessException而失败
--illegal-final-field-mutation=allow
允许变更且没有警告(不推荐)
开发者也可在启动时选择性启用反射修改final字段的功能,例如:
java --enable-final-field-mutation=ALL-UNNAMED ...
或针对特定模块启用,从而为目前无法更新的遗留库提供可控的逃生通道。
为了进一步帮助团队为未来更严格的强制执行做好准备,JDK 26还集成了JDK Flight Recorder(JFR)。新增的jdk.FinalFieldMutation事件会记录每次final字段被修改的情况,并附带完整堆栈跟踪。这使得团队能在“deny”模式成为默认值之前,全面审计整个依赖树中的违规行为。
# Auditing an application for final field mutations
java -XX:StartFlightRecording:filename=audit.jfr -jar my-app.jar
jfr print --events jdk.FinalFieldMutation audit.jfr通过这些变更,Java正回归对final更严格、更贴近其最初设计意图的阐述,同时仍为遗留场景提供了清晰的迁移路径。
原文链接:
JEP 500: Java to Enforce Strict Final Field Immutability by Restricting Reflection