在 Spring Boot 中自定义 Date/LocalDateTime/LocalDate 的序列化、反序列化格式

Spring Boot 中默认使用的 JSON 框架是 jackson,它负责把 JSON 请求体反序列化为 Java 对象,并把响应给客户端的 Java 对象序列化为 JSON 字符串。

本文将会详细介绍如何在 Spring Boot 应用中自定义 jackson 对 DateLocalDateTimeLocalDate 等日期对象的序列化、反序列化格式。

Jackson 对 Date/LocalDateTime/LocalDate 的默认处理方式

为了处理 java.time 类型的日期类,你还需要在项目中添加 com.fasterxml.jackson.datatype:jackson-datatype-jsr310 模块。

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

默认情况下,Jackson 将 Date 对象序列化为时间戳。对于 LocalDateTimeLocalDate 对象,jackson 会序列化为一个 long[],其中,从第一个元素开始分别表示日期的:年、月、日、时、分、秒、毫秒。

示例:

package cn.springdoc.test;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

public class JacksonTest {

    public static void main(String[] args) throws JsonProcessingException {
        
        ObjectMapper objectMapper = new ObjectMapper();
        
        // 注册 JavaTime 模块
        objectMapper.registerModule(new JavaTimeModule()); 
        
        Map<String, Object> map = new HashMap<>();
        
        // Date 类型
        map.put("date", new Date());
        // LocalDateTime 类型
        map.put("localDateTime", LocalDateTime.now());
        // LocalDate 类型
        map.put("localDate", LocalDate.now());

        String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(map);

        System.out.println(json);
    }
}

输出如下:

{
  "date" : 1693533643706,
  "localDateTime" : [ 2023, 9, 1, 10, 0, 43, 714833900 ],
  "localDate" : [ 2023, 9, 1 ]
}

Date 的格式化

通过 Spring Boot 提供的配置属性,可以轻松自定义 Date 对象的序列化、反序列化格式。

spring:
  jackson:
    # Date 对象的格式化方式
    date-format: "yyyy-MM-dd HH:mm:ss"
    # 格式化用的时区
    time-zone: "GMT+8"

LocalDateTime 和 LocalDate 的格式化

Spring Boot 未提供与 LocalDateTimeLocalDate 相关的格式化配置属性,但提供了一个 Jackson2ObjectMapperBuilderCustomizer 接口,使得我们可以通过 @Configuration 的方式轻松自定义 LocalDateTimeLocalDate 的序列化、反序列化格式。

package cn.springdoc.demo.config;

import java.time.format.DateTimeFormatter;

import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

@Configuration
public class JacksonConfiguration {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        
        return builder -> {
            
            // LocalDate 和 LocalDateTime 的格式化方式
            DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            DateTimeFormatter dateTimeFormatter =  DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            
            // 设置 LocalDate 和 LocalDateTime 反序列化器
            builder.deserializers(new LocalDateDeserializer(dateFormatter));
            builder.deserializers(new LocalDateTimeDeserializer(dateTimeFormatter));
            
            // 设置 LocalDate 和 LocalDateTime 序列化器
            builder.serializers(new LocalDateSerializer(dateFormatter));
            builder.serializers(new LocalDateTimeSerializer(dateTimeFormatter));
        };
    }
}

测试

创建一个简单的 controller 和 model 类。该 model 类中定义了三种日期类型的字段,并且使用这个 model 类作为 controller 唯一 handler 方法的请求体(反序列化)和响应体(序列化)。

package cn.springdoc.demo.controller;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

// Payload 请求体/响应体对象
class Payload {
    // Date 类型字段
    private Date date;
    // LocalDate 类型字段
    private LocalDate localDate;
    // LocalDateTime 类型字段
    private LocalDateTime  localDateTime;

    // 省略 get/set 方法 ...
}

@RestController
@RequestMapping("/test")
public class TestController {

    @PostMapping
    public Object test (@RequestBody Payload payload) {
        // 直接把请求体响应给客户端。
        return payload;
    }
}

使用 Postman 发起请求:

POST /test HTTP/1.1
Origin: http://localhost:8080/
Access-Control-Request-Headers: Foo
Access-Control-Request-Method: GET
Content-Type: application/json
User-Agent: PostmanRuntime/7.29.2
Accept: */*
Postman-Token: 8c5bcaff-a2f5-4231-be32-1251c0db1794
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 116
 
{
"date": "2023-09-01 10:20:28",
"localDate": "2023-09-01",
"localDateTime": "2023-09-01 10:20:28"
}
 
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: application/json
Date: Fri, 01 Sep 2023 02:22:19 GMT
 
{"date":"2023-09-01 10:20:28","localDate":"2023-09-01","localDateTime":"2023-09-01 10:20:28"}

我们往测试端点 POST 了一个 JSON 字符,其中包含了3个日期字段,这些日期格式正是使用了我们在上述中定义的格式。

{
"date": "2023-09-01 10:20:28",
"localDate": "2023-09-01",
"localDateTime": "2023-09-01 10:20:28"
}

你可以看到请求体和响应体中的日期数据完全一致,Spring Boot 确实是按照我们指定的日期格式进行了序列化和反序列化。