Spring 中主要有四种使用 LocalDateTime 的方式需要格式化,如下:

  1. LocalDateTime 作为 Controller 的参数
  2. LocalDateTime 是某实体类的字段,实体类作为 Controller 的参数
  3. LocalDateTime 作为 Controller 的返回值
  4. 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 解决。

参考