如何将具有偏移量(“2019-01-29+01:00”)的日期反序列化为 `java.time` 相关 类?
How to deserialize dates with offset ("2019-01-29+01:00") to `java.time` related classes?
我重构了 Spring Boot (2.1.2) 系统中的一些遗留代码,并从 java.util.Date
迁移到基于 java.time
的 classes (jsr310)。系统需要 ISO8601 格式字符串中的日期,而有些是带有时间信息的完整时间戳(例如 "2019-01-29T15:29:34+01:00"
),而其他只是带有偏移量的日期(例如 "2019-01-29+01:00"
)。这是 DTO(作为 Kotlin 数据 class):
data class Dto(
// ...
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
@JsonProperty("processingTimestamp")
val processingTimestamp: OffsetDateTime,
// ...
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddXXX")
@JsonProperty("orderDate")
val orderDate: OffsetDateTime,
// ...
)
虽然 Jackson 完美地反序列化了 processingTimestamp
,但它失败了 orderDate
:
Caused by: java.time.DateTimeException: Unable to obtain OffsetDateTime from TemporalAccessor: {OffsetSeconds=32400},ISO resolved to 2018-10-23 of type java.time.format.Parsed
at java.time.OffsetDateTime.from(OffsetDateTime.java:370) ~[na:1.8.0_152]
at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:207) ~[jackson-datatype-jsr310-2.9.8.jar:2.9.8]
这对我来说很有意义,因为 OffsetDateTime
找不到构建瞬间所需的任何时间信息。如果我改成 val orderDate: LocalDate
Jackson 可以成功反序列化,但是偏移信息就没有了(我需要稍后转换成 Instant
)。
问题
我目前的解决方法是结合使用 OffsetDateTime
和自定义反序列化器(见下文)。 但我想知道,是否有更好的解决方案?
此外,我希望有更合适的数据类型,例如 OffsetDate,但我在 java.time
.
中找不到它
PS
我在问自己“2019-01-29+01:00”是否对 ISO8601 有效。但是,由于我发现 java.time.DateTimeFormatter.ISO_DATE
可以正确解析它并且我无法更改客户端发送数据的格式,所以我搁置了这个问题。
解决方法
data class Dto(
// ...
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddXXX")
@JsonProperty("catchDate")
@JsonDeserialize(using = OffsetDateDeserializer::class)
val orderDate: OffsetDateTime,
// ...
)
class OffsetDateDeserializer(
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE
) : JSR310DateTimeDeserializerBase<OffsetDateTime>(OffsetDateTime::class.java, formatter) {
override fun deserialize(parser: JsonParser, context: DeserializationContext): OffsetDateTime? {
if (parser.hasToken(JsonToken.VALUE_STRING)) {
val string = parser.text.trim()
if (string.isEmpty()) {
return null
}
val parsed: TemporalAccessor = formatter.parse(string)
val offset = if(parsed.isSupported(ChronoField.OFFSET_SECONDS)) ZoneOffset.from(parsed) else ZoneOffset.UTC
val localDate = LocalDate.from(parsed)
return OffsetDateTime.of(localDate.atStartOfDay(), offset)
}
throw context.wrongTokenException(parser, _valueClass, parser.currentToken, "date with offset must be contained in string")
}
override fun withDateFormat(otherFormatter: DateTimeFormatter?): JsonDeserializer<OffsetDateTime> = OffsetDateDeserializer(formatter)
}
正如@JodaStephen 在评论中解释的那样,OffsetDate 未包含在 java.time
中以具有最小的 类 集。所以,OffsetDateTime
是最好的选择。
他还建议使用DateTimeFormatterBuilder
和parseDefaulting
创建一个DateTimeFormatter
实例,直接从格式化程序解析结果(TemporalAccessor
创建OffsetDateTime
). AFAIK,我仍然需要创建一个自定义反序列化器来使用格式化程序。这是解决了我的问题的代码:
class OffsetDateDeserializer: JsonDeserializer<OffsetDateTime>() {
private val formatter = DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_DATE)
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
.parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
.toFormatter()
override fun deserialize(parser: JsonParser, context: DeserializationContext): OffsetDateTime? {
if (parser.hasToken(JsonToken.VALUE_STRING)) {
val string = parser.text.trim()
if (string.isEmpty()) {
return null
}
try {
return OffsetDateTime.from(formatter.parse(string))
} catch (e: DateTimeException){
throw context.wrongTokenException(parser, OffsetDateTime::class.java, parser.currentToken, "error while parsing date: ${e.message}")
}
}
throw context.wrongTokenException(parser, OffsetDateTime::class.java, parser.currentToken, "date with offset must be contained in string")
}
}
我重构了 Spring Boot (2.1.2) 系统中的一些遗留代码,并从 java.util.Date
迁移到基于 java.time
的 classes (jsr310)。系统需要 ISO8601 格式字符串中的日期,而有些是带有时间信息的完整时间戳(例如 "2019-01-29T15:29:34+01:00"
),而其他只是带有偏移量的日期(例如 "2019-01-29+01:00"
)。这是 DTO(作为 Kotlin 数据 class):
data class Dto(
// ...
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
@JsonProperty("processingTimestamp")
val processingTimestamp: OffsetDateTime,
// ...
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddXXX")
@JsonProperty("orderDate")
val orderDate: OffsetDateTime,
// ...
)
虽然 Jackson 完美地反序列化了 processingTimestamp
,但它失败了 orderDate
:
Caused by: java.time.DateTimeException: Unable to obtain OffsetDateTime from TemporalAccessor: {OffsetSeconds=32400},ISO resolved to 2018-10-23 of type java.time.format.Parsed
at java.time.OffsetDateTime.from(OffsetDateTime.java:370) ~[na:1.8.0_152]
at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:207) ~[jackson-datatype-jsr310-2.9.8.jar:2.9.8]
这对我来说很有意义,因为 OffsetDateTime
找不到构建瞬间所需的任何时间信息。如果我改成 val orderDate: LocalDate
Jackson 可以成功反序列化,但是偏移信息就没有了(我需要稍后转换成 Instant
)。
问题
我目前的解决方法是结合使用 OffsetDateTime
和自定义反序列化器(见下文)。 但我想知道,是否有更好的解决方案?
此外,我希望有更合适的数据类型,例如 OffsetDate,但我在 java.time
.
PS
我在问自己“2019-01-29+01:00”是否对 ISO8601 有效。但是,由于我发现 java.time.DateTimeFormatter.ISO_DATE
可以正确解析它并且我无法更改客户端发送数据的格式,所以我搁置了这个问题。
解决方法
data class Dto(
// ...
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddXXX")
@JsonProperty("catchDate")
@JsonDeserialize(using = OffsetDateDeserializer::class)
val orderDate: OffsetDateTime,
// ...
)
class OffsetDateDeserializer(
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE
) : JSR310DateTimeDeserializerBase<OffsetDateTime>(OffsetDateTime::class.java, formatter) {
override fun deserialize(parser: JsonParser, context: DeserializationContext): OffsetDateTime? {
if (parser.hasToken(JsonToken.VALUE_STRING)) {
val string = parser.text.trim()
if (string.isEmpty()) {
return null
}
val parsed: TemporalAccessor = formatter.parse(string)
val offset = if(parsed.isSupported(ChronoField.OFFSET_SECONDS)) ZoneOffset.from(parsed) else ZoneOffset.UTC
val localDate = LocalDate.from(parsed)
return OffsetDateTime.of(localDate.atStartOfDay(), offset)
}
throw context.wrongTokenException(parser, _valueClass, parser.currentToken, "date with offset must be contained in string")
}
override fun withDateFormat(otherFormatter: DateTimeFormatter?): JsonDeserializer<OffsetDateTime> = OffsetDateDeserializer(formatter)
}
正如@JodaStephen 在评论中解释的那样,OffsetDate 未包含在 java.time
中以具有最小的 类 集。所以,OffsetDateTime
是最好的选择。
他还建议使用DateTimeFormatterBuilder
和parseDefaulting
创建一个DateTimeFormatter
实例,直接从格式化程序解析结果(TemporalAccessor
创建OffsetDateTime
). AFAIK,我仍然需要创建一个自定义反序列化器来使用格式化程序。这是解决了我的问题的代码:
class OffsetDateDeserializer: JsonDeserializer<OffsetDateTime>() {
private val formatter = DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_DATE)
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
.parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
.toFormatter()
override fun deserialize(parser: JsonParser, context: DeserializationContext): OffsetDateTime? {
if (parser.hasToken(JsonToken.VALUE_STRING)) {
val string = parser.text.trim()
if (string.isEmpty()) {
return null
}
try {
return OffsetDateTime.from(formatter.parse(string))
} catch (e: DateTimeException){
throw context.wrongTokenException(parser, OffsetDateTime::class.java, parser.currentToken, "error while parsing date: ${e.message}")
}
}
throw context.wrongTokenException(parser, OffsetDateTime::class.java, parser.currentToken, "date with offset must be contained in string")
}
}