为什么Spring在绑定带有@DateTimeFormat(iso=DateTimeFormat.iso.DATE\U time)注释的LocalDateTime时会忽略输入字符串的时间偏移?
我有一个带有以下字段的对象为什么Spring在绑定带有@DateTimeFormat(iso=DateTimeFormat.iso.DATE\U time)注释的LocalDateTime时会忽略输入字符串的时间偏移?,spring,spring-mvc,datetime,java-time,Spring,Spring Mvc,Datetime,Java Time,我有一个带有以下字段的对象活动: @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) private LocalDateTime start; 我正在通过发送以下表单将此对象绑定到控制器方法: @RequestMapping(value = "update", method = RequestMethod.POST) public String submitUpdateActivityForm(Activity activity) {
活动
:
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDateTime start;
我正在通过发送以下表单将此对象绑定到控制器方法:
@RequestMapping(value = "update", method = RequestMethod.POST)
public String submitUpdateActivityForm(Activity activity) {
activityRepository.save(activity);
return "successPage;
}
我使用的是Spring Boot 1.5.1、Spring MVC 4.3.6,在我的web应用程序中,我希望从任何时区的客户端接收时间,但始终保持LocalDateTime
为UTC。但当Spring将表单中的对象绑定到控制器中的请求参数时,它会完全忽略类型为LocalDateTime
的属性的输入字符串的时间偏移量
我想,根据@DateTimeFormat(iso=DateTimeFormat.iso.DATE\u TIME)的文档,
Spring将在字符串中找到偏移量,并将输入的日期时间从给定的时区转换为我的服务器的时区(UTC),然后绑定
例如:我希望字符串2017-05-31T12:00-03:00
转换为LocalDateTime'2017-05-31T15:00'
和2017-05-31T12:00Z
被解释为LocalDateTime'2017-05-31T12:00',
。不幸的是,无论时间偏移量是多少,我总是得到LocalDateTime
@DateTimeFormat
是正确的行为还是我做错了什么?我应该实现Spring转换器
还是扩展属性编辑器支持
这是处理时间的正确方法吗?我想接受任何类型的日期,但将它们保留在
LocalDateTime
中的UTC中,因为这样我就可以轻松地将它们发送到客户端,在那里我可以将它们从UTC转换为客户端的本地时区并显示。LocalDateTime
类没有任何时区/偏移信息。因此我怀疑问题在于(尽管我没有在Spring环境中进行测试):Spring将字符串解析为OffsetDateTime
(因为格式包含偏移量,比如-03:00
)并获取其本地日期时间部分(去掉偏移量)。或者做其他类似的事情,但忽略偏移量
执行此操作时,不会进行时间转换。因此,我认为最好的解决方案是将字段更改为OffsetDateTime
或Instant
(请参阅下面的更多详细信息)。如果不可能,您可以使用以下代码将此OffsetDateTime
转换为LocalDateTime
:
// convert OffsetDateTime to LocalDateTime (converting the time to UTC)
LocalDateTime localDateTime = OffsetDateTime.parse("2017-05-31T12:00-03:00")
// change to UTC ("sameInstant" converts the time)
.withOffsetSameInstant(ZoneOffset.UTC)
// get only localdatetime part (without offset)
.toLocalDateTime();
System.out.println(localDateTime);
withOffsetSameInstant(ZoneOffset.UTC)
将时间转换为UTC。因此,上述代码的输出为:
2017-05-31T15:00
您还可以使用相同的代码解析UTC字符串:
localDateTime = OffsetDateTime.parse("2017-05-31T12:00Z")
.withOffsetSameInstant(ZoneOffset.UTC)
.toLocalDateTime();
System.out.println(localDateTime);
请注意,在这种情况下,withOffsetSameInstant(ZoneOffset.UTC)
是多余的,因为字符串已经在UTC()中。但是你可以毫无问题地离开它。
输出将是:
2017-05-31T12:00
错误原因可能是什么 请注意,如果不将
与OffsetSameInstant一起使用,则会得到不正确的结果:
localDateTime = OffsetDateTime.parse("2017-05-31T12:00-03:00").toLocalDateTime();
System.out.println(localDateTime); // 2017-05-31T12:00 (12h instead of 15h)
我怀疑Spring就是这么做的。或者,它可能正在使用忽略偏移量的解析器解析LocalDateTime
——与此非常类似:
System.out.println(LocalDateTime.parse("2017-05-31T12:00-03:00",
DateTimeFormatter.ISO_DATE_TIME));
// output is 2017-05-31T12:00
无论如何,Spring忽略了偏移量。您可以尝试编写转换器(使用上面描述的代码)或使用下面描述的方法
处理时区时的更好方法
在我看来,在处理可以处理多个时区和偏移的日期/时间时,使用LocalDateTime
并不是最好的方法。这是因为LocalDateTime
没有任何时区/偏移量信息,无法正确处理
我认为在这种情况下最好的方法是使用OffsetDateTime
或Instant
。基于以下原因,我认为Instant
是最好的选择
如果选择将字段类型更改为OffsetDateTime
,则可以使用withOffsetSameInstant(ZoneOffset.UTC)
将其转换为UTC:
要将此OffsetDateTime
转换为另一个时区并返回UTC,您可以执行以下操作:
OffsetDateTime utcOffset = OffsetDateTime.parse("2017-05-31T12:00-03:00").withOffsetSameInstant(ZoneOffset.UTC);
System.out.println(utcOffset); // 2017-05-31T15:00Z
// convert to London timezone
ZonedDateTime z = utcOffset.atZoneSameInstant(ZoneId.of("Europe/London"));
System.out.println(z); // 2017-05-31T16:00+01:00[Europe/London]
// convert back to UTC
System.out.println(z.withZoneSameInstant(ZoneOffset.UTC)); // 2017-05-31T15:00Z
请注意,UTC的15小时在伦敦是16小时,因为5月份是英国的夏季,API会自动处理
如果选择使用即时
(UTC即时,独立于时区/偏移),则可以通过以下方式对其进行解析:
// 2017-05-31T15:00:00Z
System.out.println(OffsetDateTime.parse("2017-05-31T12:00-03:00").toInstant());
// 2017-05-31T12:00:00Z
System.out.println(OffsetDateTime.parse("2017-05-31T12:00Z").toInstant());
如果您想明确说明日期/时间是UTC,我相信Instant
是最佳选择。如果使用LocalDateTime
,则不清楚它在哪个时区(实际上,因为此类没有此类信息,从技术上讲,它不在任何时区),并且必须记住(或将此信息存储在其他任何地方)。使用Instant
,UTC的用法是明确的
使用Instant
,从另一个时区到另一个时区的转换非常容易:
// UTC instant (2017-05-31T15:00:00Z)
Instant instant = OffsetDateTime.parse("2017-05-31T12:00-03:00").toInstant();
// converts to London timezone
ZonedDateTime london = instant.atZone(ZoneId.of("Europe/London"));
System.out.println(london); // 2017-05-31T16:00+01:00[Europe/London]
// ** note that 15h in UTC is 16h in London, because in May it's British's Summer Time
// converts back to UTC instant
System.out.println(london.toInstant()); // 2017-05-31T15:00:00Z
请注意,伦敦的夏季时间也适用
将Instant
转换为OffsetDateTime
也很简单:
// converts to offset +05:00
OffsetDateTime odt = instant.atOffset(ZoneOffset.ofHours(5));
System.out.println(odt); // 2017-05-31T20:00+05:00
// converts back to UTC instant
System.out.println(odt.toInstant()); // 2017-05-31T15:00:00Z
但如果您使用LocalDateTime
,则在处理某些情况时并不明显:
// LocalDateTime (2017-05-31T15:00)
LocalDateTime dt = OffsetDateTime.parse("2017-05-31T12:00-03:00")
.withOffsetSameInstant(ZoneOffset.UTC)
.toLocalDateTime();
// converts to London timezone (wrong way)
ZonedDateTime wrongLondon = dt.atZone(ZoneId.of("Europe/London"));
System.out.println(wrongLondon); // 2017-05-31T15:00+01:00[Europe/London] (hour is 15 instead of 16)
// converts to London timezone (right way: first convert to UTC, then to London)
ZonedDateTime correctLondon = dt.atZone(ZoneOffset.UTC).withZoneSameInstant(ZoneId.of("Europe/London"));
System.out.println(correctLondon); // 2017-05-31T16:00+01:00[Europe/London]
PS:当我说“错误的方式”时,我的意思是“你的情况是错误的”。如果我的本地时间是10小时,并且我想创建一个表示“伦敦时区10小时”的对象,localDateTime.atZone()。但在您的情况下,10小时是UTC时间。但由于对象是本地对象,因此在转换到另一时区之前,需要将其转换为UTC。这就是为什么LocalDateTime
不适合您的情况
当转换回LocalDateTime
时,在获取本地部分之前,必须注意转换为UTC:
// wrong: 2017-05-31T16:00
System.out.println(correctLondon.toLocalDateTime());
// correct: 2017-05-31T15:00
System.out.println(correctLondon.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime());
因此,IMO使用Instant
更为直接,因为UTC的使用是明确的,并且与其他时区的转换也很容易。但是如果您不能更改字段的类型,您可以转换Loc
// wrong: 2017-05-31T16:00
System.out.println(correctLondon.toLocalDateTime());
// correct: 2017-05-31T15:00
System.out.println(correctLondon.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime());