从XMLGregorianCalendar转换为Calendar时的日期更改

在测试在系统之间映射日期时间类型的Web服务时,我注意到在公历开始时间之前发送任何日期会导致在转换为最终类型时失去准确性,最终结果总是在范围内略微提前几天。

我把问题缩小到了确切的界限,但我仍然无法弄清楚为什么它会像这样投射,从文件中说明朱利安日历用于公历开始之前的日期时间:1582年10月15日。

问题行是从XMLGregorianCalendarGregorianCalendar ,第78行: calendarDate = argCal.toGregorianCalendar(); 当从第86行的calendarDate获取时间时: cal.setTime(calendarDate.getTime()); 正如您在下面的程序输出中所看到的那样,时间比1月3日而不是1月1日提前2天。

这是我为展示铸造过程的端到端示例程序:

 import java.sql.Date; import java.util.Calendar; import java.util.GregorianCalendar; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; public class TestDateConversions { public static void main(String[] args) { TestDateConversions testDates = new TestDateConversions(); try { XMLGregorianCalendar testDate1 = DatatypeFactory.newInstance().newXMLGregorianCalendar(); testDate1.setYear(0001); testDate1.setMonth(01); testDate1.setDay(01); System.out.println("Start date: "+testDate1.toString() +"\n**********************"); testDates.setXMLGregorianCalendar(testDate1); System.out.println("\nNull given \n"+ "**********"); testDates.setXMLGregorianCalendar(null); } catch(Exception e) { System.out.println(e); } } public void setXMLGregorianCalendar(XMLGregorianCalendar argCal) { GregorianCalendar calendarDate; if (argCal != null) { calendarDate = argCal.toGregorianCalendar(); System.out.println("XMLGregorianCalendar time: " + argCal.getHour() + ":"+argCal.getMinute()+":"+argCal.getSecond()); System.out.println("XMLGregorianCalendar time(ms): "+argCal.getMillisecond()); System.out.println("XMLGregorianCalendar -> GregorianCalendar: "+calendarDate.get(GregorianCalendar.YEAR) + "-"+(calendarDate.get(GregorianCalendar.MONTH)+1) + "-"+calendarDate.get(GregorianCalendar.DAY_OF_MONTH)); System.out.println("!!!!PROBLEM AREA!!!!"); Calendar cal = Calendar.getInstance(); System.out.println("-- New Calendar instance: "+cal.get(Calendar.YEAR) + "-"+(cal.get(Calendar.MONTH)+1)+"-"+cal.get(Calendar.DAY_OF_MONTH)); System.out.println("-- Calling Calendar.setTime(GregorianCalendar.getTime())"); cal.setTime(calendarDate.getTime()); System.out.println("-- calendarDate.getTime() = " + calendarDate.getTime() + " <-- time is incorrect"); System.out.println("-- Calendar with time set from GregorianCalendar: "+cal.get(Calendar.YEAR) + "-"+(cal.get(Calendar.MONTH)+1)+"-"+cal.get(Calendar.DAY_OF_MONTH) + " <-- day is increased here"); setCalendar(cal); } else { setCalendar(null); } } public void setCalendar(Calendar argCal) { if (argCal != null) { Date date = new Date(argCal.getTimeInMillis()); System.out.println("Calendar to Date: "+date); setDate(date); } else { setDate(null); } } public void setDate(Date argDate) { try { if (argDate == null) { Calendar cal = new GregorianCalendar(1,0,1); Date nullDate = new Date(cal.getTimeInMillis()); System.out.println("Null Calendar created: "+cal.get(Calendar.YEAR) + "-"+(cal.get(Calendar.MONTH)+1)+"-"+cal.get(Calendar.DAY_OF_MONTH)); System.out.println("Null Date created: "+nullDate); } else { System.out.println("Final date type: "+argDate); } } catch (Exception ex) { System.out.println(ex); } } } 

摘自XMLGregorianCalendar.toGregorianCalendar() JavaDoc ,了解它们如何创建GregorianCalendar实例:

通过调用GregorianCalendar.setGregorianChange(new Date(Long.MIN_VALUE))获取纯格里高利历。

这意味着,创建的日历将是无意义的,并且不会像旧日期那样切换到Julian日历。 然后问题在这里:

  • argCal.toGregorianCalendar()使用字段表示XMLGregorianCalendar转换为GregorianCalendar (不使用Julian系统 – 见上文)
  • cal.setTime(calendarDate.getTime());
    • 这实际上是将字段表示转换为时间戳表示并使用此时间戳初始化新日历
    • 新日历使用Julian系统来表示日期,因为它早于1582年

解决这个问题的方法很简单:

  • 如果您只对日期感兴趣,请使用JodaTime和LocalDate#fromCalendarFiels
  • 使用字段访问而不是#getTime方法转换日历
  • 强制格里高利历使用proleptic系统(与XMLGregorianCalendar一样)

更新请注意,Java日期和日历API设计得不是很好,而且有时候会很混乱。 这也是为什么Java 8包含完全重写的日期时间库JSR-310 (顺便说一下基于JodaTime)的原因。

