簡體   English   中英

使用 objectMapper 將 JSON 日期格式反序列化為 ZonedDateTime

[英]Deserialize JSON date format to ZonedDateTime using objectMapper

背景

  1. 我有以下 JSON(來自 Kafka 的消息)
{
      "markdownPercentage": 20,
      "currency": "SEK",
      "startDate": "2019-07-25"
}
  1. 我有以下(生成 JSON 模式)POJO(我無法更改 POJO,因為它是公司的共享資源)
public class Markdown {
    @JsonProperty("markdownPercentage")
    @NotNull
    private Integer markdownPercentage = 0;
    @JsonProperty("currency")
    @NotNull
    private String currency = "";
    @JsonFormat(
        shape = Shape.STRING,
        pattern = "yyyy-MM-dd"
    )
    @JsonProperty("startDate")
    @NotNull
    private ZonedDateTime startDate;

    // Constructors, Getters, Setters etc.

}
  1. 我們的應用程序是一個 Spring Boot 應用程序,它使用 Spring Cloud Stream 從 Kafka 讀取 JSON 消息 (1) 並使用 POJO (2),然后對其進行處理。

問題

當應用程序嘗試將消息反序列化到對象時,它會引發以下異常

com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.ZonedDateTime` from String "2019-07-25": Failed to deserialize java.time.ZonedDateTime: (java.time.DateTimeException) Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2019-07-25 of type java.time.format.Parsed
 at [Source: (String)"{"styleOption":"so2_GreyMelange_1563966403695_1361997740","markdowns":[{"markdownPercentage":20,"currency":"SEK","startDate":"2019-07-25"},{"markdownPercentage":20,"currency":"NOK","startDate":"2019-07-25"},{"markdownPercentage":20,"currency":"CHF","startDate":"2019-07-25"}]}"; line: 1, column: 126] (through reference chain: com.bestseller.generated.interfacecontracts.kafkamessages.pojos.markdownScheduled.MarkdownScheduled["markdowns"]->java.util.ArrayList[0]->com.bestseller.generated.interfacecontracts.kafkamessages.pojos.markdownScheduled.Markdown["startDate"])

    at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
    at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1549)
    at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:911)
    at com.fasterxml.jackson.datatype.jsr310.deser.JSR310DeserializerBase._handleDateTimeException(JSR310DeserializerBase.java:80)
    at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:212)
    at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:50)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:286)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:245)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
    at com.bestseller.mps.functional.TestingConfiguration.test(TestingConfiguration.java:42)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2019-07-25 of type java.time.format.Parsed
    at java.base/java.time.ZonedDateTime.from(ZonedDateTime.java:566)
    at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:207)
    ... 35 more
Caused by: java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: {},ISO resolved to 2019-07-25 of type java.time.format.Parsed
    at java.base/java.time.ZoneId.from(ZoneId.java:463)
    at java.base/java.time.ZonedDateTime.from(ZonedDateTime.java:554)
    ... 36 more

當前代碼

我定義了以下 objectMapper

/**
     * Date mapper.
     *
     * @return the {@link ObjectMapper}
     */
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
        return mapper;
    }

我知道 POJO 中生成的 ZonedDateTime 需要一個源消息中不存在的“時間”元素。 我只能控制 objectMapper。 是否有任何可能的配置可以使這項工作?

筆記

如果反序列化的 POJO 中的時間元素“假定”為 startOfDay,即“00.00.00.000Z”,我很好

我只能控制ObjectMapper 是否有任何可能的配置可以使這項工作?

只要您對時間和時區的默認值感到滿意,您就可以使用自定義解串器來解決它:

public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {

    @Override
    public ZonedDateTime deserialize(JsonParser jsonParser,
                                     DeserializationContext deserializationContext)
                                     throws IOException {

        LocalDate localDate = LocalDate.parse(
                jsonParser.getText(), 
                DateTimeFormatter.ISO_LOCAL_DATE);

        return localDate.atStartOfDay(ZoneOffset.UTC);
    }
}

然后將其添加到模塊並將該模塊注冊到您的ObjectMapper實例:

SimpleModule module = new SimpleModule();
module.addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer());

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);

如果將解串器添加到模塊不適合您(從某種意義上說,此配置將應用於其他ZonedDateTime實例),那么您可以依靠混合來定義解串器將應用於哪些字段。 首先定義一個mix-in接口,如下圖:

public interface MarkdownMixIn {

    @JsonDeserialize(using = ZonedDateTimeDeserializer.class)
    ZonedDateTime getDate();
}

然后將 mix-in 接口綁定到所需的類:

ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Markdown.class, MarkdownMixIn.class);

遺憾的是,如果不將 POJO 的類型更改為 LocalDate,那將很困難。

我能想到的最接近的解決方案是為 Jackson 編寫一個自定義的JsonDeserializer ,這對於這種事情絕對不是一個好習慣。

https://fasterxml.github.io/jackson-databind/javadoc/2.3.0/com/fasterxml/jackson/databind/JsonDeserializer.html

您可以編寫自己的反序列化程序,如@cassiomolin答案中所示。 但也有另一種選擇。 在堆棧跟蹤中,我們有DeserializationContext.weirdStringException方法,它允許我們為DeserializationProblemHandler提供處理奇怪字符串值的方法。 見下面的例子:

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;

public class AppJson {

    public static void main(String[] args) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        // override default time zone if needed
        mapper.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));

        mapper.registerModule(new JavaTimeModule());
        mapper.addHandler(new DeserializationProblemHandler() {
            @Override
            public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType,
                String valueToConvert, String failureMsg) {
                LocalDate date = LocalDate.parse(valueToConvert, DateTimeFormatter.ISO_DATE);
                return date.atStartOfDay(ctxt.getTimeZone().toZoneId());
            }
        });
        String json = "{\"startDate\": \"2019-07-25\"}";
        Markdown markdown = mapper.readValue(json, Markdown.class);
        System.out.println(markdown);
    }
}

上面的代碼打印:

Markdown{startDate=2019-07-25T00:00-07:00[America/Los_Angeles]}

問題:我想將日期從 json 解析為 java LocalDateTime/ZonedDateTime 對象。 ZonedDateTimeSerializer 存在,但 ZonedDateTimeDeserializer 不存在。 因此,我創建了一個自定義 ZonedDateTimeDeserializer。

  public static final String ZONED_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSz"; 

  @Getter
  @Setter
  @JsonSerialize(using = ZonedDateTimeSerializer.class)
  @JsonDeserialize(using = ZonedDateTimeDeserializer.class) // Doesn't exist, So I created a custom ZonedDateDeserializer utility class.
  @JsonFormat(pattern = ZONED_DATE_TIME_FORMAT)
  @JsonProperty("lastUpdated")
  private ZonedDateTime lastUpdated;

解決方案:我最終得到了更簡單和更少的代碼行。

用於反序列ZonedDateTime的實用程序類:

/**
 * Custom {@link ZonedDateTime} deserializer.
 *
 * @param jsonParser             for extracting the date in {@link String} format.
 * @param deserializationContext for the process of deserialization a single root-level value.
 * @return {@link ZonedDateTime} object of the date.
 * @throws IOException throws I/O exceptions.
 */
public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {

    @Override
    public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
            throws IOException {

        return ZonedDateTime.parse(jsonParser.getText(), DateTimeFormatter.ofPattern(ZONED_DATE_TIME_FORMAT));
    }
}

如果您改用LocalDateTime會怎樣。 在這種情況下,它更容易,反序列化器和序列化器類已經提供給我們。 不需要上面定義的自定義實用程序類:

  @Getter
  @Setter
  @JsonSerialize(using = LocalDateSerializer.class)
  @JsonDeserialize(using = LocalDateTimeDeserializer.class)
  @JsonFormat(pattern = ZONED_DATE_TIME_FORMAT) //Specify the format you want: "yyyy-MM-dd'T'HH:mm:ss.SSS"
  @JsonProperty("created")
  private LocalDateTime created;

也有助於研究的其他鏈接: 導致此解決方案的想法

關鍵詞:json 格式 localDateTime zonedDateTime

不幸的是,默認情況下您不能將String Object反序列化為ZonedDateTime格式。 但是您可以通過兩種方式克服這個問題。

方式01

POJOZonedDateTime類型更改為LocalDate類型並將值作為字符串傳遞03-06-2012

方式02

但是如果您必須使用時區存儲日期和時間,那么您必須執行以下方法來克服

步驟 01

使用 DateTimeFormat 為ZonedDateTime反序列化創建一個類

public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {

    @Override
    public ZonedDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {

        DateTimeFormatter dateTimeFormatter=DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss z");
        LocalDate localDate = LocalDate.parse(p.getText(),dateTimeFormatter);

        return localDate.atStartOfDay(ZoneOffset.UTC);
    }
}

步驟 02

@JsonDeserialize方法級別注釋的支持下,您必須將該 Deserialize 類與 POJO 類中的受影響字段一起使用。

@JsonDeserialize(using = ZonedDateTimeDeserializer.class)
private ZonedDateTime startDate;

步驟 03

在 ZonedDateTimeDeserializer 類中以上述格式將值作為字符串傳遞

       "startDate" : "09-03-2003 10:15:00 Europe/Paris"

您需要將您的String變量startDate轉換為ZonedDateTimne然后它將被轉換並保存到您的數據庫或其他任何地方。

由於即將到來的 json startDate 是字符串格式,並且您已經說過您不能更改 POJO,您需要在將其分配給private ZonedDateTime startDate;之前對其進行轉換private ZonedDateTime startDate;

你可以像下面的例子一樣這樣做:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss a z");
ZonedDateTime dateTime = ZonedDateTime.parse("2019-03-27 10:15:30 AM +05:30", formatter);
System.out.println(dateTime);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM