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

个人博客:无奈何杨(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 | 芋道源码 —— 纯源码解析博客

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

image

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

image

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

image

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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自动化配置来看,如下。

1
2
3
4
5
6
7
8
9
10
11
12
@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的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 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

1
2
3
4
5
@Bean
StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(
ApplicationContext applicationContext, JacksonProperties jacksonProperties) {
return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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

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

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

1
2
3
4
5
6
/**
* 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类中注释有下面红框里这样的,只要引用这些类就可以支持其类型。

image

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

image

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

增加需要的依赖

1
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文件中添加以下依赖项:

1
2
3
4
5
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson-version}</version>
</dependency>

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

image

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

image

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

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

推荐配置

仅供参考,此时使用了HutoolDatePattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @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模块来实现的
1
builder.modules(new JavaTimeModule());

2、 serializerByType方法:

  • 功能:为特定类型注册一个自定义的序列化器。
  • 影响:当遇到指定类型的对象需要进行JSON序列化时,会使用设置好的自定义序列化器进行处理,而不是默认的序列化方式。
1
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