在KotlinCI测试期间静态最终变量初始化(在Java中)不正确
我管理一个开源项目,有一个用户报告了一种情况,根据 Java 的类中静态变量的初始化顺序,我认为这是不可能的。一个的价值static final类变量是不正确,显然是从一个依赖的静态方法的不同结果导致基于其自己的静态最终变量。
我想了解发生了什么,以便找出最佳解决方法。此刻,我很困惑。
问题
我的项目的主要入口点是SystemInfo具有以下构造函数的类:
public SystemInfo() {
if (getCurrentPlatform().equals(PlatformEnum.UNKNOWN)) {
throw new UnsupportedOperationException(NOT_SUPPORTED + Platform.getOSType());
}
}
单独运行时,问题不会重现;但是当作为正在执行的许多测试的一部分运行时,一个更大的构建 ( mvn install) 它始终是可重现的,这意味着问题可能与多线程或多个分叉有关。(澄清:我的意思是同时初始化两个不同类中的静态成员,以及与此过程相关的各种 JVM 内部锁定/同步机制。)
他们收到以下结果:
java.lang.UnsupportedOperationException:不支持操作系统:JNA 平台类型 2
此异常意味着SystemInfo实例化开始时有两件事是正确的:
- 结果
getCurrentPlatform()是枚举值PlatformEnum.UNKNOWN - 结果
Platform.getOSType()是2
不过,这种情况应该是不可能的;值 2 将返回 WINDOWS,而 unknown 将返回一个非 2 的值。因为两个变量都是static和final它们永远不应该同时达到这个状态。
(用户的)MCRE
我试图自己重现这个并失败了,我依赖于用户在他们的基于 Kotlin(kotest)框架中执行测试的报告。
用户的 MCRE 只需调用此构造函数作为在 Windows 操作系统上运行的大量测试的一部分:
public class StorageOnSystemJava {
public StorageOnSystemJava(SystemInfo info) {
}
}
class StorageOnSystemJavaTest {
@Test
void run() {
new StorageOnSystemJava(new SystemInfo());
}
}
底层代码
该getCurrentPlatform()方法仅返回此static final变量的值。
public static PlatformEnum getCurrentPlatform() {
return currentPlatform;
}
这是一个static final作为类中第一行填充的变量(因此它应该是初始化的第一件事):
private static final PlatformEnum currentPlatform = queryCurrentPlatform();
在哪里
private static PlatformEnum queryCurrentPlatform() {
if (Platform.isWindows()) {
return WINDOWS;
} else if (Platform.isLinux()) {
// other Platform.is*() checks here
} else {
return UNKNOWN; // The exception message shows the code reaches this point
}
}
这意味着在类初始化期间,所有Platform.is*()检查都返回 false。
但是,如上所述,这不应该发生。这些是对 JNAPlatform类静态方法的调用。第一个检查,应该返回true(并且,如果在构造函数或实例化后的代码中的任何地方调用)是:
public static final boolean isWindows() {
return osType == WINDOWS || osType == WINDOWSCE;
}
定义osType的static final变量在哪里:
public static final int WINDOWS = 2;
private static final int osType;
static {
String osName = System.getProperty("os.name");
if (osName.startsWith("Linux")) {
// other code
}
else if (osName.startsWith("Windows")) {
osType = WINDOWS; // This is the value being assigned, showing the "2" in the exception
}
// other code
}
根据我对初始化顺序的理解,Platform.isWindows()应该总是返回true(在 Windows 操作系统上)。我不明白false从我自己的代码的静态变量初始化中调用时它怎么可能返回。我已经尝试了静态方法和紧跟在变量声明之后的静态初始化块。
预期的初始化顺序
- 用户调用
SystemInfo构造函数 SystemInfo类初始化开始(“T 是一个类并且创建了一个 T 的实例。”)- 该
static final currentPlatform变量由初始化遇到(类的第一行) - 初始化程序调用静态方法
queryCurrentPlatform()以获取结果(如果在紧跟静态变量声明的静态块中分配值,则结果相同) - 该
Platform.isWindows()静态方法被称为 - 这
Platform类被初始化(“T是一个类和T的静态方法调用”。) - 本
Platform类套osType值2作为初始化的一部分 - 当
Platform初始化完成后,静态方法isWindows()返回true - 在
queryCurrentPlatform()看到true结果,并设置currentPlatform变量值(如预期这不会发生!) - 后
SystemInfo级初始化完成后,它的构造函数执行,显示了相互矛盾的价值观和抛出异常。
解决方法
一些解决方法可以解决问题,但我不明白他们为什么这样做:
-
Platform.isWindows()在实例化过程中的任何时候执行检查(包括构造函数)都会正确返回true并适当地分配枚举。- 这包括
currentPlatform变量的延迟实例化(删除final关键字),或忽略枚举并直接调用 JNA 的Platform类。
- 这包括
-
将对该
static方法的第一次调用getCurrentPlatform()移出构造函数。
这些变通方法意味着一个可能的根本原因与static在类初始化期间执行多个类的方法有关。具体来说:
- 在初始化期间,
Platform.isWindows()检查显然返回,false因为代码到达else块 - 初始化后(在实例化期间),
Platform.isWindows()检查返回true。(因为它基于一个static final值,所以它不应该返回不同的结果。)
研究
我已经彻底审查了多个关于 Java 的教程,清楚地显示了初始化顺序,以及这些其他 SO 问题和链接的 Java 语言规范:
- Java静态类初始化
- 类中的静态块和静态变量以什么顺序执行?
- 在 JVM 中加载类时,类的不同部分以什么顺序初始化?