在 Java 中将时间戳转换为即时会增加不必要的时间偏移



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

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 格式

我遇到的问题是 .toInstant() 将本地时间偏移量添加到时间戳对象

不,它没有。

  • 根据定义,java.sql.Timestamp采用 UTC
  • 根据定义,Instant采用 UTC

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

不要将代码堆积在一行中。将每个步骤分成单独的行,以便调试其值。

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 格式。避免这些麻烦的遗留类的众多原因之一。在您自己的计算机上运行以下代码示例,以查看默认时区对Timestamp的文本表示形式的影响。

让我们对在 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.

当前默认时区:太平洋/奥克兰

当前默认偏移量从 UTC:太平洋/奥克兰 | 总秒数: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(): 美国/蒙特利尔

zdt.toString(): 2017-06-09T00:41:10.750-04:00[美国/蒙特利尔]

避免使用传统的日期时间课程

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

符合 JDBC 4.2 的驱动程序可以通过调用PreparedStatement::setObjectResultSet::getObject直接寻址 java.time 类型。

myPreparedStatement.setObject( … , instant ) ;

。和。。。

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

如果使用尚未更新到 JDBC 4.2 和 java.time 的 JDBC 驱动程序,请使用添加到旧类的新方法短暂转换为java.sql.Timestampfrom ( Instant )、toInstant() 等。但是,除了与数据库交换数据之外,还要在java.time对象中完成所有实际工作(业务逻辑)。

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

。和。。。

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

关于java.time

java.time框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧传统日期时间类,如java.util.DateCalendarSimpleDateFormat

Joda-Time项目现在处于维护模式,建议迁移到 java.time 类。

要了解更多信息,请参阅Oracle 教程。并搜索堆栈溢出以获取许多示例和解释。规范为 JSR 310。

您可以直接与数据库交换java.time对象。使用符合 JDBC 4.2 或更高版本的 JDBC 驱动程序。不需要字符串,不需要java.sql.*类。

从哪里获得java.time类?

Java SE
  • 8Java SE 9及更高版本
    • 内置。
    • 具有捆绑实现的标准 Java API 的一部分。
    • Java 9添加了一些小功能和修复。
  • Java SE 6 和 Java SE 7 大部分
    • java.time 功能在ThreeTen-Backport中向后移植到 Java 6 和 7。
  • Android
    • 更高版本的 java.time 类的 Android 捆绑实现。
    • 对于早期的Android(<26),ThreeTenABP项目适应了ThreeTen-Backport(如上所述)。请参阅如何使用ThreeTenABP...

ThreeTen-Extra项目通过额外的类扩展了java.time。这个项目是未来可能添加到java.time的试验场。你可以在这里找到一些有用的类,如IntervalYearWeekYearQuarter等。

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

因此,长话短说,不要担心,您的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);

最新更新