解决json字符串转为对象时LocalDateTime异常问题
1 出现异常
这次的异常出现在前端向后端发送请求体⾥带了两个⽇期,在后端的实体类中,这两个⽇期的格式都是JDK8中的时间类LocalDateTime。默认情况下,LocalDateTime只能解
析2020-01-01T10:00:00这样标准格式的字符串,这⾥⽇期和时间中间有⼀个T。如果不做任何修改的话,LocalDateTime直接解析2020-05-01 08:00:00这种我们习惯上能接受的⽇期格
式,会抛出异常。
异常信息:
org.verter.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.fo // 省略部分异常信息
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默认使⽤的是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);
}
string转date的方法}
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 = Text().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 = im();
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上到了⼀个问答,地址:。
// 这是⼀个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();
// 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时出现解析异常的问题就完全解决了。
本⽂由博客发⼀⽂多发等运营⼯具平台发布
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论