现在,您必须意识到,您可以通过两种截然不同的方法存储和使用特定的即时 (日历独立关键字):

  • 从一个名为epoch的明确定义的瞬间存储一个偏移量(例如以毫秒为单位)(例如unix epoch 1970-01-01 )
  • 按日历字段存储日期(例如1970年1月1日)

第一种方法是java.util.Date引用的内容。 然而,这种表示通常是非人类友好的。 人类使用日历日期,而不是时间戳。 将时间戳转换为日期字段是日历的步骤。也就是有趣的部分开始的地方……如果你想通过其字段表示日期,你需要意识到总有多种方法可以做到这一点。 有些国家可以决定使用阴历月,其他人可能会说0年仅仅是10年前。 格里高利历只是将实际即时字段转换为实际日期字段的一种方式。

关于XMLGregorianCalendarGregorianCalendar的一点点:

  • XML规范明确指出人类可读日期是格里高利历日期
  • Java的GregorianCalendar包含这个“魔法” ,如果瞬间比定义的切换日期更早,它将切换到引擎盖下的Julian系统
  • 这就是为什么XMLGregorianCalendar在初始化期间修改GregorianCalendar以禁用这个魔术开关(参见上面的JavaDoc摘录)

现在有趣的部分:

如果Julian开关不会被禁用, GregorianCalendar会假设日历字段来自Julian系统,它会将它们移动3天。 你认为这个日期已经改变了3天,而且肯定会出现问题,对吧? 不,日期实际上一直都是正确的 ,它包含引擎盖下的正确时间戳! 只有日历向你展示了朱利安领域而不是格里高利领域。 我觉得这很令人困惑:) [JSR-310在后台笑]。

因此,如果你想使用纯格里高利历(即在旧日期使用所谓的预感格里高利 ),你需要像这样初始化日历:

 Calendar calendar = Calendar.getInstance(); ((GregorianCalendar) calendar).setGregorianChange(new Date(Long.MIN_VALUE)); 

你可能会说: calendar.getTime()仍然给我错误的日期。 好吧,那是因为java.util.Date.toString() (由System.out.println调用)正在使用默认Calendar ,它将切换到旧日期的Julian系统。 困惑? 也许生气(我知道我是:))?


更新2

 // Get XML gregorian calendar XMLGregorianCalendar xmlCalendar = DatatypeFactory.newInstance().newXMLGregorianCalendar(); xmlCalendar.setYear(1); // Watch for octal number representations (you had there 0001) xmlCalendar.setMonth(1); xmlCalendar.setDay(1); // Convert to Calendar as it is easier to work with it Calendar calendar = xmlCalendar.toGregorianCalendar(); // Proleptic for old dates // Convert to default calendar (will misinterpret proleptic for Julian, but it is a workaround) Calendar result = Calendar.getInstance(); result.setTimeZone(calendar.getTimeZone()); result.set(Calendar.YEAR, calendar.get(Calendar.YEAR)); result.set(Calendar.MONTH, calendar.get(Calendar.MONTH)); result.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH)); result.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY)); result.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE)); result.set(Calendar.SECOND, calendar.get(Calendar.SECOND)); result.set(Calendar.MILLISECOND, calendar.get(Calendar.MILLISECOND)); System.out.println(result.getTime()); 

免责声明: 此代码错误 (结果瞬间与XML文件中的结果不同),但OP了解问题及其后果(请参阅本答案下的讨论)。

看起来好像你不小心从格里高利历转换到朱利安日历。 (外推或延长的 )阳历称为0001-01-01的那一天确实是朱利安日历中的0001-01-03。 如果您将日期更改为1001-01-01,您会发现最终结果是1000-12-26。 这与从格里高利到朱利安的日期转换一致,因为偏移随时间变化。 它在4世纪之前实际上是积极的,但从那以后一直是消极的(随着时间的推移越来越消极)。

我得到的XMLGregorianCalendar的实例实际上是com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl ,而toGregorianCalendar()方法包括以下这些行:

  result = new GregorianCalendar(tz, locale); result.clear(); result.setGregorianChange(PURE_GREGORIAN_CHANGE); 

这使得日历成为纯粹的渐变GregorianCalendar,因此1583年之前的日期仍然通过格里高利算法从纪元时间(自1970年以来的毫秒)转换。 这意味着问题可能出在GregorianCalendar ……

确实如此。 试试这个示例程序,你会看到:

 public static void main(String...args) { GregorianCalendar gcal = new GregorianCalendar(); gcal.clear(); gcal.setGregorianChange(new Date(Long.MIN_VALUE)); gcal.set(Calendar.YEAR, 1); // or any other year before 1582 gcal.set(Calendar.MONTH, Calendar.JANUARY); gcal.set(Calendar.DATE, 1); Date d = gcal.getTime(); System.out.println(d); } 

由于getGregorianChange()的值, getGregorianChange()的字段值被解释为Gregorian,但是出现(从epoch毫秒转换)该字段似乎是不服从的。 getTime()出来的Date没有Gregorian转换值的概念。 这可能是也可能不是错误,但它肯定是出乎意料的行为。