将 java.time.Instant 转换为没有区域偏移的 java.sql.Timestamp

Posted

技术标签:

【中文标题】将 java.time.Instant 转换为没有区域偏移的 java.sql.Timestamp【英文标题】:Convert java.time.Instant to java.sql.Timestamp without Zone offset 【发布时间】:2017-08-03 15:34:05 【问题描述】:

在我正在开发的应用程序中,我需要将java.time.Instant 对象转换为java.sql.Timestamp。当我创建 Instant 对象时:

Instant now = Instant.now();

我收到类似2017-03-13T14:28:59.970Z 的信息。当我尝试像这样创建Timestamp 对象时:

Timestamp current = Timestamp.from(now);

我收到类似2017-03-13T16:28:59.970Z 的信息。结果相同,但额外延迟了 2 小时。 有人可以解释为什么会发生这种情况,并立即为我提供解决此问题的答案吗?

当我这样创建时:

LocalDateTime ldt = LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC);
Timestamp current = Timestamp.valueOf(ldt);

一切正常,但我尽量避免转换。有没有办法只使用Instant 对象来做到这一点?

【问题讨论】:

你确定他们两个都真的显示Z吗?你能提供一个minimal reproducible example 在没有调试器的情况下演示吗? 目前我无法为您提供,但我几乎可以肯定数据库中的记录将应用区域偏移量。第二个例子经过测试,记录符合我的预期。 @JonSkeet 。对不起我错了,Timestamp current = Timestamp.from(now); 中没有出现Z; 是的 - 现在您的本地时区是否比 UTC 早两个小时?如果是这样,这两个值代表相同的时间点。 InstantTimestamp 都不持有任何时区偏移。 Timestamp.toString() 选择您计算机的默认时区并将其用于显示,这让您感到困惑。因此,即使 rhey 在 UTC 中保持相同的毫秒数,它们的显示也不相同。 【参考方案1】:

我将计算机的时区更改为欧洲/布加勒斯特进行实验。这是 UTC + 2 小时,就像您的时区一样。

现在,当我复制您的代码时,我得到的结果与您的相似:

    Instant now = Instant.now();
    System.out.println(now); // prints 2017-03-14T06:16:32.621Z
    Timestamp current = Timestamp.from(now);
    System.out.println(current); // 2017-03-14 08:16:32.621

输出以 cmets 为单位。但是,我继续:

    DateFormat df = DateFormat.getDateTimeInstance();
    df.setTimeZone(TimeZone.getTimeZone("UTC"));
    // the following prints: Timestamp in UTC: 14-03-2017 06:16:32
    System.out.println("Timestamp in UTC: " + df.format(current));

现在您可以看到Timestamp 确实与我们开始时的Instant 一致(只有毫秒没有打印,但我相信它们也在其中)。所以你已经正确地完成了所有事情,只是感到困惑,因为当我们打印 Timestamp 时,我们隐式调用了它的 toString 方法,而这个方法又会获取计算机的时区设置并显示该时区的时间。也正因为如此,才表现的不一样。

您尝试的另一件事,使用 LocalDateTime,似乎有效,但实际上并不能满足您的需求:

    LocalDateTime ldt = LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC);
    System.out.println(ldt); // 2017-03-14T06:16:32.819
    current = Timestamp.valueOf(ldt);
    System.out.println(current); // 2017-03-14 06:16:32.819
    System.out.println("Timestamp in UTC: " + df.format(current)); // 14-03-2017 04:16:32

现在,当我们使用我们的 UTC DateFormat 打印 Timestamp 时,我们可以看到太早了 2 小时,04:16:32 UTC,而 Instant 是 06:16:32 UTC。所以这个方法是骗人的,看起来是行得通,其实行不通。

这显示了导致设计 Java 8 日期和时间类以替换旧的类的麻烦。因此,真正解决您的问题的好方法可能是让自己获得一个可以轻松接受Instant 对象的JDBC 4.2 驱动程序,这样您就可以完全避免转换为Timestamp。我不知道你现在是否可以使用,但我相信它会的。

【讨论】:

详细信息:符合 JDBC 4.2 的驱动程序可能使用Instant,但必须使用OffsetDateTime。转换:OffsetDateTime odt = myInstant.atOffset( ZoneOffset.UTC ) ;【参考方案2】:

使用错误的类

LocalDateTime ldt = LocalDateTime.ofInstant(Instnant.now(), ZoneOffset.UTC);
Timestamp current = Timestamp.valueOf(ldt);

该代码有两个问题。

