解析时间戳时时区是否重要?

Does the timezone matter when parsing a Timestamp?

我有一个编码为 String 的时间戳——例如,"2012-02-12T09:08:13.123456-0400",来自 Oracle 数据库。

我能想到的读取此时间戳的唯一方法是使用 Timestamp.valueOf(),这需要 yyyy-[m]m-[d]d hh:mm:ss[.f...]

的格式

我确信这是在不损失精度的情况下读取时间的唯一方法,因为其他方法不支持上述示例中包含的纳秒级精度 (".123456")。

考虑到这一点,我可以简单地 trim 所需的值,以适应所需的格式。因此,原始字符串将被转换:

如果我这样做,我会删除 "-0400" 时区偏移量。这对我来说是一个危险信号,直到我看到这个 post。建议的答案之一指出,

I think the correct answer should be java.sql.Timestamp is NOT timezone specific. Timestamp is a composite of java.util.Date and a separate nanoseconds value. There is no timezone information in this class. Thus just as Date this class simply holds the number of milliseconds since January 1, 1970, 00:00:00 GMT + nanos.

为了向自己证明不需要偏移量,我写了一个简单的集成测试。

将此时间戳插入数据库:"2015-09-08 11:11:12.123457"。使用 Java 读取数据库,并打印出详细信息。我得到 "2015-09-08 11:11:12.123457",这是相同的值。这恰好没问题,因为我的 JVM 和 Oracle 数据库 运行 在同一台机器上。

  1. 时区不是 java.sql.Timestamp 的因素是事实吗?
  2. 有没有更好的方法来读取整个时间戳,而不会在 Java 7 中丢失任何精度?

你说得对 java.sql.Timestamp 没有时区信息。在幕后,这只是自纪元以来几毫秒的瞬间,而纪元本身基本上是用 0 偏移量定义的,1970 年 1 月 1 日,00:00:00 GMT。

话虽这么说,当处理时间时,偏移量总是很重要,这会把你带入地狱,呃,"dynamic world" 那是 Java 8 java.time 中的 ISO-8601 .偏移量很重要,除非你站在英国格林威治。如果您试图忽略它,您有时可能会猜错日期;例如,格林威治的星期一比西雅图的星期一早 8 小时。我认为 Java8 没有针对 ISO-8601 的 -0000 变体的内置 DateTimeFormatter,但如果您确定您的格式是 "stable",这将起作用。

