• 解决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 发布

  • 相关阅读:
    oracle之同义词
    oracle之序列
    oracle之视图
    oracle表空间
    oracle 闪回技术
    oracle权限管理
    oracle的undo表空间
    创建数据库之间的连接
    oracle监听
    oracle11g文件系统库迁移到ASM库上面
  • 原文地址:https://www.cnblogs.com/FatShallot/p/12848444.html
Copyright © 2020-2023  润新知