解決json字符串轉為對象時LocalDateTime異常問題


1 出現異常

這次的異常出現在前端向后端發送請求體里帶了兩個日期,在后端的實體類中,這兩個日期的格式都是JDK8中的時間類LocalDateTime。默認情況下,LocalDateTime只能解析2020-01-01T10:00:00這樣標准格式的字符串,這里日期和時間中間有一個T。如果不做任何修改的話,LocalDateTime直接解析2020-05-01 08:00:00這種我們習慣上能接受的日期格式,會拋出異常。

請求

異常信息:

org.springframework.http.converter.HttpMessageNotReadableException: Invalid JSON input: Cannot deserialize value of type `java.time.LocalDateTime` from String "2020-05-04 00:00": Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) Text '2020-05-04 00:00' could not be parsed at index 10; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.LocalDateTime` from String "2020-05-04 00:00": Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) Text '2020-05-04 00:00' could not be parsed at index 10

// 省略部分異常信息

Caused by: java.time.format.DateTimeParseException: Text '2020-05-04 00:00' could not be parsed at index 10

// 省略部分異常信息

從異常信息中,我們可以看到2020-05-04 00:00解析到索引為10的位置出現問題,因為這里第10位是一個空格,而LocalDateTime的標准格式里第10位是一個T。

2 問題描述

現在的問題是:

  • 后端使用LocalDateTime類。LocalDateTime類相比於之前的Date類,存在哪些優點,網上的資料已經非常詳盡。
  • 前端傳回的數據,可能是yyyy-MM-dd HH:mm:ss,也可能是yyyy-MM-dd HH:mm,但肯定不會是yyyy-MM-ddTHH:mm:ss。也就是說,前端傳回的日期格式是不確定的,可能是年月日時分秒,可能是年月日時分,還可能是其他任何一般人會用到的日期格式。但顯然不會是年月日T時分秒,因為這樣前端需要額外的轉換,且完全不符合人類的使用習慣。

3 嘗試過的方法

我的SpringBoot版本是2.2.5。

3.1 @JsonFormat

在實體類的字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

這個方法可以解決問題,缺點是要給每個出現的地方都加上注解,無法做全局配置,而且只能設定一種格式,不能滿足我的需求。

3.2 注冊Converter<String, LocalDateTime>的實現類成為bean

結果:沒有生效。這個方法解決controller層的方法的@RequestParam參數的轉化倒是有效。

后來發現這個方案是給控制層方法的參數使用的。也就是下面這種場景:

@GetMapping("/test")
public void test(@RequestParam("time") LocalDateTime time){
    // 省略代碼
}

3.3 注冊Formatter<LocalDateTime>的實現類成為bean

結果:沒有生效。

后來發現這個方案也是給控制層方法參數使用的。

4 解決問題

參考資料:springboot中json轉換LocalDateTime失敗的bug解決過程

首先,我們要知道,SpringBoot默認使用的是Jackson進行序列化。從博客中我們可以了解到,將JSON字符串里的日期從字符串格式轉換成LocalDateTime類的工作是由com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer類的deserialize()方法完成的。這一點可以通過斷點調試確認

jackson的LocalDateTimeDeserializer

解決思路是用自定義的反序列化器替換掉jackson里面的反序列化器,在解析的時候使用自己定義的解析邏輯。

在這里,序列化(serialize)是指將Java對象轉成json字符串的操作,而反序列化(deserialize)指將json字符串解析成Java對象的操作。現在要解決的是反序列化問題。

4.1 實體類

public class LeaveApplication {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Long proposerUsername;
    // LocalDateTime類
    private LocalDateTime startTime;
    // LocalDateTime類
    private LocalDateTime endTime;
    private String reason;
    private String state;
    private String disapprovedReason;
    private Long checkerUsername;
    private LocalDateTime checkTime;

    // 省略getter、setter
}

4.2 controller層方法

@RestController
public class LeaveApplicationController {
    private LeaveApplicationService leaveApplicationService;

    @Autowired
    public LeaveApplicationController(LeaveApplicationService leaveApplicationService) {
        this.leaveApplicationService = leaveApplicationService;
    }

    /**
     * 學生發起請假申請
     * 申請的時候只是向請假申請表里插入一條數據,只有在同意的時候,才會形成job和trigger
     */
    @PostMapping("/leave_application")
    public void addLeaveApplication(@RequestBody LeaveApplication leaveApplication) {
        leaveApplicationService.addLeaveApplication(leaveApplication);
    }

}

4.3 自定義LocalDateTimeDeserializer

com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer類整個地復制過來。這里要注意,我用來原來的類名,所以如果直接將代碼復制過來,會有類名沖突,IDEA自動導入``com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer`,將類的前綴全部去掉就行了。

