由于 Java SimpleDateFormat 问题,在 Oracle DB 中保存时间数据(带区域)不起作用

Posted

技术标签:

【中文标题】由于 Java SimpleDateFormat 问题,在 Oracle DB 中保存时间数据(带区域)不起作用【英文标题】:Saving time data (with Zone) in Oracle DB not working because of Java SimpleDateFormat issue 【发布时间】:2018-01-09 23:32:07 【问题描述】:

我有一个定义为 TIMESTAMP WITH TIME ZONE 的字段。

要保存的值以:"09-23-2019 10:03:11 pm" 开头,在US/Hawaii 的区域中。

这是我要保存到数据库的内容(所有日期信息加上区域)

数据库以UTC格式存储时间信息。

截至目前,日期已存储在数据库中,如下所示:

DAYS
---------------------------------------------------------------------------
23-SEP-19 10.03.11.000000 PM -05:00
23-SEP-19 10.03.11.000000 PM -05:00

在处理过程中,它会运行这段代码:

    dateStr: the date (as seen above)
    ZoneLoc: 'US/Hawaii'

    public Calendar convDateStrWithZoneTOCalendar(String dateStr,
            String ZoneLoc) throws Exception 

        // convert the string sent in from user (which uses AM/PM) to one that uses military time (24HR)
        // it
        String formattedDate = null;
        DateFormat readFormat = new SimpleDateFormat(this.getPattern());
        DateFormat writeFormat = new SimpleDateFormat("MM-dd-yyyy'T'HH:mm:ss'Z'");
        writeFormat.setTimeZone(TimeZone.getTimeZone(ZoneLoc));
        Date date = null;
        date = readFormat.parse(dateStr);
        formattedDate = writeFormat.format(date);

        // see if you can parse the date needed WITH the TimeZone
        Date d;
        SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy'T'HH:mm:ss'Z'");
        sdf.setTimeZone(TimeZone.getTimeZone(ZoneLoc));
        d = sdf.parse(formattedDate);
        Calendar cal = Calendar.getInstance();
        cal.setTime(d);

        system.out.println(" ZONELOC VALUE " + ZoneLoc);
        system.out.println(" RETURNED VALUE " + cal );

        return cal;
    

返回的日历信息是:

ZONELOC 价值是美国/夏威夷

