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

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

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

1
2
3
4
5
6
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)的第四个变体似乎无法解析。

如果解决方案能在模型的每个日期/时间字段上都不必添加注释,那么解决方案就可以了。

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


一种选择是创建自定义解串器。首先,您要注释各自的字段:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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,发现了一个较短(但不多)的解决方案:

1
2
3
4
5
6
7
8
9
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,因为此类是不可变的并且是线程安全的。


非常感谢您的所有投入!

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

1
2
3
4
5
6
7
8
9
10
11
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时我们使用正确的格式:

1
2
3
4
5
6
7
8
9
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类,以使其在数据类上没有任何注释地工作:

1
2
3
4
5
6
7
8
9
@Configuration
open class JacksonConfig {

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

这仅是答案的四分之一。我既没有Kotlin的经验,也没有Jackson的经验,但是我想为Java提供一些解决方案。如果您能以某种方式使它们适合整体解决方案,我将感到高兴。

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

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

在Java 8中也可以使用的更好的解决方案(我已经测试过):

1
2
3
4
5
    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匹配。我们需要按从最长到最短的顺序尝试它们,以确保在所有情况下都可以解析所有文本。