一、为什么使用LocalDateTime
LocalDateTime 和 Date 是 Java 中处理日期和时间的两种不同的类,在 JDK8 中引入了 java.time 包。LocalDateTime 相比 Date 有一些优势,主要体现在以下几个方面:
- 不可变性(Immutability):
- LocalDateTime 是不可变的类,一旦创建就不能被修改。任何对 LocalDateTime 的操作都会返回一个新的对象,而不会修改原始对象。这有助于避免在多线程环境中的并发问题。
- 线程安全性:
- 由于 LocalDateTime 是不可变的,因此它天然具有线程安全性,可以在多线程环境中安全使用。
- 可读性和易用性:
- LocalDateTime 提供了更加清晰和直观的API,使得处理日期和时间更加易读、易用。例如,通过使用方法链式调用,可以轻松地执行各种操作,而不需要复杂的日期格式化和解析。
- 更好的API设计:
- LocalDateTime 提供了更丰富、灵活和易用的API,允许进行各种日期和时间的操作,例如增减天数、小时、分钟等。而 Date 的 API相对较为古老和不够直观。
- 时区处理:
- LocalDateTime 能够更好地处理时区信息,通过 ZonedDateTime 类可以轻松转换到不同的时区。而 Date 类在处理时区时较为复杂,通常需要使用 Calendar 类。
总的来说,LocalDateTime 提供了更现代、清晰和强大的日期和时间处理功能,使得开发者更容易编写可读性高且可维护性强的代码。在新的代码中,特别是在使用 JDK 8 及更新版本的项目中,推荐使用 LocalDateTime 替代 Date。
二、使用LocalDateTime遇到的问题
众所周知,SpringBoot会自动对前端的传值进行解析,将 HttpServletRequest 中的请求内容,转换成后端 Controoler 控制器中的实体类参数,但在实际使用 LocalDateTime 作用参数时,我们会发现 SpringBoot 好像不能正常工作了,下面是两个问题例子。
- GET请求使用 LocalDateTime 作为参数
下面是使用 @RequestParam 和 @PathVariable 两种最常见的用法示例
1 |
|
点击运行,无论是第一个还是第二个方法,都会出现如下异常
1 | org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [ .springframework.web.bind.annotation.PathVariable java.time.LocalDateTime] for value '2023-01-01 12:12:12'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2023-01-01 12:12:12] |
POST请求体中使用 LocalDateTime 作为参数
下面是使用 @RequestBody 的示例1
2
3
4
5
6
7
8
9
10
11
12
13
public class LocalDateTimeController {
public void resolveBodyDateTime( { ResolveBody body)
System.out.println("ok");
}
private static class ResolveBody {
private LocalDateTime dateTime;
}
}点击运行,会出现如下异常
1
2
3
4
5
6
7
8
9
10
11
12
13org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Expected array or string.; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Expected array or string.
at [Source: (PushbackInputStream); line: 1, column: 1]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:245)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:227)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:205)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:158)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:131)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Expected array or string.
at [Source: (PushbackInputStream); line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1442)简单分析
观察堆栈错误信息,在使用 GET 请求时抛出的异常的 MethodArgumentTypeMismatchException ,而使用 POST请求时抛出的异常是 HttpMessageNotReadableException 。
二者最外层抛出的异常不一样,但是我们继续往下看最内层抛出的异常, GET 请求的报错很明显就是 String 类型不能被转换为 LocalDateTime 类型抛出的异常,POST请求抛出的是 jackson 反序列化类型不匹配的异常。
通过分析可得,上述 GET 和 POST 请求的报错,都是由于 SpringBoot 未能正常将参数中 String 类型转换为 LocalDateTime 类型抛出的。
三、异常源码分析
通过堆栈信息可以看出,我们的请求链路在进行参数解析,也就是走到spring mvc 的HandlerMethodArgumentResolverComposite.resolveArgument()这个方法时发生的异常,从这个方法入手debug打断点依次往下执行,一直走到最内层的堆栈位置,也就是实际进行参数解析的核心代码,下面列出实际参数解析代码。
- GET请求
第13行就是实际的转换代码,走到这里时spring拿到的converter是通过WebMvcAutoConfiguration类自动装配注入的FormattingConversionService对象 ,但是这个converter默认情况下并不能将String转换为LocalDateTime
1 |
|
POST请求
在第11~12行就是实际的转换代码,走到这里时spring拿到的converter是 MappingJackson2HttpMessageConverter ,但是这个converter并不能将String转换为LocalDateTime1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
# 这里去做类型转换
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}MappingJackson2HttpMessageConverter的read解析方法继续往后走,调用的是父类AbstractJackson2HttpMessageConverter的read方法,实际的解析方法就是第18行的ObjectMapper类的readValue方法
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
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(type, contextClass);
return readJavaType(javaType, inputMessage);
}
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
try {
if (inputMessage instanceof MappingJacksonInputMessage) {
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
if (deserializationView != null) {
return this.objectMapper.readerWithView(deserializationView).forType(javaType).
readValue(inputMessage.getBody());
}
}
# jackson实际解析
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
}
}问题分析
SpringBoot GET请求参数类型转换使用的是GenericConversionService类里注册的一个个GenericConverter;String转LocalDateTime类型默认情况下的StringToLocalDateTimeConverter不能正常解析。
SpringBoot POST请求参数类型转换使用的是AbstractMessageConverterMethodArgumentResolver类里List<HttpMessageConverter<?>> messageConverters里注册的一个个HttpMessageConverter;String转LocalDateTime类型默认情况下的MappingJackson2HttpMessageConverter不能正常解析,也就是默认的ObjectMapper不能正常解析。
四、解决思路
通过上述问题分析,下面有两种解决思路(推荐第二种)
- 通过配置使得默认的StringToLocalDateTimeConverter支持GET请求String转LocalDateTime类型,默认的MappingJackson2HttpMessageConverter支持POST请求String转LocalDateTime类型
- spring注入GenericConverter类型的Bean支持GET请求String转LocalDateTime类型,注入ObjectMapper类型的Bean支持POST请求String转LocalDateTime类型
4.1、GET请求解决思路
GET请求解决思路,通过阅读源码,可以发现以下三种springboot为用户预留出来的实现方式:
- application.yml配置
在2.3.x以上版本,springmvc增加了日期时间格式配置,并且可以将格式注册到对应的日期解析器中
1 | // springboot自动装配WebConversionService对象 |
1 | private void addFormatters(DateTimeFormatters dateTimeFormatters) { |
使用@DateTimeFormat注解
1
2
3
4
5
6
7
8
9
10
11
12public void registerFormatters(FormatterRegistry registry) {
// 这里可以拿到application.yml配置文件指定的格式
DateTimeFormatter df = this.getFormatter(DateTimeFormatterRegistrar.Type.DATE);
DateTimeFormatter tf = this.getFormatter(DateTimeFormatterRegistrar.Type.TIME);
DateTimeFormatter dtf = this.getFormatter(DateTimeFormatterRegistrar.Type.DATE_TIME);
registry.addFormatterForFieldType(LocalDate.class, new TemporalAccessorPrinter(df == DateTimeFormatter.ISO_DATE ? DateTimeFormatter.ISO_LOCAL_DATE : df), new TemporalAccessorParser(LocalDate.class, df));
registry.addFormatterForFieldType(LocalTime.class, new TemporalAccessorPrinter(tf == DateTimeFormatter.ISO_TIME ? DateTimeFormatter.ISO_LOCAL_TIME : tf), new TemporalAccessorParser(LocalTime.class, tf));
registry.addFormatterForFieldType(LocalDateTime.class, new TemporalAccessorPrinter(dtf == DateTimeFormatter.ISO_DATE_TIME ? DateTimeFormatter.ISO_LOCAL_DATE_TIME : dtf), new TemporalAccessorParser(LocalDateTime.class, dtf));
// 使用@DateTimeFormat注解来帮忙完成LocalDateTime类型的解析
registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory());
}向容器中添加自定义Converter
我们在WebConversionService对象自动装配方法中看到了这行代码:this.addFormatters(conversionService);
这其实就是springboot为用户预留的拓展方法,它支持用户向容器中添加自定义Converter
1 | // WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#addFormatters()方法 |
4.2、POST请求解决思路
通过异常源码分析我们可以得知,由于jackson也就是ObjectMapper对象在默认情况下并不能完成LocalDateTime类型的解析,所有需要对jackson进行配置;通过阅读源码,以下有两种解决方式:
配置类注入Jackson2ObjectMapperBuilderCustomizer类型的Bean对jackson进行配置(推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static class JacksonObjectMapperBuilderConfiguration {
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
// ioc容器中获取所有Jackson2ObjectMapperBuilderCustomizer类型的bean, 调用customize方法配置Jackson2ObjectMapperBuilder
customize(builder, customizers);
return builder;
}
private void customize(Jackson2ObjectMapperBuilder builder,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
}
}直接注入ObjectMapper类型的Bean进行覆盖
1
2
3
4
5
6
7
8
9
10
11
12
static class JacksonObjectMapperConfiguration {
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
// 通过Jackson2ObjectMapperBuilder构建出ObjectMapper
return builder.createXmlMapper(false).build();
}
}
五、解决方案落地
5.1、GET请求解决方案
application.yml配置
SpringBoot2.3.x以及更高的版本,springmvc增加了日期时间格式配置,既可以解决LocalDateTime类型参数解析,也可以解决Date类型参数解析1
2
3
4
5spring:
mvc:
date: yyyy-MM-dd
time: HH:mm:ss
date-time: yyyy-MM-dd HH:mm:ss注解配置
SpringBoot针对LocalDateTime类型解析增加了@DateTimeFormatter注解,可以在请求参数中加上这个注解完成解析1
2
3
4
5
6
7
8
9
10
11
12
public class LocalDateTimeController {
public void resolveRequestParamDateTime( { LocalDateTime dateTime)
System.out.println("ok");
}
public void resolvePathVariableDateTime( { LocalDateTime dateTime)
System.out.println("ok");
}
}Java Config注入Bean
在Spring IOC容器中注入Converter,SpringBoot会自动将IOC容器中的Converter放到GenericConversionService中
1 |
|
5.2、POST请求解决方案
注解配置
在实体类的字段上使用@JsonFormat注解配置格式,使用 @JsonSerialize注解配置序列化器,使用 @JsonDeserialize注解配置反序列化器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LocalDateTimeController {
public void resolveBodyDateTime( { ResolveBody body)
System.out.println("ok");
}
private static class ResolveBody {
private LocalDateTime dateTime;
}
}Java Config注入Bean
在Spring IOC容器中注入Jackson2ObjectMapperBuilderCustomizer类型的Bean可以对Jackson进行自定义配置;也可以直接注入一个ObjectMapper进行替换
1 |
|