个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview


参考

【相见欢】Spring MVC 源码剖析(五) —— 消息转换器 HttpMessageConverter | 芋道源码 —— 纯源码解析博客

Spring boot 中时间类型的序列化与反序列化 - 掘金

Spring boot 中 Jackson 的常用配置

Failed to deserialize java.time.LocalDateTime

这是web开发中常见的一个错误,无法完成LocalDataTime的反序列化。

这是怎么回事?

其实这个问题在于LocalTimeLocalDateLocalDateTime的序列化上。

这就要从SpringMVC说起了!

Spring MVCSpring Framework 的核心组件之一,与其他模块(如 Spring BootSpring DataSpring Security 等)一起构成了完整的 Spring 生态系统。

Spring MVCWeb开发中极其重要,其设计思想是非常值得学习的。

关于SpringMVC的学习可以参考以下文章

Category: Spring-MVC | 芋道源码 —— 纯源码解析博客

请求报文在传输中是要进行序列化和反序列化的,有大致经历了如下流程。

SpringBoot之Jackson,自动化配置,Java 8 date/time type java.time.LocalTime not supported_反序列化

其中这个HttpMessageConver是这个过程中的关键,这个接口继承树如下图。

SpringBoot之Jackson,自动化配置,Java 8 date/time type java.time.LocalTime not supported_jackson_02

默认配置下Spring MVC使用Jackson库进行json处理,使用MappingJackson2HttpMessageConverter,在其中有一个非常重要的类ObjectMapper

SpringBoot之Jackson,自动化配置,Java 8 date/time type java.time.LocalTime not supported_springboot_03

从自动化配置JacksonHttpMessageConvertersConfiguration来看,MappingJackson2HttpMessageConverter的创建使用new MappingJackson2HttpMessageConverter(objectMapper)方法,使用了objectMapperbean

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnBean(ObjectMapper.class)
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
        havingValue = "jackson", matchIfMissing = true)
static class MappingJackson2HttpMessageConverterConfiguration {

    @Bean
    @ConditionalOnMissingBean(value = MappingJackson2HttpMessageConverter.class,
            ignoredType = {
                    "org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter",
                    "org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter" })
    MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        return new MappingJackson2HttpMessageConverter(objectMapper);
    }

}

关于ObjectMapperbean要从JacksonAutoConfiguration自动化配置来看,如下。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration {

    @Bean
    @Primary
    @ConditionalOnMissingBean
    ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        return builder.createXmlMapper(false).build();
    }

}

ObjectMapper的创建由Jackson2ObjectMapperBuilder构建而来,可以在同一个类中看到。

从以下方法可以看到,customize(builder, customizers);此方法很关键,其使用到了Jackson2ObjectMapperBuilderCustomizer,这个也是官方提供的,用于进一步自定义ObjectMapper的接口。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperBuilderConfiguration {

    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
            List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.applicationContext(applicationContext);
        customize(builder, customizers);
        return builder;
    }

    private void customize(Jackson2ObjectMapperBuilder builder,
            List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
        for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
            customizer.customize(builder);
        }
    }

}

Jackson2ObjectMapperBuilderCustomizer如下。

/**
 * Callback interface that can be implemented by beans wishing to further customize the
 * {@link ObjectMapper} through {@link Jackson2ObjectMapperBuilder} retaining its default
 * auto-configuration.
 *
 * @author Grzegorz Poznachowski
 * @since 1.4.0
 */
@FunctionalInterface
public interface Jackson2ObjectMapperBuilderCustomizer {

    /**
	 * Customize the JacksonObjectMapperBuilder.
	 * @param jacksonObjectMapperBuilder the JacksonObjectMapperBuilder to customize
	 */
    void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder);

}

默认SpringBoot存在此接口的一个实现StandardJackson2ObjectMapperBuilderCustomizer

@Bean
StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(
        ApplicationContext applicationContext, JacksonProperties jacksonProperties) {
    return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);
}

其实现此接口,方法如下,这些方法很关键,可以查看源码探究一下。

@Override
public void customize(Jackson2ObjectMapperBuilder builder) {
    if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
        builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
    }
    if (this.jacksonProperties.getTimeZone() != null) {
        builder.timeZone(this.jacksonProperties.getTimeZone());
    }
    configureFeatures(builder, FEATURE_DEFAULTS);
    configureVisibility(builder, this.jacksonProperties.getVisibility());
    configureFeatures(builder, this.jacksonProperties.getDeserialization());
    configureFeatures(builder, this.jacksonProperties.getSerialization());
    configureFeatures(builder, this.jacksonProperties.getMapper());
    configureFeatures(builder, this.jacksonProperties.getParser());
    configureFeatures(builder, this.jacksonProperties.getGenerator());
    configureDateFormat(builder);
    configurePropertyNamingStrategy(builder);
    configureModules(builder);
    configureLocale(builder);
    configureDefaultLeniency(builder);
    configureConstructorDetector(builder);
}

其中configureModules方法Module类型的bean

private void configureModules(Jackson2ObjectMapperBuilder builder) {
    Collection<Module> moduleBeans = getBeans(this.applicationContext, Module.class);
    builder.modulesToInstall(moduleBeans.toArray(new Module[0]));
}

