能否强制Java忽略时区?

4

简短版: 我能强制Java忽略时区吗?

详细版: 我有一个基于Java 6的旧三层系统,其中包括一个Java GUI前端(运行在用户PC上),一个Java / OSGi中间件服务器和一个Oracle 11g数据库后端。这两个服务器位于同一机架内的集群中。存储在数据库中的主要实体(检查)具有Date列,该列存储检查发生时的日期和时间。日期和时间以字符串形式在GUI中输入,并按以下方式存储在DB中:

(theInspection.getInspectionDate() returns a java.util.Date object)

inspectionDate = new java.sql.Timestamp(theInspection.getInspectionDate().getTime());
...
statementHandler.openPreparedStatement(C_INSERT_INTO_INSPECTIONS);
...
statementHandler.setTimestamp(4, inspectionDate);

检查日期和时间,然后将其回读到中间件服务器,以识别和定位文件名中包含日期和时间的原始数据文件。从数据库中读取的方法如下:
resultSet.getDate("inspection_date");
if (resultSet.wasNull()) {
    inspection.setInspectionDate(null);
    } else {
        date = new java.util.Date(resultSet.getDate("inspection_date").getTime() 
                    + resultSet.getTime("inspection_date").getTime() + (3600 * 1000L) );
        inspection.setInspectionDate(date);
    }

现在,在CET/CEST(UTC+1)时区下运行良好的代码,但是在移动到MSK(UTC+3)时区后出现了问题。时间在存储到数据库时会向前偏移2小时,并在读取时再次向前偏移2小时。我已经确定是Java导致的问题,在这种设置下Oracle不支持时区。
我考虑更改中间件服务器的设置,将其转换为UTC进行存储,在读取时将其转换回本地时区。我知道java.util.Date已过期,并且有其他表示时间和日期的方式。然而,如果GUI客户端和服务器位于不同的时区,或者服务器或数据被移动到不同的时区,我会担心会发生什么。
最后,强制Java忽略时区可能是最合理的选择。这是否可能?
谢谢!

inspection_date列的数据类型是什么?日期,时间戳,带时区的时间戳,带本地时区的时间戳? - krokodilko
日期,不包含任何时区数据。 - larslars
这两个服务器的操作系统时区是否相同?Java是否依赖于该时区,还是您指定了覆盖操作系统的区域设置? - Alex Poole
是的,两个服务器都在同一个时区。没有覆盖操作系统的语言环境。GUI客户端可能位于不同的时区,但是Java到数据库的转换是在服务器上进行的,这就是我认为偏移量被插入的地方。 - larslars
1
你能告诉我们你使用的Oracle JDBC驱动程序版本吗?从9.2版本开始,他们改变了日期的行为,请参考:http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-faq-090281.html#08_00 - krokodilko
显示剩余2条评论
2个回答

4
你会发现 Date 使用了隐含时区,因为它被定义为自 "1970-01-01T00:00:00" UTC 以来的毫秒数。因此,使用不同时区转换日期(+时间)字符串为 Date 实例将导致内部值的毫秒数不同。
在进行字符串到日期的转换和转换为数据库值时未指定显式时区并不意味着这些值不受与时区相关的影响。
一旦在不知情的情况下静默地更改了这个隐含时区,这种更改就会表现为时间上的“扭曲”。
这种“静默”时区更改将在以下场合发生(不完全列表):
- 将 Date 对象实例从一个时区发送到另一个时区 - 在与该值关联的暗示时区不是时区的情况下解析日期 - 代码错误地假设日期/时间值的隐含时区,并试图对其进行虚假的“修正”。 (例如,当没有时区的数据库值从数据库中获取了一个隐含的时区,并且读取应用程序代码尝试将其调整为应用程序所假定的一个(不同的)本地时区时。)
为了克服这种问题,基本上有两种策略:
1. 严格强制整个应用程序使用唯一的时区。 (鉴于 Date 使用 UTC 的隐含语义,这将是一个不错的选择。)在这种情况下,您将立即将任何从“外部”输入的值转换为您共同的应用程序时区(来自已知或暗示的本地时区)。同时,仅在显示时才将(内部)日期/时间值转换为本地时区(尽可能晚)。任何“内部”应用程序(并存储在数据库中)的值都将处于应用程序时区。这对于服务器和数据库更改时区是不变的,只需要正确检测日期 I/O 位置。您还需要注意与其他外部实体相关的日期/时间值(例如您提到的文件名)。最好根据应用程序时区生成这些值。
2. 放弃试图无视时区概念,并更改为在任何日期/时间值上使用显式时区。
第一种方法的好处是提供不同时区之间一致的值(例如使用日期/时间值生成文件名 - 然后需要将其用作 UTC 值)。
第二种方法易于提供来自不同时区事件的一致排序。
在从数据库存储/读取值时进行时区转换具有可能存在误解的风险,因为客户端可能居住在完全不同的时区或在存储和检索此类值之间更改时区(正如您已经怀疑的那样)。
为了明确回答主题中的问题:
不,只要您使用java的Date类,在从/转换为字符串时始终会涉及隐式时区,而内部值则基于UTC

我终于获得了生产集群的访问权限,并将所有时区更改为UTC+1,这与已经运行多年的集群相同。我本以为“+1”应该会像“+3”一样出现问题,但显然并没有!现在所有事情都运行顺畅。问题解决了! :-) - larslars

1
JDBC提供了在读取或写入日期时设置时区的方法。
对于时间戳,以下是方法。 ResultSet getTimeStamp PreparedStatement setTimeStamp 使用这些方法与以下构造的日历实例一起使用。
Calendar gmtCalendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));

在编写代码时,它将变得

statementHandler.setTimestamp(4, inspectionDate, gmtCalendar);

在阅读时,它将会。
date = new java.util.Date(resultSet.getDate("inspection_date", gmtCalendar).getTime() 
                    + resultSet.getTime("inspection_date", gmtCalendar).getTime());

这就是解决问题所需的全部,因为您使用的是getTime()(毫秒数),所以读取的时间是GMT/UTC。由于默认情况下时间戳(毫秒数)是在GMT/UTC中,唯一剩下的事情就是指示JDBC考虑该时区。
有关此主题的更多详细信息,请参见此SO条目https://dev59.com/WJbfa4cB1Zd3GeqPy8wW#37103310

谢谢,我已经尝试过了,但它没有起作用。在JDK 6中可能有一些难以控制的Java问题。 - larslars
我在一个项目中使用了与JDK 6和Oracle相同的配置,这应该可以工作。 - 11thdimension

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接