Spring 中主要有四种使用 LocalDateTime
的方式需要格式化,如下:
-
LocalDateTime
作为Controller
的参数 -
LocalDateTime
是某实体类的字段,实体类作为Controller
的参数 -
LocalDateTime
作为Controller
的返回值 -
LocalDateTime
是某实体类的字段,实体类作为Controller
的返回值
@RestController
public class TestController {
@GetMapping("/t1")
public LocalDateTime t1(LocalDateTime time) {
return time;
}
@PostMapping("/t2")
public Obj t2(@RequestBody Obj o) {
return o;
}
}
@Data
public class Obj {
private LocalDateTime time;
}
LocalDateTime 直接作为 Controller 的参数
若直接请求 http://127.0.0.1:8080/t1?time=2021-12-06 21:15:24
会抛出异常 MethodArgumentTypeMismatchException
,最内层的异常是 DateTimeParseException: Text '2021-12-06 21:15:24' could not be parsed at index 2
。
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 [java.time.LocalDateTime] for value '2021-12-06 21:15:24'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2021-12-06 21:15:24]
......
Caused by: java.time.format.DateTimeParseException: Text '2021-12-06 21:15:24' could not be parsed at index 2
at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949) ~[na:1.8.0_311]
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851) ~[na:1.8.0_311]
at java.time.LocalDateTime.parse(LocalDateTime.java:492) ~[na:1.8.0_311]
at org.springframework.format.datetime.standard.TemporalAccessorParser.doParse(TemporalAccessorParser.java:120) ~[spring-context-5.3.13.jar:5.3.13]
at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:85) ~[spring-context-5.3.13.jar:5.3.13]
at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:50) ~[spring-context-5.3.13.jar:5.3.13]
at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:217) ~[spring-context-5.3.13.jar:5.3.13]
... 54 common frames omitted
解决方案1:添加 @DateTimeFormat 注解
使用 spring 的注解 org.springframework.format.annotation.DateTimeFormat
,这种方式是局部的,不会影响整个项目
@GetMapping("/t1")
public LocalDateTime t1(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time) {
return time;
}
解决方案2:自定义 Converter
自定义 Converter
是全局设置,会覆盖 @DateTimeFormat
@Configuration(proxyBeanMethods = false)
public class ConverterConfig {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
// 使用 lambda 启动会报错
return new Converter<String, LocalDateTime>() {
@Override
public LocalDateTime convert(String source) {
return LocalDateTime.parse(source, FORMATTER);
}
};
}
}
解决方案3:@InitBinder 自定义参数绑定
此方式可使用 @RestControllerAdvice
进行全局设置,也可以只在需要的 Controller
中进行局部设置
// 全局设置
@RestControllerAdvice
public class ExceptionProcessor {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@InitBinder
public void a(WebDataBinder binder) {
binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(LocalDateTime.parse(text, FORMATTER));
}
});
}
}
LocalDateTime 是实体类的字段,实体类作为 Controller 的参数
SpringBoot 默认使用 Jackson 进行 json 的序列化和反序列化的。但是 Jackson 默认是不支持 JDK8 新增的日期、时间类型的,会抛出异常如下,且在异常中已经给出了解决办法:引入新的模块 com.fasterxml.jackson.datatype:jackson-datatype-jsr310。
本文会假设读者已经熟悉 Jackson,不会介绍其的使用方式,有兴趣的同学可以看一下我的 CNDS 专栏 Jackson。
Jackson Java 8 模块:
-
jackson-module-parameter-names
::添加了对使用 JDK8 新特性的支持,能够访问构造函数和方法参数的名称,以允许省略@JsonProperty
。 -
jackson-datatype-jsr310
:用于支持 JDK8 新增的日期、时间类型 -
jackson-datatype-jdk8
: 支持日期/时间之外的其他新 Java 8 数据类型,比如Optional
本地测试
public static void main(String[] args) throws JsonProcessingException {
String json = "{\"time\": \"2021-12-06T21:15:24.237\"}";
ObjectMapper mapper = new ObjectMapper();
Obj obj = mapper.readValue(json, Obj.class);
System.out.println(obj);
}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
at [Source: (String)"{"time": "2021-12-06T21:15:24.237"}"; line: 1, column: 10] (through reference chain: com.example.springxx.Obj["time"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1764)
at com.fasterxml.jackson.databind.deser.impl.UnsupportedTypeDeserializer.deserialize(UnsupportedTypeDeserializer.java:36)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:324)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516)
at com.example.springxx.Obj.main(Obj.java:25)
引入 jackson-datatype-jsr310
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
public static void main(String[] args) throws JsonProcessingException {
String json = "{\"time\": \"2021-12-06T21:15:24.237\"}";
ObjectMapper mapper = JsonMapper.builder()
// 新模块
.addModule(new JavaTimeModule())
.build();
Obj obj = mapper.readValue(json, Obj.class);
System.out.println(obj);
}
// 输出
// Obj(time=2021-12-06T21:15:24.237)
Spring 测试
// 调用 http://127.0.0.1:8080/t2
// 参数
{
"time": "2021-12-06T21:15:24.237"
}
// 返回值
{
"time": "2021-12-06T21:15:24.237"
}
// 参数 yyyy-MM-dd HH:mm:ss
{
"time": "2021-12-06 21:15:24"
}
// 返回值
// 抛异常
Spring 可以处理默认格式的 LocalDateTime
,是因为在 JacksonAutoConfiguration
中默认引入了 jackson-datatype-jsr310
。
JacksonAutoConfiguration 部分源码
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperBuilderConfiguration {
// 注册了一个Bean Jackson2ObjectMapperBuilder,用于创建 ObjectMapper
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
// 实例化 Jackson2ObjectMapperBuilder
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
// 对 Jackson2ObjectMapperBuilder 定制化,即可对 ObjectMapper 定制化
// 我们也可以通过 Jackson2ObjectMapperBuilderCustomizer 的功能实现 LocalDateTime 格式化的全局配置
customize(builder, customizers);
return builder;
}
private void customize(Jackson2ObjectMapperBuilder builder,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration {
// 使用 Jackson2ObjectMapperBuilder 创建 ObjectMapper 并注册 Bean
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
}
Jackson2ObjectMapperBuilder#configure
configure
方法会调用 registerWellKnownModulesIfAvailable
查找可用的 Module,包括 com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
,然后注册所有的 Module。
public void configure(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
MultiValueMap<Object, Module> modulesToRegister = new LinkedMultiValueMap<>();
if (this.findModulesViaServiceLoader) {
ObjectMapper.findModules(this.moduleClassLoader).forEach(module -> registerModule(module, modulesToRegister));
}
else if (this.findWellKnownModules) {
// 查找 Jdk8Module、JavaTimeModule 模块
registerWellKnownModulesIfAvailable(modulesToRegister);
}
...
List<Module> modules = new ArrayList<>();
for (List<Module> nestedModules : modulesToRegister.values()) {
modules.addAll(nestedModules);
}
// 注册 Module
objectMapper.registerModules(modules);
}
private void registerWellKnownModulesIfAvailable(MultiValueMap<Object, Module> modulesToRegister) {
try {
// com.fasterxml.jackson.datatype.jdk8.Jdk8Module
Class<? extends Module> jdk8ModuleClass = (Class<? extends Module>)
ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", this.moduleClassLoader);
Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass);
modulesToRegister.set(jdk8Module.getTypeId(), jdk8Module);
}
catch (ClassNotFoundException ex) {
// jackson-datatype-jdk8 not available
}
try {
// 日期、时间模块 com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
Class<? extends Module> javaTimeModuleClass = (Class<? extends Module>)
ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", this.moduleClassLoader);
Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass);
modulesToRegister.set(javaTimeModule.getTypeId(), javaTimeModule);
}
catch (ClassNotFoundException ex) {
// jackson-datatype-jsr310 not available
}
...
}
知道了 Spring 与 Jackson 的原理,问题就好解决了。
解决方案1:添加 Jackson 的 @JsonFormat 注解
这是个局部配置
@Data
public class Obj {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime time;
}
// 参数
{
"time": "2021-12-06 21:15:24"
}
// 返回值
{
"time": "2021-12-06 21:15:24"
}
可以看到返回值也被格式化成了 yyyy-MM-dd HH:mm:ss
。
解决方案2:@JsonSerialize、@JsonDeserialize 自定义序列化器和反序列化器
@JsonSerialize、@JsonDeserialize
可分别指定序列化、反序列化时的格式:
public class MyLocalDateTimeSerializer extends com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer {
public MyLocalDateTimeSerializer() {
super(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
public class MyLocalDateTimeDeserializer extends com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer {
public MyLocalDateTimeDeserializer() {
super(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
@Data
public class Obj {
@JsonSerialize(using = MyLocalDateTimeSerializer.class)
@JsonDeserialize(using = MyLocalDateTimeDeserializer.class)
private LocalDateTime time;
}
// 参数
{
"time": "2021-12-06 21:15:24"
}
// 返回值
{
"time": "2021-12-06 21:15:24"
}
解决方案3:使用 Jackson2ObjectMapperBuilderCustomizer 定制化功能
使用 Jackson2ObjectMapperBuilderCustomizer
定制化 Jackson2ObjectMapperBuilder
,进而定制化 ObjectMapper
@Configuration
public class LocalDateTimeCustomizer implements Jackson2ObjectMapperBuilderCustomizer {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
// 反序列化(参数)
jacksonObjectMapperBuilder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(FORMATTER));
// 序列化(返回值)
//jacksonObjectMapperBuilder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(FORMATTER));
}
}
// 参数
{
"time": "2021-12-06 21:15:24"
}
// 返回值:注释掉 serializerByType
{
"time": "2021-12-06T21:15:24"
}
// 返回值:不注释 serializerByType
{
"time": "2021-12-06 21:15:24"
}
//
LocalDateTime 作为返回值
不管是 LocalDateTime
直接作为返回值,还是实体类作为返回值,都可以用 Jackson2ObjectMapperBuilderCustomizer
解决。
参考