在 Kotlin CI 测试期间静态最终变量初始化在 Java 中不正确 问题用户的MCRE底层代码预期的初始化顺序解决方法研究

问题描述

我管理一个开源项目,有一个用户报告了一个我认为根据 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 的值。由于两个变量都是 staticfinal,因此它们不应同时达到此状态。

(用户的)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。

但是,如上所述,这不应该发生。这些是对 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。我已经尝试了静态方法和紧跟在变量声明之后的静态初始化块。

预期的初始化顺序

  1. 用户调用 SystemInfo 构造函数
  2. SystemInfo 类初始化开始(“T 是一个类并且创建了 T 的一个实例。”)
  3. 初始化程序遇到 static final currentPlatform 变量(类的第一行)
  4. 初始化程序调用静态方法 queryCurrentPlatform() 来获取结果(如果在紧跟静态变量声明的静态块中赋值,则结果相同)
  5. 调用了 Platform.isWindows() 静态方法
  6. 初始化 Platform 类(“T 是一个类,并且调用了 T 的静态方法。”)
  7. 作为初始化的一部分,Platform 类将 osType 值设置为 2
  8. Platform 初始化完成时,静态方法 isWindows() 返回 true
  9. queryCurrentPlatform() 查看 true 结果并设置 currentPlatform 变量值(这没有按预期发生!
  10. SystemInfo 类初始化完成后,其构造函数执行,显示冲突值并抛出异常。

解决方法

一些解决方法可以解决问题,但我不明白为什么会这样:

  • 在实例化过程(包括构造函数)期间随时执行 Platform.isWindows() 检查正确返回 true 并适当分配枚举。

    • 这包括 currentPlatform 变量的延迟实例化(删除 final 关键字),或忽略枚举并直接调用 JNA 的 Platform 类。
  • 将对 static 方法 getCurrentPlatform() 的第一次调用移出构造函数。

这些变通办法意味着一个可能的根本原因与在类初始化期间执行多个类的 static 方法有关。具体:

  • 在初始化期间,Platform.isWindows() 检查显然返回 false,因为代码到达了 else
  • 初始化后(在实例化期间),Platform.isWindows() 检查返回 true。 (由于它基于 static final 值,因此不应返回不同的结果。)

研究

我已经彻底查看了多个关于 Java 的教程,清楚地显示了初始化顺序,以及这些其他 SO 问题和链接的 Java 语言规范:

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)