如何在我的 Java 应用程序中设置 JVM 时间(不是 OS 时间)?

How can I set JVM time(not OS time) in my Java application?

我正在设计一个回放测试应用程序,它将重放以前记录的数据并处理数据。我想在我的应用程序中配置日期和时间值,这不会影响操作系统。我发现有一个用于设置 JVM 时区的 JVM 参数,但我找不到类似的日期和时间参数。 可以吗?

此致

不是。没有办法使例如System.currentTimeMillis()(因此,Instant.now() 和其他类似调用)在不改变 OS 时钟的情况下改变行为。

如果你想要 'testable' 时间,你不能调用那些方法。相反,制作一个 Clock 对象并使用它。

因此,这就是你必须做的:

  1. 查找代码中所有硬编码为使用 OS 时钟的地方。 System.currentTimeMillis()new Date()X.now(),其中 X 是 java.time 包中的任何类型(InstantLocalDateTime 等),关键是任何库那是在幕后做的。将它们替换为适当的基于时钟的调用。 System.currentTimeMillis() 变为 clock.millis()Instant.now() 变为 clock.instant(),依此类推。

  2. 在测试期间,制作您自己的时钟,或使用 Clock.fixed 始终报告完全相同时间的时钟。在生产过程中,时钟必须是 Clock.systemUTC().

这可能需要使用依赖注入框架。或者,为您的整个应用程序制作一个全局时钟(一个 class 带有一个所有代码都从中读取的静态时钟字段,并且您的测试代码集,默认为 systemUTC)。

据我所知,无法配置 System.currentTimeMillis 和朋友使用的时钟。

为特定进程更改 date/time 的常用方法是使用拦截这两个函数的 LD_PRELOAD trick. There are two basic functions in Linux to get absolute time: gettimeofday and clock_gettime. Here is an example library 挂钩相应的系统函数。

不幸的是,上述技巧仅适用于 Linux。但这里有另一个专门针对 Java 的便携式解决方案。它依赖于 JVM Tool Interface.

在 OpenJDK 中,所有用于获取当前 date/time 的 Java API 最终都会调用 System.currentTimeMillis()VM.getNanoTimeAdjustment()。后者是提供高分辨率Clock 的内部JDK 方法。因此,为了更改 Java 应用程序的 date/time,调整这两个方法的实现就足够了。

思路如下。

  • JVM TI 代理通过订阅 NativeMethodBind 事件拦截本机方法绑定。
  • NativeMethodBindcurrentTimeMillisgetNanoTimeAdjustment 调用时,代理会记住本机函数的地址并将其替换为自己的地址。
  • 每次调用钩子方法时,代理首先委托给原始函数,然后将指定的偏移量添加到返回值。

此类代理的完整代码为here

如何编译:

g++ -O2 -fPIC -shared -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -olibfaketime.so faketime.cpp

如何运行:

java -agentpath:/path/to/libfaketime.so=<newtime> MainClass

其中 newtime 是距 Epoch 的绝对时间戳(以毫秒为单位),或者(当以 +- 开头时)距当前时间的相对偏移量(以毫秒为单位)。

唯一的问题是 System.currentTimeMillis 是一个 JVM 内部函数。这意味着,JIT 编译的方法可能会跳过调用 JNI 实现(从而跳过我们的钩子)。为避免这种情况,我们可以简单地使用更多 JVM 选项禁用相应的内部函数:

-XX:+UnlockDiagnosticVMOptions -XX:DisableIntrinsic=_currentTimeMillis -XX:CompileCommand=dontinline,java.lang.System::currentTimeMillis