首先,切勿将现代 java.time 类(此处为LocalDateTime)与可怕的旧旧日期时间类(此处为java.sql.Timestamp)混用。从采用JSR 310 开始,java.time 框架完全取代了糟糕的旧类。您再也不需要使用Timestamp:从JDBC 4.2 开始,我们可以直接与数据库交换java.time 对象。

另一个问题是LocalDateTime 类,根据定义,不能代表片刻。它故意缺少时区或与 UTC 的偏移量。 LocalDateTime 仅当您的意思是日期与时间everywhereanywhere,换句话说,任何/所有更多的时刻在大约的范围内26-27 小时(全球当前的极端时区)。

不要使用LocalDateTime 表示特定时刻,时间轴上的特定点。而是使用:

Instant(始终为 UTC) OffsetDateTime(带有时间和从 UTC 偏移的日期) ZonedDateTime(带有时间和时区的日期)。

然后我尝试创建 Timestamp 对象

不要。

永远不要使用java.sql.Timestamp。替换为java.time.Instant。继续阅读以了解更多信息。

当前时刻

要捕捉 UTC 中的当前时刻,请使用以下任一方法:

Instant instant = Instant.now() ;

……或者……

OffsetDateTime odt = OffsetDateTime.now( ZoneOffset.UTC );

两者都代表相同的事物,即 UTC 中的一个时刻。

数据库

这里有一些示例 SQL 和用于将当前时刻传递到数据库的 Java 代码。

该示例使用 Java 内置的 H2 Database Engine。

sql = "INSERT INTO event_ ( name_ , when_ ) " + "VALUES ( ? , ? ) ;";
try ( PreparedStatement preparedStatement = conn.prepareStatement( sql ) ; ) 
    String name = "whatever";
    OffsetDateTime odt = OffsetDateTime.now( ZoneOffset.UTC );

    preparedStatement.setString( 1 , name );
    preparedStatement.setObject( 2 , odt );
    preparedStatement.executeUpdate();

这是一个使用该代码的完整示例应用程序。

package com.basilbourque.example;

import java.sql.*;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.UUID;

public class MomentIntoDatabase 

    public static void main ( String[] args ) 
        MomentIntoDatabase app = new MomentIntoDatabase();
        app.doIt();
    

    private void doIt ( ) 
        try 
            Class.forName( "org.h2.Driver" );
         catch ( ClassNotFoundException e ) 
            e.printStackTrace();
        

        try (
                Connection conn = DriverManager.getConnection( "jdbc:h2:mem:moment_into_db_example_" ) ;
                Statement stmt = conn.createStatement() ;
        ) 
            String sql = "CREATE TABLE event_ (\n" +
                    "  id_ UUID DEFAULT random_uuid() PRIMARY KEY ,\n" +
                    "  name_ VARCHAR NOT NULL ,\n" +
                    "  when_ TIMESTAMP WITH TIME ZONE NOT NULL\n" +
                    ") ; ";
            System.out.println( sql );
            stmt.execute( sql );

            // Insert row.
            sql = "INSERT INTO event_ ( name_ , when_ ) " + "VALUES ( ? , ? ) ;";
            try ( PreparedStatement preparedStatement = conn.prepareStatement( sql ) ; ) 
                String name = "whatever";
                OffsetDateTime odt = OffsetDateTime.now( ZoneOffset.UTC );

                preparedStatement.setString( 1 , name );
                preparedStatement.setObject( 2 , odt );
                preparedStatement.executeUpdate();
            

            // Query all.
            sql = "SELECT * FROM event_ ;";
            try ( ResultSet rs = stmt.executeQuery( sql ) ; ) 
                while ( rs.next() ) 
                    //Retrieve by column name
                    UUID id = ( UUID ) rs.getObject( "id_" );  // Cast the `Object` object to UUID if your driver does not support JDBC 4.2 and its ability to pass the expected return type for type-safety.
                    String name = rs.getString( "name_" );
                    OffsetDateTime odt = rs.getObject( "when_" , OffsetDateTime.class );

                    //Display values
                    System.out.println( "id: " + id + " | name: " + name + " | when: " + odt );
                
            
         catch ( SQLException e ) 
            e.printStackTrace();
        
    

解析字符串

关于 Melnyk 的相关评论,这里是基于上面示例代码的另一个示例。此代码不是捕获当前时刻,而是解析一个字符串。

输入字符串缺少time zone 或offset-from-UTC 的任何指示符。所以我们将其解析为LocalDateTime,请记住,这代表一个时刻,不是时间轴上的一个点。

String input = "22.11.2018 00:00:00";
DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd.MM.uuuu HH:mm:ss" );
LocalDateTime ldt = LocalDateTime.parse( input , f );

ldt.toString(): 2018-11-22T00:00

