Java 中的即时时间戳转换增加了不必要的时间偏移

Timestamp conversion to instant in Java adds unnecessary time offset

我需要能够将从存储在 "datetime" 字段中的 MySQL 数据库中获取的数据转换为 Java ZonedDateTime 对象。

ZonedDateTime dt = ZonedDateTime.ofInstant(rs.getTimestamp("Start").toInstant(), UTC_ZONE_ID)

我遇到的问题是 toInstant() 将本地时间偏移添加到 Timestamp 对象,我不需要它,因为日期时间已经以 UTC 格式存储在数据库中。 所以当我运行下面的代码:

ZonedDateTime startDT = 
        ZonedDateTime.ofInstant(rs.getTimestamp("Start").toInstant(),Globals.LOCALZONEID);
System.out.println(rs.getTimestamp("start"));
System.out.println(rs.getTimestamp("start").toInstant());

我得到:

2017-06-08 13:15:00.0
2017-06-08T17:15:00Z

我需要时间组件保持不变。

我找不到任何明显的问题解决方案,所以我在这里遗漏了什么吗?

Timestamp & Instant 始终采用 UTC

The problem I'm having is that .toInstant() add local time offset to the Timestamp object

不,它没有。

两者都不能分配任何其他区域。

不要将代码集中在一行中。将每个步骤分成单独的行,以便您可以调试它们的值。

java.sql.Timestamp ts = rs.getTimestamp("Start") ;  // Actually in UTC, but it's `toString` method applies JVM’s current default time zone while generating string.
Instant instant = ts.toInstant() ;                  // Same moment, also in UTC.
ZoneId z = ZoneId.of( "America/Montreal" ) ;        // Or call your global var: `Globals.LOCALZONEID`.
ZonedDateTime zdt = instant.atZone( z );            // Same moment, same point on timeline, but with wall-clock time seen in a particular zone.

之后,您可能会看到问题(或非问题)。如果不是,请编辑您的问题以显示每个变量的调试值。

不信任Timestamp::toString

重要:java.sql.Timestamp::toString方法在于。该方法在生成字符串时应用 JVM 当前的默认时区。实际值始终采用 UTC。避免这些麻烦的遗留 classes 的众多原因之一。 运行 以下代码示例在您自己的机器上查看您的默认时区对 Timestamp.

的文本表示的影响

让我们 运行 模拟一下 code running live in IdeOne.com。 IdeOne.com 处的 JVM 默认为 UTC/GMT,因此我们通过将默认值任意指定为 Pacific/Auckland 来覆盖默认值。

Instant now = Instant.now() ;                       // Simulating fetching a `Timestamp` from database by using current moment in UTC.

TimeZone.setDefault( TimeZone.getTimeZone( "Pacific/Auckland" ) ) ;
ZoneId zoneIdDefault = ZoneId.systemDefault() ;
ZoneOffset zoneOffset = zoneIdDefault.getRules().getOffset( now ) ;

java.sql.Timestamp ts = java.sql.Timestamp.from( now ) ;  // Actually in UTC, but it's `toString` method applies JVM’s current default time zone while generating string.
Instant instant = ts.toInstant() ;                  // Same moment, also in UTC.
ZoneId z = ZoneId.of( "America/Montreal" ) ;        // Or call your global var: `Globals.LOCALZONEID`.
ZonedDateTime zdt = instant.atZone( z );            // Same moment, same point on timeline, but with wall-clock time seen in a particular zone.

Current default time zone: Pacific/Auckland

Current default offset-from-UTC: Pacific/Auckland | total seconds: 43200

now.toString(): 2017-06-09T04:41:10.750Z

ts.toString(): 2017-06-09 16:41:10.75

instant.toString(): 2017-06-09T04:41:10.750Z

z.toString(): America/Montreal

zdt.toString(): 2017-06-09T00:41:10.750-04:00[America/Montreal]

避免遗留日期时间 classes

在 java.time 包之外找到的旧日期时间 classes 很麻烦、令人困惑、设计糟糕且有缺陷。尽可能避免使用它们。这包括 java.sql.Timestamp.

你的 JDBC 4.2 compliant driver can directly address java.time types by calling PreparedStatement::setObject and ResultSet::getObject

myPreparedStatement.setObject( … , instant ) ;

……和……

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

如果使用尚未更新到 JDBC 4.2 和 java.time 的 JDBC 驱动程序,请使用添加到旧 [=150= 的新方法短暂转换为 java.sql.Timestamp ]:from ( Instant ), toInstant(),等等。但是除了与数据库交换数据之外,在 java.time 个对象中完成所有真正的工作(业务逻辑)。

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

……和……

Instant instant = myResultSet.getTimestamp( … ).toInstant() ;

关于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 classes.

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

您可以直接与数据库交换 java.time 对象。使用 JDBC driver compliant with JDBC 4.2 或更高版本。不需要字符串,不需要 java.sql.* classes.

从哪里获得java.time classes?

  • Java SE 8, Java SE 9,及以后
    • 内置。
    • 标准 Java API 的一部分,带有捆绑实施。
    • Java 9 添加了一些小功能和修复。
  • Java SE 6 and Java SE 7
  • Android
    • Android java.time classes.
    • 捆绑实施的更高版本
    • 对于较早的 Android (<26),ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See

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.

java.sql.Timestamp这样的旧类在设计上有很多问题。经常引起混淆的一件事是 Timestamp.toString() 打印 JVM 时区的时间,即使其中的时间 Timestamp 只是保存一个没有时区的时间点。具体来说,当 Timestamp 等于 UTC 中的 2017-06-08T17:15:00Z 并且您在计算机上打印它时 运行 匹兹堡时间(每年的这个时候有一个偏移量-4:00 from UTC),Timestamp.toString() 被隐式调用,它读取 JVM 的时区并将时间打印为 13:15:00.0 只是因为宾夕法尼亚州的这个时间等于 [=] 中的 UTC 时间12=].

长话短说,别担心,你的 TimestampInstant 都是正确的。

我认为这段代码片断可以回答您的问题。这会在本地时区接收一个字符串,将其转换为 UTC,并将其存储在数据库中。

    //Getting the LocalDateTime Objects from String values
    DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd kk:mm"); 
    String txtStartTime = "2017-03-29 12:00";

    LocalDateTime ldtStart = LocalDateTime.parse(txtStartTime, df);


    //Convert to a ZonedDate Time in UTC
    ZoneId zid = ZoneId.systemDefault();

    ZonedDateTime zdtStart = ldtStart.atZone(zid);
    System.out.println("Local Time: " + zdtStart);
    ZonedDateTime utcStart = zdtStart.withZoneSameInstant(ZoneId.of("UTC"));
    System.out.println("Zoned time: " + utcStart);
    ldtStart = utcStart.toLocalDateTime();
    System.out.println("Zoned time with zone stripped:" + ldtStart);
    //Create Timestamp values from Instants to update database
    Timestamp startsqlts = Timestamp.valueOf(ldtStart); //this value can be inserted into database
    System.out.println("Timestamp to be inserted: " +startsqlts);

    //insertDB(startsqlts);