解决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里面的反序列化器,在解析的时候使用自己定义的解析逻辑。
在这里,序列化(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 发布
上一篇: php实现简单聊天功能