但我们已被告知该字符串旨在表示 UTC 中的一个时刻,但发件人搞砸了并且未能包含该信息(例如 Z+00:00 在末尾表示 UTC)。因此,我们可以应用 0 小时分秒的 UTC 偏移量来确定实际时刻,即时间线上的特定点。结果为OffsetDateTime 对象。

OffsetDateTime odt = ldt.atOffset( ZoneOffset.UTC );

odt.toString(): 2018-11-22T00:00Z

末尾的Z 表示UTC,发音为“Zulu”。在 ISO 8601 标准中定义。

现在我们有时间了,我们可以在 SQL 标准类型 TIMESTAMP WITH TIME ZONE 的列中将它发送到数据库。

preparedStatement.setObject( 2 , odt );

然后检索存储的值。

 OffsetDateTime odt = rs.getObject( "when_" , OffsetDateTime.class );

2018-11-22T00:00Z

这是此示例应用程序的完整内容。

package com.basilbourque.example;

import java.sql.*;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

public class MomentIntoDatabase 

    public static void main ( String[] args ) 
        MomentIntoDatabase app = new MomentIntoDatabase();
        app.doIt();
    

    private void doIt ( ) 
        try 
            Class.forName( "org.h2.Driver" );
         catch ( ClassNotFoundException e ) 
            e.printStackTrace();
        

        try (
                Connection conn = DriverManager.getConnection( "jdbc:h2:mem:moment_into_db_example_" ) ;
                Statement stmt = conn.createStatement() ;
        ) 
            String sql = "CREATE TABLE event_ (\n" +
                    "  id_ UUID DEFAULT random_uuid() PRIMARY KEY ,\n" +
                    "  name_ VARCHAR NOT NULL ,\n" +
                    "  when_ TIMESTAMP WITH TIME ZONE NOT NULL\n" +
                    ") ; ";
            System.out.println( sql );
            stmt.execute( sql );

            // Insert row.
            sql = "INSERT INTO event_ ( name_ , when_ ) " + "VALUES ( ? , ? ) ;";
            try ( PreparedStatement preparedStatement = conn.prepareStatement( sql ) ; ) 
                String name = "whatever";
                String input = "22.11.2018 00:00:00";
                DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd.MM.uuuu HH:mm:ss" );
                LocalDateTime ldt = LocalDateTime.parse( input , f );
                System.out.println( "ldt.toString(): " + ldt );
                OffsetDateTime odt = ldt.atOffset( ZoneOffset.UTC );
                System.out.println( "odt.toString(): " + odt );

                preparedStatement.setString( 1 , name );
                preparedStatement.setObject( 2 , odt );
                preparedStatement.executeUpdate();
            

            // Query all.
            sql = "SELECT * FROM event_ ;";
            try ( ResultSet rs = stmt.executeQuery( sql ) ; ) 
                while ( rs.next() ) 
                    //Retrieve by column name
                    UUID id = ( UUID ) rs.getObject( "id_" );  // Cast the `Object` object to UUID if your driver does not support JDBC 4.2 and its ability to pass the expected return type for type-safety.
                    String name = rs.getString( "name_" );
                    OffsetDateTime odt = rs.getObject( "when_" , OffsetDateTime.class );

                    //Display values
                    System.out.println( "id: " + id + " | name: " + name + " | when: " + odt );
                
            
         catch ( SQLException e ) 
            e.printStackTrace();
        
    

转换

如果您必须与 java.time 尚未更新的旧代码进行互操作,则可以来回转换。查看添加到旧类的新方法 to…/from…

要获取旧版 java.sql.Timestamp 对象,请调用 Timestamp.from( Instant )。要从上面看到的OffsetDateTime 对象中获取Instant,只需调用OffsetDateTime::toInstant

java.sql.Timestamp ts = Timestamp.from( odt.toInstant() ) ;

往另一个方向发展。

OffsetDateTime odt = OffsetDateTime.ofInstant( ts.toInstant() , ZoneOffset.UTC ) ;

如果将 ThreeTen-Backport 库用于 Java 6 和 7 项目,请查看 DateTimeUtils 类以了解 to…/from… 转换方法。


关于java.time

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

要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310。

Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

您可以直接与您的数据库交换 java.time 对象。使用符合JDBC 4.2 或更高版本的JDBC driver。不需要字符串,不需要java.sql.* 类。 Hibernate 5 & JPA 2.2 支持 java.time

从哪里获取 java.time 类?