public void java8TimeWTF() {
  OffsetDateTime odt = OffsetDateTime.parse(
    "2012-02-12T09:08:13.123456-0400",
    DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.nZ"));
  Instant i = odt.toInstant();
  System.out.printf("odt: %s, i: %s\n", odt, i);
}

输出odt: 2012-02-12T09:08:13.000123456-04:00, i: 2012-02-12T13:08:13.000123456Z

这是我们处理来自客户的所有 ISO-8601 变体的方法

import org.testng.annotations.Test;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import static org.testng.Assert.assertEquals;

public class Java8Time8601 {
  private final long EXPECTED_MILLIS = 1493397412000L;

  public Instant iso8601ToInstant(String s) {
    DateTimeFormatter[] dateTimeFormatters = {
            DateTimeFormatter.ISO_INSTANT,
            DateTimeFormatter.ISO_OFFSET_DATE_TIME,
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ")
    };
    for (DateTimeFormatter dtf : dateTimeFormatters) {
      try {
        OffsetDateTime odt = OffsetDateTime.parse(s, dtf);
        Instant i = odt.toInstant();
        return i;
      } catch (DateTimeParseException dtpe) {
        ;
      }
    }
    throw new IllegalArgumentException(String.format("failed to parse %s", s));
  }

  @Test
  public void testInstantParse8601_Z() throws Exception {
    String[] candidates = {
            "2017-04-28T16:36:52.000Z",
            "2017-04-28T16:36:52.00Z",
            "2017-04-28T16:36:52.0Z",
            "2017-04-28T16:36:52Z",
            "2017-04-28T16:36:52+00:00",
            "2017-04-28T16:36:52-00:00",
            "2017-04-28T09:36:52-07:00",
            "2017-04-28T09:36:52-0700",
    };
    for (String candidate : candidates) {
      Instant i = iso8601ToInstant(candidate);
      assertEquals(i.toEpochMilli(), EXPECTED_MILLIS, String.format("failed candidate %s", candidate));
      System.out.println(i.toString());
    }
  }
}

必须有更好的方法。

java.sql.Timestamp.valueOf(String) 解析当前时区中提供的时间。您可以通过查看实现来检查这一点,它最终简单地调用 Timestamp(int year, int month, int date, int hour, int minute, int second, int nano) which calls public Date(int year, int month, int date, int hrs, int min, int sec),它说(强调我的):

Allocates a Date object and initializes it so that it represents the instant at the start of the second specified by the year, month, date, hrs, min, and sec arguments, in the local time zone.

确实 Timestamp 没有时区信息(它只是一个包装器,包含自 GMT 纪元以来的秒数 + 纳秒),但是在加载或存储时 Timestamp , JDBC 将使用本地(默认)时区,除非另有明确声明。

这意味着在您本地时区 10:00 的时间戳将在数据库中作为 10:00 的 SQL 时间戳结束,而不是 - 例如 - 08:00 如果您的时区比格林威治标准时间早 2 小时;加载时相反。

tl;博士

org.threeten.bp.OffsetDateTime odt = 
    OffsetDateTime.parse(
        "2012-02-12T09:08:13.123456-0400",
        org.threeten.bp.format.DateTimeFormatter.ofPattern( "yyyy-MM-dd'T'HH:mm:ssZ" )  // Specify pattern as workaround for Java 8 bug in failing to parse if optional colon is not present.
    )
;

使用java.time

您应该检索一个对象,一个日期时间对象,特别是 java.time 对象,而不是从您的数据库中接收一个字符串。

java.time 类 取代了麻烦的旧日期时间 类,包括 java.sql.Timestamp。如果您的 JDBC driver 支持 JDBC 4.2 及更高版本,您可以直接传递和接收 java.time 对象。

Instant

Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds(最多九 (9) 位小数)。因此,这相当于 java.sql.Timestamp 包括对输入数据的六位微秒的支持,因此根据您的问题的要求不会丢失精度。

Instant instant = myResultSet.getObject( … , Instant.class ) ;

instant.toString(): 2012-02-12T13:08:13.123456Z

ZonedDateTime

如果您想通过特定地区挂钟时间的镜头看到同一时刻,请应用 ZoneId 以获得 ZonedDateTime 对象。

ZoneId z = ZoneId.of( "America/St_Thomas" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;

zdt.toString(): 2012-02-12T09:08:13.123456-04:00[America/St_Thomas]

OffsetDateTime

关于如何将字符串 2012-02-12T09:08:13.123456-0400 理解为日期时间值的直接问题,请解析为 OffsetDateTime

时区的名称格式为 continent/region,表示过去、现在和未来由夏令时 (DST) 等异常引起的区域偏移量变化的历史记录。我们知道您的字符串所在的时区,因此我们使用 OffsetDateTime 而不是 ZonedDateTime.

OffsetDateTime odt = OffsetDateTime.parse( "2012-02-12T09:08:13.123456-0400" ) ;

好吧,上面那行代码 应该 有效,但是在 Java 8 中,在解析缺少可选的 COLON 字符的偏移量时存在一个小错误小时和分钟。所以 Java 8 中的 -04:00 将解析而不是 -0400。 Java 中修复的错误 9. 您的字符串确实符合 java.time 类 中默认使用的日期时间格式的 ISO 8601 标准。提示:通常最好始终使用冒号、hours/minutes 和填充零来格式化偏移量——我看到其他协议和库只需要这样的完整格式。

在您移动到 ​​Java 9 之前,明确指定格式化模式而不是依赖隐式默认模式,作为此错误的解决方法。

OffsetDateTime odt = 
    OffsetDateTime.parse(
        "2012-02-12T09:08:13.123456-0400",
        DateTimeFormatter.ofPattern( "yyyy-MM-dd'T'HH:mm:ssZ" )  // Specify pattern as workaround for Java 8 bug in failing to parse if optional colon is not present.
    )
;

正在转换

如果您的 JDBC 驱动程序还不符合 JDBC 4.2,请检索一个 java.sql.Timestamp 对象,仅供 短暂使用 。使用添加到旧日期时间 类.

的新方法立即转换为 java.time
java.sql.Timestamp ts = myResultSet.getTimestamp( … ) ;
Instant instant = ts.toInstant();

继续在 java.time 类 中执行您的业务逻辑。要将日期时间发送回数据库,请将 Instant 转换为 java.sql.Timestamp

myPreparedStatement.setTimestamp( … , java.sql.Timestamp.from( instant ) ) ;

Java 6 & 7

在Java6 & 7中,上述概念仍然适用,但java.time不是内置的。请改用 ThreeTen-Backport 库。要获取,请参阅下面的项目符号。

在Java 7 中,您不能使用JDBC 4.2 功能。所以我们不能通过JDBC驱动直接从数据库中访问java.time对象。如上所示,我们必须将 Instant 简单地转换为 java.sql.Timestamp。调用实用程序方法 DateTimeUtils.toInstant(Timestamp sqlTimestamp) & DateTimeUtils.toSqlTimestamp(Instant instant)

java.sql.Timestamp ts = myResultSet.getTimestamp( … ) ;
Instant instant = DateTimeUtils.toInstant( ts ) ;

……和……

java.sql.Timestamp ts = DateTimeUtils.toSqlTimestamp( instant ) ;
myPreparedStatement.setTimestamp( … , ts ) ;

关于java.time

java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

Joda-Time project, now in maintenance mode, advises migration to the java.time 类.

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

在哪里获取java.time类?

ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.