1. 概述
本文将深入探讨 Java 17 引入的 InstantSource 接口。该接口提供了当前时刻的可插拔表示,有效避免了时区引用问题,是处理时间戳的优雅解决方案。
2. InstantSource 接口解析
- 为 java.time.Clock 提供的时区创建抽象层
- 简化测试中获取时间戳代码的桩实现
它被引入 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 获取。