【问题标题】:Does the timezone matter when parsing a Timestamp?解析时间戳时时区是否重要?
【发布时间】:2017-06-16 09:31:10
【问题描述】:

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

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

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

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

  • 之前:"2012-02-12T09:08:13.123456-0400"
  • 之后:"2012-02-12 09:08:13.123456"

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

我认为正确的答案应该是 java.sql.Timestamp 不是特定于时区的。时间戳是 java.util.Date 和单独的纳秒值的组合。此类中没有时区信息。因此,就像 Date 一样,这个类只保存自 1970 年 1 月 1 日 00:00:00 GMT + nanos 以来的毫秒数。

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

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

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

【问题讨论】:

  • java.sql.Timestamp 是特定于时区的。来自 Javadocs:使用给定的 Calendar 对象将指定参数设置为给定的 java.sql.Timestamp 值。驱动程序使用日历对象构造一个 SQL TIMESTAMP 值,然后驱动程序将其发送到数据库。使用 Calendar 对象,驱动程序可以在考虑自定义时区的情况下计算时间戳。如果未指定日历对象,则驱动程序使用默认时区,即运行应用程序的虚拟机的时区。
  • 您的测试似乎有效,因为您在时区偏移量与原始字符串中显示的相同的机器上运行它。但是如果字符串有不同的偏移量,显然你的 Timestamp 值不会改变,因为你忽略了偏移量。最佳解决方案取决于您运行的是 Java 8 还是更高版本。请说明。
  • @erickson 确实,我是。 && 我正在运行 java 7
  • 当然时区很重要。在伦敦的2017-06-15 12:00:00 发生的事件和在纽约的2017-06-15 12:00:00 发生的事件相隔 5 小时。

标签: java java-7


【解决方案1】:

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 类表示UTC 中时间轴上的时刻,分辨率为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[美国/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 中,在解析缺少小时和分钟之间的可选冒号字符的偏移时存在一个小错误。所以 Java 8 中的 -04:00 将解析但不会解析 -0400。 Java 9 中修复的错误。您的 String 确实符合 java.time 类中默认使用的日期时间格式的 ISO 8601 标准。提示:通常最好始终使用冒号、小时/分钟和填充零来格式化偏移量——我见过其他协议和库只需要这种完整格式。

在您迁移到 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

在 Java 6 和 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 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

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

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

从哪里获得 java.time 类?

ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

【讨论】:

    【解决方案2】:

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

    分配一个 Date 对象并对其进行初始化,以便它表示由年、月、日、小时、分钟和秒参数指定的秒开始时的瞬间,在本地时区.

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

    这意味着您本地时区 10:00 的时间戳将在 10:00 以 SQL TIMESTAMP 形式出现在数据库中,而不是在 - 例如 - 如果您的时区比 GMT 早 2 小时,则在 08:00 ;加载时反之。

    【讨论】:

      【解决方案3】:

      您是正确的,java.sql.Timestamp 没有时区信息。在幕后,它只是从纪元开始的毫秒数,而纪元本身的定义基本上是 0 偏移量,即 1970 年 1 月 1 日,格林威治标准时间 00:00:00。

      话虽如此,在处理时间时,偏移总是很重要,这将带您进入地狱,呃,“动态世界”,即 Java 8 java.time 中的 ISO-8601。偏移量很重要,除非你站在英格兰的格林威治。如果您试图忽略它,您有时可能会猜错一天;例如,格林威治的星期一比西雅图的星期一早 8 小时。我不认为 Java8 为 ISO-8601 的 -0000 变体内置了 DateTimeFormatter,但如果您确定您的格式是“稳定的”,这将起作用。

      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());
          }
        }
      }
      

      一定有更好的方法。

      【讨论】:

      • OP 正在使用 Java 7。
      • 当这个问题是关于 java.sql.Timestamp 时,你为什么要抱怨 Java 8 的 java.time。
      • @shmosel 我没有在任何地方看到它,我的错。在 Java 7 中,JODA 可能是您唯一的出路。看起来 Joda 在任何情况下都缩短了工作时间,但似乎失去了纳秒
      • @MarkRotteveel 他在问他是否可以忽略偏移量以及如何解析。 DateTimeFormatter 是关于解析的。事实上,我在抱怨 java.time 中的难度有多大。继续。
      猜你喜欢
      • 2015-08-03
      • 2021-07-25
      • 1970-01-01
      • 1970-01-01
      • 2021-06-30
      • 2021-09-24
      • 1970-01-01
      • 2015-07-26
      • 1970-01-01
      相关资源
      最近更新 更多