Override Java System.currentTimeMillis 用于测试时间敏感型代码tl;博士 Clock在 java.time 中关于 java.time

2022-08-31 07:23:07

除了手动更改主机上的系统时钟之外,有没有办法在代码中使用代码或使用 JVM 参数来覆盖当前时间,如 via 所示?System.currentTimeMillis

一些背景知识:

我们有一个系统,运行许多会计工作,这些工作围绕当前日期(即每月1日,一年中的第1天等)旋转其大部分逻辑。

不幸的是,许多遗留代码调用诸如 或 之类的函数,这两个函数最终都调用了 。new Date()Calendar.getInstance()System.currentTimeMillis

出于测试目的,现在,我们只能手动更新系统时钟,以操纵代码认为正在运行的测试和日期。

所以我的问题是:

有没有办法覆盖 返回的内容?例如,告诉 JVM 在从该方法返回之前自动添加或减去一些偏移量?System.currentTimeMillis


答案 1

强烈建议您不要弄乱系统时钟,而是咬紧牙关,重构遗留代码以使用可替换时钟。理想情况下,这应该通过依赖注入来完成,但即使你使用可替换的单例,你也会获得可测试性。

这几乎可以通过搜索和替换单例版本来自动化:

  • 替换为 。Calendar.getInstance()Clock.getInstance().getCalendarInstance()
  • 替换为new Date()Clock.getInstance().newDate()
  • 替换为System.currentTimeMillis()Clock.getInstance().currentTimeMillis()

(根据需要等)

迈出第一步后,您可以一次用DI替换单例。


答案 2

tl;博士

除了手动更改主机上的系统时钟之外,有没有办法(无论是在代码中还是使用JVM参数)来覆盖当前时间,如System.currentTimeMillis所示?

是的。

Instant.now( 
    Clock.fixed( 
        Instant.parse( "2016-01-23T12:34:56Z"), ZoneOffset.UTC
    )
)

Clock在 java.time 中

对于可插拔时钟更换的问题,我们有了一个新的解决方案,以方便使用虚假的日期时间值进行测试。Java 8 中的 java.time 包包含一个抽象类 java.time.Clock,具有明确的用途:

允许在需要时插入备用时钟

您可以插入自己的 实现,尽管您可能可以找到一个已经满足您需求的实现。为了您的方便,java.time包含静态方法以产生特殊的实现。这些替代实现在测试期间可能很有价值。Clock

改变的节奏

各种方法产生的时钟以不同的节奏增加当前矩。tick…

缺省值报告更新时间在 Java 8 中为毫秒,在 Java 9 中更新为纳秒(具体取决于您的硬件)。您可以要求以不同的粒度报告真实的当前时刻。Clock

假时钟

某些时钟可能会丢失,从而产生与主机操作系统硬件时钟不同的结果。

  • 固定 - 将单个不变(非递增)力矩报告为当前矩。
  • 偏移量 - 报告当前时刻,但由传递的持续时间参数平移。

例如,锁定今年最早的圣诞节的第一刻。换句话说,当圣诞老人和他的驯鹿第一站时。现在最早的时区似乎是 太平洋/基里蒂马蒂 在 。+14:00

LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) );
LocalDate xmasThisYear = MonthDay.of( Month.DECEMBER , 25 ).atYear( ld.getYear() );
ZoneId earliestXmasZone = ZoneId.of( "Pacific/Kiritimati" ) ;
ZonedDateTime zdtEarliestXmasThisYear = xmasThisYear.atStartOfDay( earliestXmasZone );
Instant instantEarliestXmasThisYear = zdtEarliestXmasThisYear.toInstant();
Clock clockEarliestXmasThisYear = Clock.fixed( instantEarliestXmasThisYear , earliestXmasZone );

使用那个特殊的固定时钟来始终返回相同的时刻。我们在Kiritimati得到了圣诞节的第一个时刻,UTC显示的挂钟时间比12月24日前一天早10点早了14个小时。

Instant instant = Instant.now( clockEarliestXmasThisYear );
ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear );

瞬时至弦(): 2016-12-24T10:00:00Z

zdt.toString(): 2016-12-25T00:00+14:00[太平洋/基里蒂马蒂]

查看 IdeOne.com 中的实时代码

真实时间,不同时区

您可以控制实现分配的时区。这在某些测试中可能很有用。但我不建议在生产代码中使用此方法,因为在生产代码中,应始终显式指定可选参数或参数。ClockZoneIdZoneOffset

您可以将 UTC 指定为默认区域。

ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );

您可以指定任何特定的时区。以 的格式指定适当的时区名称,例如 美国/蒙特利尔非洲/卡萨布兰卡或 。切勿使用3-4个字母的缩写,例如或因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!)。continent/regionPacific/AucklandESTIST

ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );

您可以指定 JVM 的当前缺省时区应为特定对象的缺省时区。Clock

ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );

运行此代码进行比较。请注意,它们都报告相同的时刻,时间轴上的相同点。它们仅在挂钟时间上有所不同;换句话说,三种方式说同样的事情,三种方式来表达同一时刻。

System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );

America/Los_Angeles是运行此代码的计算机上的 JVM 当前默认区域。

zdtClockSystemUTC.toString(): 2016-12-31T20:52:39.688Z

zdtClockSystem.toString(): 2016-12-31T15:52:39.750-05:00[美国/蒙特利尔]

zdtClockSystemDefaultZone.toString(): 2016-12-31T12:52:39.762-08:00[美国/Los_Angeles]

根据定义,Instant 类始终采用 UTC 格式。因此,这三种与区域相关的用法具有完全相同的效果。Clock

Instant instantClockSystemUTC = Instant.now ( Clock.systemUTC () );
Instant instantClockSystem = Instant.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
Instant instantClockSystemDefaultZone = Instant.now ( Clock.systemDefaultZone () );

即时时钟系统UTC.toString(): 2016-12-31T20:52:39.763Z

即时时钟系统到字符串(): 2016-12-31T20:52:39.763Z

即时时钟系统默认Zone.toString(): 2016-12-31T20:52:39.763Z

默认时钟

默认情况下使用的实现是 Clock.systemUTC() 返回的实现。这是在未指定 .在 Instant.now 的预发布 Java 9 源代码中亲自查看。Instant.nowClock

public static Instant now() {
    return Clock.systemUTC().instant();
}

的缺省值为 。请参阅源代码ClockOffsetDateTime.nowZonedDateTime.nowClock.systemDefaultZone()

public static ZonedDateTime now() {
    return now(Clock.systemDefaultZone());
}

缺省实现的行为在 Java 8 和 Java 9 之间发生了变化。在Java 8中,尽管这些类能够存储纳秒的分辨率,但仅以毫秒为单位捕获当前时刻。Java 9 带来了一种新的实现,能够以纳秒的分辨率捕获当前时刻 - 当然,这取决于计算机硬件时钟的功能。


关于 java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧日期时间类,如java.util.DateCalendarSimpleDateFormat

要了解更多信息,请参阅 Oracle 教程。搜索 Stack Overflow 以获取许多示例和解释。规格是JSR 310

Joda-Time 项目现在处于维护模式,建议迁移到 java.time 类。

您可以直接与数据库交换 java.time 对象。使用符合 JDBC 4.2 或更高版本的 JDBC 驱动程序。不需要字符串,不需要类。Hibernate 5 & JPA 2.2 支持 java.timejava.sql.*

从哪里获取 java.time 类?