Java SE 8Java SE 9Java SE 10Java SE 11 和更高版本 - 具有捆绑实现的标准 Java API 的一部分。 Java 9 带来了一些小功能和修复。 Java SE 6Java SE 7 大部分 java.time 功能在ThreeTen-Backport 中向后移植到 Java 6 和 7。 Android java.time 类的更高版本的 android (26+) 捆绑实现。 对于早期的 Android (API desugaring 的进程带来了最初未内置于 Android 中的 subset of the java.time 功能。 如果脱糖不能满足您的需求,ThreeTenABP 项目会将ThreeTen-Backport(如上所述)适配到 Android。见How to use ThreeTenABP…

【讨论】:

@Dogbert_1825027 (A) 至于与尚未更新到 java.time 的旧代码互操作的需要,好点。您的评论促使我在底部附近添加一个 Converting 部分,其中包含在旧类和现代类之间来回切换的代码示例。 (B) 至于我坚持避免遗留类,我坚持自己的立场。 遗留的日期时间类真的很糟糕,在许多方面存在缺陷,非常难以使用,这可以从 Stack Overflow 上的大量问题中看出。将 ThreeTen-Backport 添加到旧的 Java 6/7 项目非常值得。【参考方案3】:

如果你想要当前的时间戳为什么不使用下面的函数,我已经在各种项目中使用过并且效果很好:

public static Timestamp getTimeStamp()

    // Calendar information
    Calendar calendar       = Calendar.getInstance();
    java.util.Date now      = calendar.getTime();
    Timestamp dbStamp       = new Timestamp(now.getTime());
    return dbStamp;
   

例子:

System.out.println( getTimeStamp() );

输出:2017-03-13 15:01:34.027

编辑

使用 Java 8 本地日期时间:

public static Timestamp getTimeStamp()

    return Timestamp.valueOf(LocalDateTime.now());
   

【讨论】:

谢谢,但我想使用 java 8 方式,因为它是不可变的类 @AleydinKaraimin 我添加了一种不同的方式,使用 LocalDateTime 提供相同的输出。这个怎么样? 是的,我在帖子中提到使用这种方式很好,但是,我只想使用即时,并且想了解发生这种情况的原因。当接受即时时,可能会创建时间戳添加偏移量【参考方案4】:

Instant 始终提供 UTC 时间,而 Timestamp 提供您当地区域的时间。 因此,如果您不关心任何特定时区,您可以使用 Instant。 以 UTC 格式保存记录也是一种很好的做法,这样您的应用程序在部署在任何其他时区时都不会受到影响。

【讨论】:

【参考方案5】:

在将记录保存到 SQL Server 数据库期间,我遇到了同样的问题。我已经使用java.sql.Timestamp.valueOf(String s)UTC 中获取 Timestamp

import java.time.Instant; import java.time.format.DateTimeFormatter; .... .... DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(UTC); String dateTime = dateTimeFormatter.format(Instant date); Timestamp timestamp = Timestamp.valueOf(dateTime);

它对我有用。

【讨论】:

永远不要使用 java.sql.Timestamp 类。 它已完全被现代 java.time 类所取代。无需将可怕的传统课程与现代课程混为一谈。 Timestamp 替换为 Instant(根据定义始终为 UTC)或 OffsetDateTime(设置为 UTC)。 我使用 java.sql.Timestamp 类 将日期存储到数据库中。 从 JDBC 4.2 开始,时间戳被 OffsetDateTime 取代。搜索堆栈溢出。这已经被报道过很多次了。 myPreparedStatement.setObject( … , Instant.now().atOffset( ZoneOffset.UTC ) ) ; 我有:String dateStr = "22.11.2018 00:00:00"; OffsetDateTime offsetDateTime = date.atOffset(ZoneOffset.UTC); -> 2018-11-22T00:00Z SQl 服务器数据库 将 offsetDateTime 存储为 2018-11-21 22:00:00.000 而不是 2018-11-22 00:00:00.000 我添加了 my own Answer 以及示例代码供您学习。该示例使用 H2 数据库引擎,但我希望您在使用 Microsoft SQL Server 时看到相同的结果。

以上是关于将 java.time.Instant 转换为没有区域偏移的 java.sql.Timestamp的主要内容,如果未能解决你的问题,请参考以下文章

如何在本地时区将 java.time.Instant 格式化为字符串?

使用 JOOQ 在 java.sql.Timestamp 和 java.time.Instant 之间转换时遇到问题

将毫秒时间戳反序列化为 java.time.Instant

调用需要 API 级别 26(当前最低为 23):java.time.Instant#now

com.fasterxml.jackson.databind.exc.InvalidDefinitionException:无法构造`java.time.Instant`的实例(没有创建者,如默认构造

Java SE 8 TemporalAccessor.from 与 java.time.Instant 对象一起使用时出现问题