【发布时间】:2021-09-25 14:57:01
【问题描述】:
我管理一个开源项目,并且有一个用户报告了我认为根据 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*() 检查都返回错误。
但是,如上所述,这不应该发生。这些是对 JNA 的 Platform 类静态方法的调用。第一个检查应该返回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 语言规范:
【问题讨论】:
-
在类顶部移动静态初始化程序块不起作用?
-
也许应该先问这个问题,但是...
private static final PlatformEnum currentPlatform = queryCurrentPlatform();与private static final int osType;在同一个类中并且在其静态初始化块之上?如果是,那么我想知道你是如何设法将除 0 以外的任何内容分配为osType首先。 -
我会通过向所有涉及的类添加调试日志来调查这一点,并查看失败运行产生的日志文件(要将日志输出添加到已编译的类,我会在调试器中执行代码并通过提供在返回 true 之前写入
System.out的“条件”来滥用条件断点)。这样,很容易验证这是一个多线程问题,还是相互依赖的类在完全初始化之前相互看到,或者完全是其他什么。 -
(当然,如果故障不是特定于时间的,您可以简单地在调试器中执行测试套件,添加一些断点,然后单步执行代码以查看发生了什么)跨度>
-
会不会是一些测试改变了
os.name系统属性?
标签: java static-initialization order-of-execution