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里面的反序列化器,在解析的時候使用自己定義的解析邏輯。
在這里,序列化(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 發布