1. 概述

本文将深入探讨 Java 17 引入的 InstantSource 接口。该接口提供了当前时刻的可插拔表示,有效避免了时区引用问题,是处理时间戳的优雅解决方案。

2. InstantSource 接口解析

根据原始提案相关问题,该接口的核心目标有两个:

  1. java.time.Clock 提供的时区创建抽象层
  2. 简化测试中获取时间戳代码的桩实现

它被引入 Java 17 用于提供安全获取当前时刻的方式,示例如下:

class AQuickTest {
    InstantSource source;
    ...
    Instant getInstant() {
        return source.instant();
    }
}

使用方式很简单:

var quickTest = new AQuickTest(InstantSource.system());
quickTest.getInstant();

其实现类可在任意位置创建对象来获取时间戳,并为测试场景提供了高效的桩实现机制。下面我们深入分析该接口的核心优势。

3. 问题与解决方案

为更好理解 InstantSource,我们剖析它要解决的关键问题及对应的解决方案。

3.1. 测试难题

测试涉及获取 Instant 的代码通常是噩梦,尤其当基于当前时间方案(如 *LocalDateTime.now()*)获取时间戳时。

为在测试中提供特定时间点,我们常创建临时方案,如外部时间工厂并在测试中注入桩实例。以下是常见变通方案示例:

InstantExample 类通过 InstantWrapper(变通方案)获取时间戳:

class InstantExample {
    InstantWrapper instantWrapper;
    Instant getCurrentInstantFromInstantWrapper() {
        return instantWrapper.instant();
    }
}

InstantWrapper 变通方案类本身:

class InstantWrapper {
    Clock clock;
    InstantWrapper() {
        this.clock = Clock.systemDefaultZone();
    }
    InstantWrapper(ZonedDateTime zonedDateTime) {
        this.clock = Clock.fixed(zonedDateTime.toInstant(), zonedDateTime.getZone());
    }
    Instant instant() {
        return clock.instant();
    }
}

测试时提供固定时间戳:

// given
LocalDateTime now = LocalDateTime.now();
InstantExample tested = new InstantExample(InstantWrapper.of(now), null);
Instant currentInstant = now.toInstant(ZoneOffset.UTC);
// when
Instant returnedInstant = tested.getCurrentInstantFromWrapper();
// then
assertEquals(currentInstant, returnedInstant);

3.2. 测试难题的解决方案

本质上,InstantSource 就是上述变通方案的标准化实现。它提供外部时间戳工厂,可在需要时任意使用。Java 17 提供了默认系统级实现(位于 Clock 类),我们也可自定义:

class InstantExample {
    InstantSource instantSource;
    Instant getCurrentInstantFromInstantSource() {
        return instantSource.instant();
    }
}

InstantSource 是可插拔的,可通过依赖注入框架或构造函数参数注入被测对象。这样就能轻松创建桩实现,返回测试所需的时间戳:

// given
LocalDateTime now = LocalDateTime.now();
InstantSource instantSource = InstantSource.fixed(now.toInstant(ZoneOffset.UTC));
InstantExample tested = new InstantExample(null, instantSource);
Instant currentInstant = instantSource.instant();
// when
Instant returnedInstant = tested.getCurrentInstantFromInstantSource();
// then
assertEquals(currentInstant, returnedInstant);

3.3. 时区陷阱

需要获取 Instant 时,我们有多种来源Instant.now()Clock.systemDefaultZone().instant() 或 *LocalDateTime.now.toInstant(zoneOffset)*。问题在于,不同选择可能引入时区问题

例如,通过 Clock 获取时间戳:

Clock.systemDefaultZone().instant();

输出:

2022-01-05T06:47:15.001890204Z

换种方式获取:

LocalDateTime.now().toInstant(ZoneOffset.UTC);

输出:

2022-01-05T07:47:15.001890204Z

理论上应相同,实际却相差 60 分钟。更糟的是,团队不同成员可能在代码不同部分混用这两种方式,导致潜在问题。

此时我们通常不希望处理时区,但创建时间戳需要数据源,而该源必然关联时区。

3.4. 时区问题的解决方案

InstantSource 抽象了时间戳源的选择。这个选择已由框架或团队预定义——可能是其他程序员设置了系统级自定义实现,也可能是使用 Java 17 提供的默认实现(下文详述)。

InstantExample 所示,只需注入 InstantSource,无需关心实现细节。可以移除 InstantWrapper 变通方案,直接使用注入的 InstantSource

接下来通过静态方法和实例方法,进一步探索该接口的能力。

4. 工厂方法

以下工厂方法可创建 InstantSource 对象:

  • system() – 默认系统级实现
  • tick(InstantSource, Duration) – 返回按指定时长截断到最近时间点InstantSource
  • fixed(Instant) – 返回始终返回相同时间戳InstantSource
  • offset(InstantSource, Duration) – 返回提供带偏移量时间戳InstantSource

4.1. system()

Java 17 的默认实现是 Clock.SystemInstantSource 类:

Instant i = InstantSource.system().instant();

4.2. tick()

基于前例:

Instant i = InstantSource.system().instant();
System.out.println(i);

输出:

2022-01-05T07:44:44.861040341Z

应用 2 小时截断:

Instant i = InstantSource.tick(InstantSource.system(), Duration.ofHours(2)).instant();

输出:

2022-01-05T06:00:00Z

4.3. fixed()

测试场景中创建桩实现时特别实用:

LocalDateTime fixed = LocalDateTime.of(2022, 1, 1, 0, 0);
Instant i = InstantSource.fixed(fixed.toInstant(ZoneOffset.UTC)).instant();
System.out.println(i);

始终返回固定值:

2022-01-01T00:00:00Z

4.4. offset()

对固定时间源应用偏移:

LocalDateTime fixed = LocalDateTime.of(2022, 1, 1, 0, 0);
InstantSource fixedSource = InstantSource.fixed(fixed.toInstant(ZoneOffset.UTC));
Instant i = InstantSource.offset(fixedSource, Duration.ofDays(5)).instant();
System.out.println(i);

输出:

2022-01-06T00:00:00Z

5. 实例方法

InstantSource 实例提供以下方法:

  • instant() – 返回当前时间戳
  • millis() – 返回当前时间戳的毫秒数
  • withZone(ZoneId) – 接收 ZoneId,返回基于指定时区的时钟对象

5.1. instant()

基础用法:

Instant i = InstantSource.system().instant();
System.out.println(i);

输出:

2022-01-05T08:29:17.641839778Z

5.2. millis()

获取纪元毫秒数:

long m = InstantSource.system().millis();
System.out.println(m);

输出:

1641371476655

5.3. withZone()

为特定时区创建 Clock 实例:

Clock c = InstantSource.system().withZone(ZoneId.of("-4"));
System.out.println(c);

输出:

SystemClock[-04:00]

6. 总结

本文深入分析了 InstantSource 接口,列举了它解决的核心问题,并通过实际案例展示了其在日常工作中的应用价值。该接口通过抽象时间戳源和时区处理,显著提升了时间相关代码的可测试性和健壮性。

完整代码示例可在 GitHub 获取。


原始标题:An Introduction to InstantSource in Java 17