返回值为 java.util.GregorianCalendar[time=1577678591000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Chicago",offset=-21600000,dstSavings=3600000,useDaylight =true,transitions=235,lastRule=java.util.SimpleTimeZone[id=America/Chicago,offset=-21600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8, startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1, YEAR=2019,MONTH=11,WEEK_OF_YEAR=1,WEEK_OF_MONTH=5,DAY_OF_MONTH=29,DAY_OF_YEAR=363,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=5,AM_PM=1,HOUR=10,HOUR_OF_DAY=22,MINUTE=3,SECOND= 11,MILLISECOND=0,ZONE_OFFSET=-21600000,DST_OFFSET=0]

返回值中似乎没有设置美国/夏威夷。

我可以做些什么来确保它被设置?

之后,我可以将它放在数据库中,看看设置是否会“粘住”而不恢复到America/Chicago

更新 @Patrick H - 感谢您的输入。我使用您指定的模式进行了更改,并且能够保存数据。现在看起来像这样:

2017-08-02 13:38:49 TRACE ohtype.descriptor.sql.BasicBinder - 绑定参数 [26] 作为 [TIMESTAMP] - [java.util.GregorianCalendar[time=1569294191000,areFieldsSet=true,areAllFieldsSet= true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Chicago",offset=-21600000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id =America/Chicago,offset=-21600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3, endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2019,MONTH=8,WEEK_OF_YEAR=39,WEEK_OF_MONTH=4, DAY_OF_MONTH=23,DAY_OF_YEAR=266,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=10,HOUR_OF_DAY=22,MINUTE=3,SECOND=11,MILLISECOND=0,ZONE_OFFSET=-21600000,DST_OFFSET=3600000]]

数据库中的数据如下所示:

23-SEP-19 10.03.11.000000 PM -05:00

即使指定了US/Hawaii,区域仍然是America/Chicago。怎样才能让US/Hawaii 坚持下来而不恢复到America/Chicago

【问题讨论】:

【参考方案1】:

根据SimpleDateFormat,我认为您的格式化字符串是错误的。您还可以在返回值中看到月份和日期是错误的。 MONTH=11,DAY_OF_MONTH=29

这是您目前拥有的: 23-SEP-19 10.03.11.000000 PM -05:00

我认为格式化字符串应该是:'dd-MMM-yy hh.mm.ss.SSSSSS a Z'

看起来时区问题也可能是因为其中有一个冒号。 SimpleDateFormat 的文档表明,对于 RFC 822 时区,它需要采用这种格式:-0500 您可能会发现使用通用时区组件更容易。

【讨论】:

感谢您的回复。我更新了我的帖子。你能看吗?只是想知道如何指定区域。 TIA 尝试将d = sdf.parse(formattedDate); 的行更改为d = sdf.format(sdf.parse(formattedDate)); 我得到:类型不匹配:无法从字符串转换为日期 从我刚刚做的一点谷歌搜索来看,似乎为了转换它,你必须使用第二个 SimpleDateFormat 对象并再次调用格式命令。 现在再看一遍,日历只是在输入一个日期字段。我认为您想要做的是更改日历的时区本身。在放入日期之前将其格式化为字符串应该无关紧要。在日历上设置时间后尝试添加此行。 cal.setTimeZone(TimeZone.getTimeZone(ZoneLoc));【参考方案2】:

根据这个输出:

java.util.GregorianCalendar[time=1569294191000,...

上面的时间值(表示自 unix 纪元 (1970-01-01T00:00Z) 以来的 1569294191000 毫秒)相当于 芝加哥 中的 09-23-2019 10:03 PM。那是因为readFormat使用的是系统的默认时区(可能是America/Chicago,只需检查TimeZone.getDefault()的值)。

要解析输入09-23-2019 10:03:11 pm 并将其视为夏威夷的当地时间,您只需将相应的时区设置为SimpleDateFormat 实例(在本例中为readFormat,因为它需要在输入日期是什么时区 - 因为您没有设置任何时区,所以它使用系统的默认值)。您也不需要其他格式化程序(writeFormatsdf),只需一个格式化程序即可获取对应的日期:

SimpleDateFormat parser = new SimpleDateFormat("MM-dd-yyyy hh:mm:ss a");
// the input is in Hawaii timezone
parser.setTimeZone(TimeZone.getTimeZone("US/Hawaii"));
Date date = parser.parse("09-23-2019 10:03:11 pm");

上面的date 相当于夏威夷的晚上 10:03。 实际上,日期本身仅包含从 unix 纪元开始的毫秒数(date.getTime() 返回 1569312191000)和has no format nor any timezone information

然后您可以将其设置为 Calendar 实例(不要忘记设置日历的时区):

Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("US/Hawaii"));
cal.setTime(date);

自从我使用 oracle 的时间戳和时区类型以来已经有一段时间了,但我认为那将是 enough to save the correct values。 calendar 的值为:

java.util.GregorianCalendar[time=1569312191000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="US/Hawaii", offset=-36000000,dstSavings=0,useDaylight=false,transitions=7,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2019,MONTH=8,WEEK_OF_YEAR=39,WEEK_OF_MONTH=4, DAY_OF_MONTH=23,DAY_OF_YEAR=266,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=10,HOUR_OF_DAY=22,MINUTE=3,SECOND=11,MILLISECOND=0,ZONE_OFFSET=- 36000000,DST_OFFSET=0]


Java 新的日期/时间 API

旧的类(DateCalendarSimpleDateFormat)有 lots of problems 和 design issues,它们正在被新的 API 取代。

主要问题之一是使用不同的时区是多么困难和混乱。

如果您使用的是 Java 8,请考虑使用new java.time API。更简单,less bugged and less error-prone than the old APIs。

如果您使用的是 Java ,则可以使用 ThreeTen Backport,这是 Java 8 新日期/时间类的一个很好的向后移植。对于Android,还有ThreeTenABP(更多关于如何使用它here)。

下面的代码适用于两者。 唯一的区别是包名(在 Java 8 中是 java.time,在 ThreeTen Backport(或 android 的 ThreeTenABP)中是 org.threeten.bp),但类和方法 names 是相同的。

要解析输入 09-23-2019 10:03:11 pm,您可以使用 DateTimeFormatter 并将其解析为 LocalDateTime - 输入没有时区信息,因此我们只考虑日期和时间,然后我们可以将其转换为时区。

// parse the input
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // parse AM/PM and am/pm
    .parseCaseInsensitive()
    // input pattern
    .appendPattern("MM-dd-yyyy hh:mm:ss a")
    // use English locale for am/pm symbols
    .toFormatter(Locale.ENGLISH);
LocalDateTime dt = LocalDateTime.parse("09-23-2019 10:03:11 pm", fmt);
// convert to Hawaii timezone
ZonedDateTime hawaiiDate = dt.atZone(ZoneId.of("US/Hawaii"));

最新的 JDBC 驱动程序支持新的 API(但我猜只支持 Java 8),但如果您仍需要使用 Calendar,您可以轻松地将 ZonedDateTime 转换为它:

Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("US/Hawaii"));
calendar.setTimeInMillis(hawaiiDate.toInstant().toEpochMilli());

在 Java 8 中,您还可以这样做:

Calendar calendar = GregorianCalendar.from(hawaiiDate);

如果您需要与旧的 CalendarDate API 的互操作性,您可以在内部使用新的 API 进行计算并在需要时与 API 进行转换。

【讨论】:

嘿@Hugo - 感谢您的详细回复!我做了你建议的改变。对于引用日历的第二个条目,我没有使用“US/Hawaii”,而是将其更改为“UTC”。就我而言,数据库设置为使用 UTC。我也在使用 Spring Boot - 所以 - 我将 TimeZone.setDefault(TimeZone.getTimeZone("UTC")) 添加到 Spring Boot 应用程序的主要功能中。因此,可以根据客户的位置向客户显示数据。数据现在显示正确的值。再次感谢您的意见!

以上是关于由于 Java SimpleDateFormat 问题,在 Oracle DB 中保存时间数据(带区域)不起作用的主要内容,如果未能解决你的问题,请参考以下文章

Java SimpleDateFormat时间解析时区问题

Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法

Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法

类DateFormat(子类SimpleDateFormat)

抽象类DateFormat及子类SimpleDateFormat

Java学习笔记4.6.2 格式化 - SimpleDateFormat类