如何使用 Jackson 和 java.time 解析不同的 ISO 日期/时间格式?

Posted

技术标签:

【中文标题】如何使用 Jackson 和 java.time 解析不同的 ISO 日期/时间格式?【英文标题】:How to parse different ISO date/time formats with Jackson and java.time? 【发布时间】:2018-08-29 15:39:11 【问题描述】:

我们的 Rest API 从多个外部方获取 JSON 输入。它们都使用“ISO-ish”格式,但时区偏移的格式略有不同。以下是我们看到的一些最常见的格式:

2018-01-01T15:56:31.410Z
2018-01-01T15:56:31.41Z
2018-01-01T15:56:31Z
2018-01-01T15:56:31+00:00
2018-01-01T15:56:31+0000
2018-01-01T15:56:31+00

我们的堆栈是带有 Jackson ObjectMapper 的 Spring Boot 2.0。在我们的数据类中,我们经常使用java.time.OffsetDateTime 类型。

几个开发者已经尝试实现一个解析所有上述格式的解决方案,但没有一个成功。特别是带有冒号的第四个变体 (00:00) 似乎无法解析。

如果解决方案无需在我们模型的每个日期/时间字段上放置注释即可工作,那就太好了。

尊敬的社区,您有解决方案吗?

【问题讨论】:

ISO 8601 有一些变化空间。我相信你所有的例子都符合。 【参考方案1】:

非常感谢您的所有意见!

我选择了 jeedas 建议的反序列化器和 Ole V.V 建议的格式化程序(因为它更短)。

class DefensiveIsoOffsetDateTimeDeserializer : JsonDeserializer<OffsetDateTime>() 
    private val formatter = DateTimeFormatterBuilder()
        .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
        .appendPattern("[XXX][XX][X]")
        .toFormatter()

    override fun deserialize(p: JsonParser, ctxt: DeserializationContext) 
      = OffsetDateTime.parse(p.text, formatter)

    override fun handledType() = OffsetDateTime::class.java

我还添加了一个自定义序列化程序,以确保我们在生成 json 时使用正确的格式:

class OffsetDateTimeSerializer: JsonSerializer<OffsetDateTime>() 
    override fun serialize(
        value: OffsetDateTime, 
        gen: JsonGenerator, 
        serializers: SerializerProvider
    ) = gen.writeString(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))

    override fun handledType() = OffsetDateTime::class.java

将所有部分放在一起,我在我的 spring 类路径中添加了一个 @Configuraton 类,以使其在数据类上没有任何注释的情况下工作:

@Configuration
open class JacksonConfig 

  @Bean
  open fun jacksonCustomizer() = Jackson2ObjectMapperBuilderCustomizer  
    it.deserializers(DefensiveIsoOffsetDateTimeDeserializer())
    it.serializers(OffsetDateTimeSerializer())
  

【讨论】:

【参考方案2】:

另一种方法是创建自定义反序列化程序。首先,您注释相应的字段:

@JsonDeserialize(using = OffsetDateTimeDeserializer.class)
private OffsetDateTime date;

然后你创建反序列化器。它使用java.time.format.DateTimeFormatterBuilder,使用大量可选部分来处理所有不同类型的偏移:

public class OffsetDateTimeDeserializer extends JsonDeserializer<OffsetDateTime> 

    private DateTimeFormatter fmt = new DateTimeFormatterBuilder()
        // date/time
        .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
        // offset (hh:mm - "+00:00" when it's zero)
        .optionalStart().appendOffset("+HH:MM", "+00:00").optionalEnd()
        // offset (hhmm - "+0000" when it's zero)
        .optionalStart().appendOffset("+HHMM", "+0000").optionalEnd()
        // offset (hh - "+00" when it's zero)
        .optionalStart().appendOffset("+HH", "+00").optionalEnd()
        // offset (pattern "X" uses "Z" for zero offset)
        .optionalStart().appendPattern("X").optionalEnd()
        // create formatter
        .toFormatter();

    @Override
    public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException 
        return OffsetDateTime.parse(p.getText(), fmt);
    

我还使用了内置常量DateTimeFormatter.ISO_LOCAL_DATE_TIME,因为它处理了可选的秒数小数部分——而且小数位数似乎也是可变的,而且这个内置格式化程序已经处理了这些细节给你。


我正在使用 JDK 1.8.0_144 并找到了一个更短(但不多)的解决方案:

private DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // date/time
    .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
    // offset +00:00 or Z
    .optionalStart().appendOffset("+HH:MM", "Z").optionalEnd()
    // offset +0000, +00 or Z
    .optionalStart().appendOffset("+HHmm", "Z").optionalEnd()
    // create formatter
    .toFormatter();

您可以进行的另一项改进是将格式化程序更改为static final、because this class is immutable and thread-safe。

【讨论】:

【参考方案3】:

这只是答案的四分之一。我既没有使用 Kotlin 也没有使用过 Jackson,但我有几个 Java 解决方案我想贡献一下。如果您能以某种方式将它们放入一个完整的解决方案中,我会很高兴。

    String modifiedEx = ex.replaceFirst("(\\d2)(\\d2)$", "$1:$2");
    System.out.println(OffsetDateTime.parse(modifiedEx));

在我的 Java 9 (9.0.4) 上,单参数 OffsetDateTime.parse 解析所有示例字符串,除了偏移量为 +0000 且不带冒号的字符串。所以我的技巧是插入那个冒号然后解析。上面解析了你所有的字符串。它在 Java 8 中不容易工作(从 Java 8 到 Java 9 有一些变化)。

也适用于 Java 8 的更好的解决方案(我已经测试过):

    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
            .appendPattern("[XXX][XX][X]")
            .toFormatter();
    System.out.println(OffsetDateTime.parse(ex, formatter));

模式XXXXXX 分别匹配+00:00+0000+00。我们需要按从最长到最短的顺序尝试它们,以确保在所有情况下都解析所有文本。

【讨论】:

如果可能的话,我不想使用正则表达式替换东西......但你的第二个解决方案效果很好! 我说这是一个黑客。很高兴您更喜欢第二个 sn-p。

以上是关于如何使用 Jackson 和 java.time 解析不同的 ISO 日期/时间格式?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确地在Spring Data JPA和Jackson中用上Java 8的时间相关API(即JSR 310也即java.time包下的众神器)

使用 Spring Boot 2 和 Kotlin 进行 Jackson 反序列化,无法构造 `java.time.LocalDate` 的实例

Jackson , java.time , ISO 8601 , 无毫秒序列化

Spring Boot 问题使用 Jackson 序列化 java.time.LocalDateTime 以返回 ISO-8601 JSON 时间戳?

如何将 graphql-java-extended-scalars DateTime 与 Jackson 一起使用

是否有任何选项可以在 Spring Boot 中使用 Jackson 为 java.time.* 包注册一次 Serializer/Deserializer?