此抽象类用于ObjectMapper支持新的数据类型。

/**
 * Simple interface for extensions that can be registered with {@link ObjectMapper}
 * to provide a well-defined set of extensions to default functionality; such as
 * support for new data types.
 */
public abstract class Module implements Versioned

正好在Jackson2ObjectMapperBuilder类中注释有下面红框里这样的,只要引用这些类就可以支持其类型。

SpringBoot之Jackson,自动化配置,Java 8 date/time type java.time.LocalTime not supported_jackson_04

因为这些包实现一些接口,增加了新的序列化和反序列化类。

SpringBoot之Jackson,自动化配置,Java 8 date/time type java.time.LocalTime not supported_springboot_05

到这里几乎就能清楚了一些关于SpringMVC出现的json序列化和反序列化的问题了。

增加需要的依赖

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type java.time.LocalTime not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling

这个错误是由于Java 8中的LocalTime类型不被默认支持导致的。要解决这个错误,需要添加JDK 8的日期/时间API依赖项到您的项目中。

在您的pom.xml文件中添加以下依赖项:

<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
  <version>${jackson-version}</version>
</dependency>

这里要注意,所有序列化是有默认的序列化方法的,如下。

SpringBoot之Jackson,自动化配置,Java 8 date/time type java.time.LocalTime not supported_反序列化_06

可以看到其使用的ISO的序列化方式,简单地讲就是会有yyyy-MM-dd'T'HH:mm:ssyyyy-MM-dd HH:mm:ss,SSS的差别,使用时要注意些。

SpringBoot之Jackson,自动化配置,Java 8 date/time type java.time.LocalTime not supported_java_07

虽然有@JsonFormat这样的注解也可以完成序列化配置,但更推荐有特殊要求时使用,项目中还是统一配置一下吧。

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateTime;

推荐配置

仅供参考,此时使用了HutoolDatePattern

/**
 * @author wnhyang
 * @date 2024/6/20
 **/
@Configuration
@Slf4j
public class JacksonConfig {

    public static JavaTimeModule buildJavaTimeModule() {
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DatePattern.NORM_TIME_FORMATTER));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DatePattern.NORM_DATE_FORMATTER));
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMATTER));

        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DatePattern.NORM_TIME_FORMATTER));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DatePattern.NORM_DATE_FORMATTER));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMATTER));
        return javaTimeModule;
    }

    @Bean
    @ConditionalOnMissingBean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        log.info("[Jackson2ObjectMapperBuilderCustomizer][初始化customizer配置]");
        return builder -> {
            builder.locale(Locale.CHINA);
            builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
            builder.simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN);
            builder.serializerByType(Long.class, ToStringSerializer.instance);
            builder.modules(buildJavaTimeModule());
        };
    }
}

配置说明补充

Jackson2ObjectMapperBuilderCustomizer接口允许开发者自定义ObjectMapper的行为,而ObjectMapperJackson库中用于JSON序列化和反序列化的核心组件。下面分别说明在该接口实现中使用modules方法和serializerByType方法对ObjectMapper配置的影响:

1、modules方法:

  • 功能:添加模块(Module)到ObjectMapper中。
  • 影响:通过模块可以扩展Jackson的功能,例如添加新的序列化器、反序列化器、类型信息处理器等。这些模块可能包含一组针对特定类型的序列化和反序列化规则,或者提供一些全局的配置选项。例如,Java 8日期时间APIJSR-310)的支持就是通过JavaTimeModule模块来实现的
builder.modules(new JavaTimeModule());

2、 serializerByType方法:

  • 功能:为特定类型注册一个自定义的序列化器。
  • 影响:当遇到指定类型的对象需要进行JSON序列化时,会使用设置好的自定义序列化器进行处理,而不是默认的序列化方式。
builder.serializerByType(Long.class, ToStringSerializer.instance);

上述代码片段的作用是将所有Long类型的值在序列化时转换为其字符串表示形式,而非默认的数字形式。

总结来说,modules方法主要用于引入整个功能模块或大规模的配置更改,而serializerByType方法则用于精确控制单个类型如何被序列化到JSON。两者共同作用于ObjectMapper上,以满足应用程序对于JSON数据转换的各种定制需求。

优先级

Jackson2ObjectMapperBuilder中,通过modules方法添加的模块和通过serializerByType设置的特定类型序列化器,在处理对象序列化时的优先级关系是:

  • ObjectMapper遇到一个需要序列化的对象时,会首先查找是否有为该具体类型注册的自定义序列化器(如通过serializerByType设置)。
  • 若没有找到针对该类型的自定义序列化器,它会继续查找已添加的所有模块(如通过modules方法添加),看这些模块中是否存在对该类型的序列化规则。

因此,对于特定类型而言,如果同时通过serializerByType设置了自定义序列化器并且在模块中有相应的序列化规则,那么serializerByType设置的序列化器将具有更高的优先级。不过通常情况下,开发者应当避免这样的冲突设计,确保配置的清晰和一致性。

写在最后

拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。


个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview

SpringBoot之Jackson,自动化配置,Java 8 date/time type java.time.LocalTime not supported_反序列化_08