19

解决json字符串转为对象时LocalDateTime异常问题

 3 years ago
source link: http://www.cnblogs.com/FatShallot/p/12848444.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1 出现异常

这次的异常出现在前端向后端发送请求体里带了两个日期,在后端的实体类中,这两个日期的格式都是JDK8中的时间类LocalDateTime。默认情况下,LocalDateTime只能解析 2020-01-01T10:00:00 这样标准格式的字符串,这里日期和时间中间有一个T。如果不做任何修改的话,LocalDateTime直接解析 2020-05-01 08:00:00 这种我们习惯上能接受的日期格式,会抛出异常。

MFRzAn2.png!web

异常信息:

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()方法 完成的。这一点可以通过断点调试确认

v2qUZbz.png!web

解决思路是用自定义的反序列化器替换掉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 发布


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK