乐于分享
好东西不私藏

从源码到实战:彻底解决 Spring Boot 中 JDK8 日期时间的序列化难题

从源码到实战:彻底解决 Spring Boot 中 JDK8 日期时间的序列化难题

请在微信客户端打开

概述
在 Spring Boot 应用开发中,处理 JDK8 的 LocalDateTime 的序列化、反序列化往往是让人头疼的问题。在默认情况下,LocalDateTime 的序列化、反序列化对应的字符串的格式为【yyyy-MM-dd'T'HH:mm:ss】,但是这个格式并不符合我们的阅读习惯
本文将以 POST 请求、通过 @RequestBody 接收对象参数为例,从源码开始入手分析,教你 Spring Boot 是如何整合 Jackson,并提供三种可靠的解决方案

源码分析
分析之前,必须清楚一个知识点:Spring Boot 将请求体中数据反序列化为对象参数是由消息转换器完成的,而内部具体的处理逻辑则由 Jackson 的 ObjectMapper 来处理
在 POST 请求中,@RequestBody 要接收对象参数,是由 MappingJackson2HttpMessageConverter 消息转换器完成的,其源码如下(仅截图了构造方法)
在图中的两个构造方法中,可以处理 application/json 的媒体类型格式的构造方法是第二个,那么使用正则表达式 MappingJackson2HttpMessageConverter \S+\( 全局搜索 Spring Boot 是在哪里创建并注入 MappingJackson2HttpMessageConverter 对象,匹配数量很少、可以非常快速定位到类 JacksonHttpMessageConvertersConfiguration,其中对应的源码如下
到了此处,我们可以确认 Spring Boot 就是集成 Jackson、使用 ObjectMapper 来进行序列化、反序列化
但是继续跟踪,接下来查找一下 Spring Boot 是在哪里创建并注入 ObjectMapper 对象,使用正则表达式 ObjectMapper \S+\( 进行全局搜索,虽然会匹配到很多项,但是其中创建对象并注入 Bean 容器的仅有一处,在类 JacksonAutoConfiguration 中,其中对应的源码如下
源码跟踪到这里,有经验的小伙伴应该立马就注意到了注解 @ConditionalOnMissingBean,代表当 Spring 的 Bean 容器中没有该对象时则执行该方法并将方法结果注入到容器中。而上面也反复提到真正处理序列化、反序列化的就是 ObjectMapper,我们完全可以自己创建一个带有处理 JDK8 日期时间逻辑的 ObjectMapper 对象并注入Spring 的 Bean 容器中,这是一种方案
想要再深入了解一下的化,可以继续源码跟踪,接下来查找一下 Spring Boot 是在哪里创建并注入 Jackson2ObjectMapperBuilder 对象,使用正则表达式 Jackson2ObjectMapperBuilder \S+\( 进行全局搜索,也是会匹配到很多项,但是其中创建对象并注入 Bean 容器的仅有一处,在类 JacksonAutoConfiguration 中,其中对应的源码如下
到了这里就清楚了,若想要进行自定义逻辑处理,则需要从 Jackson2ObjectMapperBuilderCustomizer 入手,而它是一个函数式接口,其源码如下
从这个接口看,想要自定义,那么又回到 Jackson2ObjectMapperBuilder 对象,这里是终点了,我们只能观察一下该类的方法,一点点勘察可以看到语义上跟序列化、反序列化相关的成员变量、方法,其源码如下
此为最终自定义逻辑的位置了,注释已经明确说明这些方法是设置某个类的自定义序列化、反序列化逻辑了,我们只需要在函数式接口的实现类中调用这些方法实现处理逻辑即可

解决方案

方案一
通过上面的源码分析,从 Jackson2ObjectMapperBuilderCustomizer 接口切入,可以创建一个它的实现类并注入到 Spring 的 Bean 容器中
package wxw.mengyuan.dsf.common.bean.datetime8;import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.time.LocalDate;import java.time.LocalDateTime;import java.time.LocalTime;import java.time.format.DateTimeFormatter;/** * JDK8日期时间序列化、反序列化的格式配置 */@Configurationpublic class ObjectMapperBean {  @Bean  public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {    DateTimeFormatter date = DateTimeFormatter.ofPattern("yyyy-MM-dd");    DateTimeFormatter time = DateTimeFormatter.ofPattern("HH:mm:ss");    DateTimeFormatter datetime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");    return builder -> {      builder.serializerByType(LocalDate.class, new LocalDateSerializer(date));      builder.serializerByType(LocalTime.class, new LocalTimeSerializer(time));      builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(datetime));      builder.deserializerByType(LocalDate.class, new LocalDateDeserializer(date));      builder.deserializerByType(LocalTime.class, new LocalTimeDeserializer(time));      builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(datetime));    };  }}
方案二
在源码分析的中段位置,也说过可以自己创建一个具有处理 JDK8 日期时间能力的 ObjectMapper 对象并注入到 Spring 的 Bean 容器中,让消息转换器使用我们自己的 ObjectMapper 对象进行序列化、反序列化
package wxw.mengyuan.springdemo.date8;import com.fasterxml.jackson.databind.DeserializationFeature;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;/** * JSON转换对象 */@Configurationpublic class ObjectMapperBean {  /**   * JSON转换   */  @Bean  @Primary  public ObjectMapper objectMapper() {    DateTimeFormatter date = DateTimeFormatter.ofPattern("yyyy-MM-dd");    DateTimeFormatter time = DateTimeFormatter.ofPattern("HH:mm:ss");    DateTimeFormatter datetime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");    // 创建对象    ObjectMapper mapper = new ObjectMapper();    // 识别Java8的日期时间、并且设置格式    JavaTimeModule javaTimeModule = new JavaTimeModule();    javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(date));    javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(time));    javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(datetime));    javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(date));    javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(time));    javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(datetime));    mapper.registerModule(javaTimeModule);    // 返回对象    return mapper;  }}
方案三
若习惯默认的日期时间格式,仅仅是某些字段需要特殊处理、使用其他格式,那么可以使用 Jackson 提供的 @JsonFormat 注解来对单个字段精确控制
package wxw.mengyuan.springdemo.hibernate;import com.fasterxml.jackson.annotation.JsonFormat;import jakarta.validation.constraints.NotNull;import lombok.Getter;import lombok.Setter;import java.io.Serial;import java.io.Serializable;import java.time.LocalDateTime;/** * 视图类 */@Setter@Getterpublic class SpringDemoVO implements Serializable {  /** 序列版本号 */  @Serial  private static final long serialVersionUID = 1L;  @NotNull(message = "【开始时间】不能为空")  @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")  private LocalDateTime startTime;  @NotNull(message = "【结束时间】不能为空")  @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")  private LocalDateTime endTime;}
最终方案建议
推荐使用方案一进行全局日期时间格式定义,使用方案三对部分特殊字段进行精确控制

相关学习资料