java.sql.Timestamp 由 java.time.Instant 的 MIN/MAX 构建的行为不同于由 Long.MIN_VALUE / Long.MAX_VALUE 构建的行为

java.sql.Timestamp made from java.time.Instant's MIN/MAX behaves differently than when constructed from Long.MIN_VALUE / Long.MAX_VALUE

我在编写测试用例时遇到了这个问题,我必须在一系列时间戳之间获取一系列记录——使用 H2 嵌入式数据库和 spring-data-jpa

原刊位于:

我有 java.time.Instant 个实例的时间戳。

如果用户没有提供起止时间戳,我继续并分别插入Instant.MINInstant.MAX

令我困惑的是以下测试用例通过了:

  @Test
  public void test_date_min_max_instants_timestamps() {
    Timestamp past = new Timestamp(Long.MIN_VALUE); 
    Timestamp future = new Timestamp(Long.MAX_VALUE); 
    Timestamp present = Timestamp.from(Instant.now()); 

    assertTrue(present.after(past));
    assertTrue(future.after(past));
    assertTrue(future.after(present));

    assertTrue(present.before(future));
    assertTrue(past.before(present));
    assertTrue(past.before(future));
  }

但是,以下测试用例失败了:

 @Test
  public void test_instant_ranges() throws InterruptedException {
    Timestamp past = Timestamp.from(Instant.MIN);
    Timestamp future = Timestamp.from(Instant.MAX);
    Timestamp present = Timestamp.from(Instant.now());

    assertTrue(present.after(past));
    assertTrue(future.after(past));
    assertTrue(future.after(present));

    assertTrue(present.before(future));
    assertTrue(past.before(present));
    assertTrue(past.before(future));
  }

此外,如果pastfuture不是MIN/MAX值,而是正常值,则结果符合预期。

知道为什么 java.sql.Timestamp 会这样吗?

此外,如果 Instant 表示的时间对于 Timestamp 来说太大了,它不应该失败吗?

P.S。如果这个问题已经被问过,有人可以 link 原来的,因为我找不到它。

编辑:添加了我在评论部分提到的调试信息,以便我们将所有内容集中在一个地方。

对于由 Instant.MINInstant.MAX 生成的 Timestamp 个实例,我有以下值:

past = 169108098-07-03 21:51:43.0 
future = 169104627-12-11 11:08:15.999999999 
present = 2018-07-23 10:46:50.842 

并且对于由 Long.MIN_VALUELong.MAX_VALUE 生成的 Timestamp 个实例,我得到:

past = 292278994-08-16 23:12:55.192 
future = 292278994-08-16 23:12:55.807 
present = 2018-07-23 10:49:54.281

为了澄清我的问题,时间戳应该显式失败,而不是默默地失败或在内部使用不同的值。目前没有。

这是 Timestamp class 及其从 Instant 转换而来的已知错误。它于三年半前的 2015 年 1 月在 Java 错误数据库中注册(并且仍然开放,没有确定的修复版本)。请参阅底部的 link 到官方错误报告。

预期的行为很明确

Timestamp.from(Instant) 的文档对此非常清楚:

Instant can store points on the time-line further in the future and further in the past than Date. In this scenario, this method will throw an exception.

所以是的,应该抛出异常。

重现bug很简单

在我的 Java 10 上,我重现了几个示例,在这些示例中,转换默默地给出了不正确的结果,而不是抛出异常。一个例子是:

        Instant i = LocalDate.of(-400_000_000, Month.JUNE, 14)
                .atStartOfDay(ZoneId.of("Africa/Cairo"))
                .toInstant();
        Timestamp ts = Timestamp.from(i);
        System.out.println("" + i + " -> " + ts + " -> " + ts.toInstant());

这会打印:

-400000000-06-13T21:54:51Z -> 184554049-09-14 14:20:42.0 -> +184554049-09-14T12:20:42Z

前一个转换显然是错误的:很久以前的时间被转换成遥远(虽然不是很远)的未来(转换回 Instant 似乎是正确的).

附录:JDK源代码

好奇这里是转换方法的实现:

public static Timestamp from(Instant instant) {
    try {
        Timestamp stamp = new Timestamp(instant.getEpochSecond() * MILLIS_PER_SECOND);
        stamp.nanos = instant.getNano();
        return stamp;
    } catch (ArithmeticException ex) {
        throw new IllegalArgumentException(ex);
    }
}

似乎作者预料到乘法运算溢出会导致ArithmeticException。它没有。

链接