public class LocalDateTimeDeserializer extends JSR310DateTimeDeserializerBase<LocalDateTime> {
    
    // 省略不需要修改的代碼

    /**
     * 關鍵方法
     */
    @Override
    public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        if (parser.hasTokenId(6)) {
            // 修改了這個分支里面的代碼
            String string = parser.getText().trim();
            if (string.length() == 0) {
                return !this.isLenient() ? (LocalDateTime) this._failForNotLenient(parser, context, JsonToken.VALUE_STRING) : null;
            } else {
                return convert(string);
            }
        } else {
            // 省略了沒有修改的代碼
        }
    }

    public LocalDateTime convert(String source) {
        source = source.trim();
        if ("".equals(source)) {
            return null;
        }
        if (source.matches("^\\d{4}-\\d{1,2}$")) {
            // yyyy-MM
            return LocalDateTime.parse(source + "-01 00:00:00", dateTimeFormatter);
        } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")) {
            // yyyy-MM-dd
            return LocalDateTime.parse(source + " 00:00:00", dateTimeFormatter);
        } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")) {
            // yyyy-MM-dd HH:mm
            return LocalDateTime.parse(source + ":00", dateTimeFormatter);
        } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")) {
            // yyyy-MM-dd HH:mm:ss
            return LocalDateTime.parse(source, dateTimeFormatter);
        } else {
            throw new IllegalArgumentException("Invalid datetime value '" + source + "'");
        }
    }
}

在這個過程中,我對博客中的方法做了改進,在解析字符串的使用,用正則表達式判斷這個日期的實際格式,然后再將字符串解析成LocalDateTime。這種方法使轉換過程可以兼容多種日期類型,達到了我想要的效果。

4.4 替換反序列化器

但是我按照博客中的方法來替換,卻並沒有產生效果。反序列化的時候,

@Configuration
public class LocalDateTimeSerializerConfig {
   @Bean
   public ObjectMapper serializingObjectMapper() {
    JavaTimeModule module = new JavaTimeModule();
       // 這里導包的時候選擇自己定義的LocalDateTimeDeserializer
    LocalDateTimeDeserializer dateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    module.addDeserializer(LocalDateTime.class, dateTimeDeserializer);
    return Jackson2ObjectMapperBuilder.json().modules(module)
        .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
  }
}

4.5 再次替換反序列化器

我再次踏上查資料的不歸路,最后在強大的stack overflow上找到了一個問答,地址:How to custom a global jackson deserializer for java.time.LocalDateTime

// 這是一個webmvc的配置類
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // 重寫configureMessageConverters
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        JavaTimeModule module = new JavaTimeModule();
        // 序列化器
        module.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        // 反序列化器
        // 這里添加的是自定義的反序列化器
        module.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

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

        // add converter at the very front
        // if there are same type mappers in converters, setting in first mapper is used.
        converters.add(0, new MappingJackson2HttpMessageConverter(mapper));
    }
}

此時運行程序,發現還是不行,沒有走自定義的反序列化器。但是這時候,我看到了原問答里的這句話 if there are same type mappers in converters, setting in first mapper is used.,意思是說,如果converter里有一個相同類型的mapper,那么先設置的那個會生效。

然后我想起來,之前在統一返回值格式的時候,如果返回值是String類型,會拋出異常。為了解決這個問題,我重寫了webmvc配置里的extendMessageConverters()

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
	converters.add(0, new MappingJackson2HttpMessageConverter());
}

很可能是這里出了問題,所以我先將這個方法注釋掉。果然,再運行程序,日期的解析走到了自定義的反序列化器中。同時,可以看到兩個方法里都調了 converters.add(),所以之前返回String出現異常的問題也不會再發生。

到此,json字符串里日期解析為LocalDateTime時出現解析異常的問題就完全解決了。

本文由博客群發一文多發等運營工具平台 OpenWrite 發布


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM