【问题标题】:Java 1.7 change in daylight savings is not recognizedJava 1.7 对夏令时的更改无法识别
【发布时间】:2018-03-06 06:55:52
【问题描述】:

Java:ver 1.7 更新 71 数据库:MySQL 5.6.x 版

我们有多个正在运行的应用程序访问同一个数据库。两个应用程序在 Tomcat 7(版本 7.0.52)上运行,两个应用程序使用 Netty 3.10。服务器位于 EST(美国)时区。

Web 应用程序技术栈: 春天 3.1.x Struts MVC 2.3.14 休眠 3.3.x

Netty 应用技术栈: 网络 3.10 休眠 3.3.x

我们正在使用许多 Apache log4j,跨应用程序的公共库。

在最近的夏令时更改期间,在 tomcat 下运行的应用程序能够在 DST 之后获得正确的时间。根据 DST 的时区,Linux 和 DB 显示正确的时间。然而,在 Netty 上运行的那些继续落后一小时。不仅为应用程序插入的行设置的日期,包括日志文件中的时间戳在内的所有日期值都显示不正确的时间。

重新启动后,它们开始显示正确的时间。

Java 使用底层操作系统的时区详细信息。作为启动的一部分,没有提供与时区相关的特定设置。

我认为这个问题在 JDK 1.5/1.6 期间已解决,并在 1.7 中自动处理。

是否有一些补丁需要应用于 Java 1.7?应用程序中是否需要明确执行任何操作以识别 DST 的变化? Tomcat 是如何处理这个问题的?

【问题讨论】:

标签: java tomcat netty dst


【解决方案1】:

tzdata

时区数据几乎总是由 tz database 定义,以前称为 Olson 数据库,现在由 IANA 处理。该数据库记录了全球每个地区(时区)的人们使用的 UTC 偏移量的过去、现在和未来变化的历史。

区域定义更改

此数据库经常更改,因为世界各地的政客莫名其妙地被频繁更改所吸引。更改通常是在没有预先警告的情况下进行的,只需提前几周即可。

当不安分的政客做出改变时,如果您关心受影响的区域,您需要更新您的操作系统(对于本机应用程序)和您的 Java 实现(对于 Java 应用程序)的“tzdata”。 Oracle 提供了Timezone Updater Tool 来为他们的实现安装一个新的 tzdata。 Oracle 的更新版本通常包括最新的 tzdata,但并非总是如此,因此您应该阅读发行说明以了解此类详细信息。随着 Oracle 和 OpenJDK 实施新计划的季度发布节奏,如果您选择与 Java 版本保持同步,手动安装 tzdata 的需求可能会减少。

因此,当您关心的时区定义发生变化时,您可能需要至少在三个地方更改 tzdata

  • 您的计算机操作系统。
  • 你的 JVM。
  • 您的数据库服务器,如果该数据库具有复杂的日期时间处理功能,例如 Postgres(请参阅 example history)。

Java 使用底层操作系统的时区详细信息。

没有。时区细节的来源是每个 Java 实现的实现细节。

在我所知道的 JVM 实现中,JVM 通过询问主机操作系统作为 JVM 启动的一部分来确定当前的默认时区。作为启动的一部分,默认时区可能已作为启动参数传递给 JVM。第三,任何应用程序的任何线程中的任何代码在任何时候在运行时都可能更改默认时区(这几乎不是一个好主意)。

但在所有这些情况下,区域的定义存储在 JVM 本身。因此,如果感兴趣的区域发生了变化,您需要保持 JVM 的 tzdata 处于最新状态。

问题的可能原因:缓存偏移而不是区域

在 Netty 上运行的继续落后一小时……

重新启动后,它们开始显示正确的时间。

没有看到您的应用程序的编程,也没有看到 Netty 的编程,我们只能猜测具体问题。我怀疑该应用可能无法正确处理日期时间。

捕捉瞬间的正确方法是在UTC 中进行。在 Java 中,这意味着 Instant 类。 Instant 类代表UTC 时间线上的时刻,分辨率为nanoseconds(最多九 (9) 位小数)。

Instant instant = Instant.now() ;  // Capture current moment in UTC.

或者,您可以捕捉特定时区的时刻。

ZoneId z = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.now( z );

您的应用可能仅使用应用启动时缓存的与 UTC 的偏移量。

ZoneOffset offset = ZoneOffset.systemDefault() ; // Do NOT cache this, as it will be invalid after a DST cut-over.

时区总是比offset-from-UTC 更可取。偏移量只是几个小时、分钟和秒数——不多也不少。相比之下,time zone 是特定地区的人们使用的偏移量的过去、现在和未来变化的历史。

如果应用在启动时检测到当前正在使用的偏移量,并无限期地缓存该偏移量,则在Daylight Saving Time (DST) 切换后它的使用将不再正确。

例如,当前在America/Los_Angeles 区域中,与UTC 的偏移量比UTC 晚八小时,-08:00。但是几天后,在 3 月的第二个星期日,DST 废话开始生效,当时每个人都将时钟提前一小时,从而将使用中的 UTC 偏移更改为比 UTC 晚 7 小时,-07:00。如果 -08:00 被不当缓存,捕捉当前时刻的尝试将不正确。

OffsetDateTime odt = OffsetDateTime.now( inappropriatelyCachedOffset ) ;  // Capturing the wrong wall-clock time. 

同样,预防措施很简单……使用时区,而不是与 UTC 偏移

continent/region 的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用 3-4 个字母的缩写,例如 ESTIST,因为它们不是真正的时区,没有标准化,甚至不是唯一的 (!)。

问题的可能原因:数据库tzdata 未更新

您的明显行为的另一个可能原因可能是您的应用从数据库服务器获取当前分区日期时间,其中数据库自己的 tzdata 现在已过时,尚未更新以反映该区域定义中的更改。

如上所述,一些复杂的数据库(例如 Postgres)有自己的内部存储的 tzdata 信息。预防措施是通过其版本更新使数据库服务器保持最新,或者手动安装更新的tzdata

关注世界标准时间

这也是思考、工作、记录、存储和交换 UTC 日期时间值的另一个原因。在整个问题的讨论中,时钟可能已正确设置为当前时间。这是通过一个可能出错的区域对那个时间的解释。如果当前是在 UTC 中捕获的,则不会调整到时区,因此没有问题。

所以要学会用 UTC 来思考。在担任程序员或管理员的工作时,请忘记您自己的狭隘时区。将桌面上的第二个时钟设置为 UTC。使用 UTC 进行所有记录和记笔记。将 UTC 视为一个真实时间,所有其他分区时间只是变化。

同样,Instant.now() 是您的朋友。

ISO 8601

当您需要序列化日期时间值时,请仅使用标准的ISO 8601 格式。这个标准被巧妙地设计成实用和明智的。这些格式很容易被机器解析,但也很容易被跨文化的人类阅读。

暂时采用 UTC 格式,格式为 YYYY-MM-DDTHH:MM:SS.SZ。中间的T 将年-月-日与时-分-秒分开。末尾的ZZulu 的缩写,表示UTC。

2018-01-23T12:34:56.123456789Z

java.time 类在解析/生成字符串时默认使用 ISO 8601 格式。

String output = instant.toString() ;

……和……

Instant instant = Instant.parse( "2018-01-23T12:34:56.123456789Z" ) ;

使用 java.time

另一个重要的一点:永远不要使用与 Java 的最早版本捆绑在一起的麻烦的旧日期时间类。他们是一个可怕的可怜的烂摊子,要避免。它们现在是遗留的,完全被 java.time所取代。上面看到的所有代码都使用了现代的 java.time 类。

是否有一些补丁需要应用于 Java 1.7?

我不知道。但是您可能需要在 JVM 和数据库服务器中更新您的 tzdata,如上所述。

应用程序中是否需要明确执行任何操作来识别 DST 的变化?

没有。在 DST 切换期间您无需执行任何操作如果您的 tzdata 在 (a) 在您的 JVM 中和 (b) 在您的数据库中是最新的,并且您使用时区而不是只是偏移量。

Tomcat 是如何处理这个问题的?

没有。 Apache Tomcat 与您的日期时间问题无关。 Tomcat 对时区和偏移一无所知。那是你的 JVM 的事,不是 Tomcat 的事。


关于java.time

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

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

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

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

从哪里获得 java.time 类?

  • Java SE 8Java SE 9 及更高版本
    • 内置。
    • 标准 Java API 的一部分,带有捆绑实现。
    • Java 9 添加了一些小功能和修复。
  • Java SE 6Java SE 7
    • ThreeTen-Backport 中的大部分 java.time 功能都向后移植到 Java 6 和 7。
  • Android
    • java.time 类的 Android 捆绑包实现的更高版本。
    • 对于早期的 Android (ThreeTenABP 项目采用 ThreeTen-Backport(如上所述)。见How to use ThreeTenABP…

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-08-14
    • 2020-03-01
    • 2022-01-03
    • 1970-01-01
    • 2015-02-08
    • 2014-11-03
    • 2014-09-30
    • 2017-05-13
    相关资源
    最近更新 更多