如何使用 Jackson 和 java.time 解析不同的 ISO date/time 格式?

How to parse different ISO date/time formats with Jackson and java.time?

Our 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

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

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

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

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

一种替代方法是创建自定义反序列化器。首先注释相应的字段:

@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);
    }
}

我还使用了 built-in 常量 DateTimeFormatter.ISO_LOCAL_DATE_TIME 因为它处理可选的秒数 - 小数位数似乎也是可变的,而这个 built-in 格式化程序已经为您处理了这些细节。


我正在使用 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 finalbecause this class is immutable and thread-safe.

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

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

在我的 Java 9 (9.0.4) 上,one-arg 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。我们需要按照从最长到最短的顺序尝试它们,以确保在所有情况下都能解析所有文本。

非常感谢您的所有意见!

我选择了 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 class 路径中添加了一个 @Configuraton class 以使其在没有任何数据注释的情况下工作 classes:

@Configuration
open class JacksonConfig {

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