JEP 539「Strict Field Initialization」已经 Integrated 进 JDK 28,28-ea+20 上能用。它给 class 文件加了一个字段标记 ACC_STRICT_INIT:被标上的 static 字段在类初始化结束前必须赋过值,否则类直接起不来。Java 语言目前没有对应的语法,得靠 Class-File API 自己往字节码里打。
我用 Spring Boot 4.1.1 在 28-ea+20 上现场生成三种类做对照。普通 static 字段没赋值时读到的是 null;同样没赋值但加了 STRICT_INIT,类初始化抛 IllegalStateException;在 <clinit> 里赋好值的 STRICT_INIT 字段则正常可读可改。
这是给谁准备的
JEP 原文写得很清楚:目标用户是往 JVM 上挂的语言实现和编译器,不是日常业务代码。值对象(JEP 401)会依赖它,值类的字段一律打上 ACC_STRICT_INIT。单独打开这个预览特性不需要值对象,--enable-preview 即可。
运行时要带预览,class 文件的次版本号得是 65535(ClassFile.PREVIEW_MINOR_VERSION)。不加 --enable-preview 去加载,会直接 UnsupportedClassVersionError。
用 Class-File API 生成三种类
核心工厂根据模式拼字节码:字段打不打 STRICT_INIT,以及 <clinit> 里赋不赋值。
AccessFlag[] fieldFlags = strict
? new AccessFlag[]{AccessFlag.PUBLIC, AccessFlag.STATIC, AccessFlag.STRICT_INIT}
: new AccessFlag[]{AccessFlag.PUBLIC, AccessFlag.STATIC};
clb.withField("secret", string, fb -> fb.withFlags(fieldFlags));
clb.withMethodBody("<clinit>", MethodTypeDesc.of(ConstantDescs.CD_void),
ClassFile.ACC_STATIC,
cob -> {
if (setInClinit) {
cob.ldc("from-clinit").putstatic(self, "secret", string);
}
cob.return_();
});
生成的类要和 MethodHandles.lookup() 同包,才能 defineClass。版本写成 28 加预览次版本:
clb.withVersion(ClassFile.JAVA_28_VERSION, ClassFile.PREVIEW_MINOR_VERSION);
三种模式:PLAIN_UNSET(普通字段、clinit 空)、STRICT_UNSET(STRICT_INIT、clinit 空)、STRICT_SET_IN_CLINIT(STRICT_INIT、clinit 里写成 "from-clinit")。每种都暴露 read() / write(String) 两个静态方法。
跑出来的结果
应用用 --enable-preview 启动,端口 18089,请求 /probe 一次跑完三种模式。
openjdk version "28-ea" 2027-03-23
OpenJDK Runtime Environment (build 28-ea+20-1588)
OpenJDK 64-Bit Server VM (build 28-ea+20-1588, mixed mode, sharing)
$ java --enable-preview -jar target/strict-init-demo-0.0.1.jar
08:13:05.831 INFO DemoApplication - Started DemoApplication in 1.275 seconds (process running for 1.648)
$ curl -s localhost:18089/probe
{
"jdk": "28-ea+20-1588",
"previewMinor": 65535,
"plain": {
"mode": "PLAIN_UNSET",
"accessFlags": "PUBLIC,STATIC",
"classBytes": 307,
"readBeforeWrite": "null",
"afterWrite": "hello"
},
"strictUnset": {
"mode": "STRICT_UNSET",
"accessFlags": "PUBLIC,STATIC,STRICT_INIT",
"classBytes": 307,
"classInitError": "java.lang.IllegalStateException: Strict static \"secret\" is unset after initialization of com.spring4all.strictinit.Holder2"
},
"strictSetInClinit": {
"mode": "STRICT_SET_IN_CLINIT",
"accessFlags": "PUBLIC,STATIC,STRICT_INIT",
"classBytes": 329,
"readBeforeWrite": "from-clinit",
"afterWrite": "hello"
}
}
普通字段和以前一样,没赋值就读到 null。加了 STRICT_INIT 却不在 <clinit> 里赋值,类初始化阶段就失败,异常信息点名字段 secret。在 <clinit> 里赋好值之后,首次 read 拿到 "from-clinit",再 write("hello") 也能改。
accessFlags 是反射侧的确认:后两种模式都能看到 STRICT_INIT。
不加预览会怎样
编译开了预览、运行时忘了加 --enable-preview,应用自己起不来。根因是预览 class(72.65535)进不了 JVM:
java.lang.UnsupportedClassVersionError: Preview features are not enabled for com/spring4all/strictinit/StrictFieldFactory$Mode (class file version 72.65535). Try running with '--enable-preview'
这里指向业务类的内部枚举。用 Class-File API 现场 defineClass 生成的预览 class,不加预览同样是这类 UnsupportedClassVersionError。
JDK 28 这条线上已经定档或合入的还有 简单 JSON API、结构化并发转正等。JEP 539 现在还是预览 VM 特性,业务项目用不上,做字节码工具或跟值对象相关的实验时值得摸一下。本文只测了 static 字段;实例字段的严格初始化走的是字节码校验,违规会在加载期被拒绝,没有在这个 Spring Boot 应用里单独演示。
一起交流
分享你的思考,让讨论更进一步。