自定义 Jackson 配置:支持 LocalDateTime 日期 API

本小节中,我们将为认证服务自定义 Jackson 配置,以支持 Java 8 中新的日期 API 。

1. 不支持 LocalDateTime 问题演示

image-20260204203107639

注意这里返回的时间类型并不是我们在前面设置的时间日期格式,其次如果我们按照我们先前设置的格式作为入参,同样也无法反序列化,这个我们可以做一下实验!

假设我们想让 /test3 接口支持传入参数 User 实体类,代码如下:

@PostMapping("/test3")
@ApiOperationLog(description = "测试接口3")
public Response<User> test2(@RequestBody User user) {
    return Response.success(user);
}

然后发送请求:

image-20260204203424801

再来看看后端控制台信息,有如下一行警告信息:

JSON parse error: Cannot deserialize value of type `java.time.LocalDateTime` from String "2026-02-04 12:00:00": Failed to deserialize `java.time.LocalDateTime` (with format 'ParseCaseSensitive(false)(Value(Year,4,10,EXCEEDS_PAD)'-'Value(MonthOfYear,2)'-'Value(DayOfMonth,2))'T'(Value(HourOfDay,2)':'Value(MinuteOfHour,2)[':'Value(SecondOfMinute,2)[Fraction(NanoOfSecond,0,9,DecimalPoint)]])'): (java.time.format.DateTimeParseException) Text '2026-02-04 12:00:00' could not be parsed at index 10]

提示我们 JSON 解析错误,无法将 2026-02-04 12:00:00 字符串解析为 java.time.LocalDateTime 日期类。

2. 自定义 Jackson 配置

这个我们直接把这个 Jackson的配置类封装为一个starter,这样方便我们在不同的服务中引入!首先我某地 hanserwei-framework下新建 hanserwei-spring-boot-starter-jackson模块:

image-20260204204733618

pom文件如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- 父项目配置 -->
    <parent>
        <!-- 组织ID -->
        <groupId>com.hanserwei</groupId>
        <!-- 父项目artifactId -->
        <artifactId>hanserwei-framework</artifactId>
        <!-- 版本号,使用变量引用 -->
        <version>${revision}</version>
    </parent>

    <!-- 当前模块的artifactId -->
    <artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
    <!-- 打包方式为jar包 -->
    <packaging>jar</packaging>

    <!-- 项目名称,引用artifactId -->
    <name>${project.artifactId}</name>
    <!-- 项目描述 -->
    <description>自定义JacksonStarter</description>

    <!-- 项目属性配置 -->
    <properties>
        <!-- 源代码编码格式 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- 依赖管理 -->
    <dependencies>
        <!-- hanserwei通用模块依赖 -->
        <dependency>
            <groupId>com.hanserwei</groupId>
            <artifactId>hanserwei-common</artifactId>
        </dependency>
        <!-- Spring Boot自动配置依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
    </dependencies>
</project>

我们先在 hanserwei-commonconstant包下定义一下我们常用的几个日期格式:

package com.hanserwei.framework.common.constant;

import java.time.format.DateTimeFormatter;

/**
 * @author hanser
 */
public interface DateConstants {

    /**
     * DateTimeFormatter:年-月-日 时:分:秒
     */
    DateTimeFormatter DATE_FORMAT_Y_M_D_H_M_S = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /**
     * DateTimeFormatter:年-月-日
     */
    DateTimeFormatter DATE_FORMAT_Y_M_D = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    /**
     * DateTimeFormatter:时:分:秒
     */
    DateTimeFormatter DATE_FORMAT_H_M_S = DateTimeFormatter.ofPattern("HH:mm:ss");

    /**
     * DateTimeFormatter:年-月
     */
    DateTimeFormatter DATE_FORMAT_Y_M = DateTimeFormatter.ofPattern("yyyy-MM");
}

然后回到我们的 CustomJacksonConfiguration 类中,Jackson3的配置方式和 Jackson2有些许不同,直接看我的代码,我代码中会有详细的注释:

package com.hanserwei.jackson.config;

import com.hanserwei.framework.common.constant.DateConstants;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.ext.javatime.deser.LocalDateDeserializer;
import tools.jackson.databind.ext.javatime.deser.LocalDateTimeDeserializer;
import tools.jackson.databind.ext.javatime.deser.LocalTimeDeserializer;
import tools.jackson.databind.ext.javatime.deser.YearMonthDeserializer;
import tools.jackson.databind.ext.javatime.ser.LocalDateSerializer;
import tools.jackson.databind.ext.javatime.ser.LocalDateTimeSerializer;
import tools.jackson.databind.ext.javatime.ser.LocalTimeSerializer;
import tools.jackson.databind.ext.javatime.ser.YearMonthSerializer;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.module.SimpleModule;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.util.TimeZone;

/**
 * @author hanser
 */
@AutoConfiguration
public class CustomJacksonConfiguration {

    @Bean
    public JsonMapper jsonMapper() {
        SimpleModule customModule = new SimpleModule();
        // 支持 LocalDateTime、LocalDate、LocalTime
        customModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateConstants.DATE_FORMAT_Y_M_D_H_M_S));
        customModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateConstants.DATE_FORMAT_Y_M_D_H_M_S));
        customModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateConstants.DATE_FORMAT_Y_M_D));
        customModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateConstants.DATE_FORMAT_Y_M_D));
        customModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateConstants.DATE_FORMAT_H_M_S));
        customModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateConstants.DATE_FORMAT_H_M_S));
        // 支持 YearMonth
        customModule.addSerializer(YearMonth.class, new YearMonthSerializer(DateConstants.DATE_FORMAT_Y_M));
        customModule.addDeserializer(YearMonth.class, new YearMonthDeserializer(DateConstants.DATE_FORMAT_Y_M));


        return JsonMapper.builder()
                // 在反序列化时,忽略在 JSON 中存在但 Java 对象中不存在的属性,防止报错
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                // 在序列化时,允许序列化空的 POJO 类(没有属性的类),防止报错
                .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
                // 设置默认时区为东八区(北京时间)
                .defaultTimeZone(TimeZone.getTimeZone("GMT+8"))
                // 添加自定义模块
                .addModule(customModule)
                // 自动查找并添加所有可用的 Jackson 模块
                .findAndAddModules()
                .build();
    }
}

但是,现在有个问题,在之前 hanserwei-common 公共模块中,我们封装了一个 JsonUtils 工具类,里面的序列化、反序列是有重复定义的,怎么能统一复用上面的呢? 这样就不用适配多处了,代码看起来也优雅很多。

为了解决上面提到的问题,我们可以在 JsonUtils 工具类中,定义一个 init() 初始化方法,代码如下:

package com.hanserwei.framework.common.util;

import tools.jackson.databind.json.JsonMapper;

/**
 * JSON 工具类,提供对象与 JSON 字符串之间的转换功能
 *
 * @author hanser
 */
public class JsonUtils {

    /**
     * 初始化 Jackson 的 JsonMapper 实例
     */
    private static JsonMapper JSON_MAPPER = new JsonMapper();
  
    /**
     * 初始化:统一使用 Spring Boot 个性化配置的 ObjectMapper
     *
     * @param jsonMapper 自定义的 JsonMapper 实例
     */
    public static void init(JsonMapper jsonMapper) {
        JSON_MAPPER = jsonMapper;
    }

    /**
     * 将对象序列化为 JSON 字符串
     *
     * @param object 需要转换的对象
     * @return 转换后的 JSON 字符串
     */
    public static String toJsonString(Object object) {
        return JSON_MAPPER.writeValueAsString(object);
    }
}

然后在 CustomJacksonConfiguration注入bean的时候调用一下 init方法:

image-20260204210841118

最后一步,在 org.springframework.boot.autoconfigure.AutoConfiguration.imports里面导入自动配置类的全类名:

image-20260204205934785

为了方便在其他模块导入这个starter,我们在最外层的pom文件中定义这个starter的版本!

<dependency>
    <groupId>com.hanserwei</groupId>
    <artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
    <version>${revision}</version>
</dependency>

image-20260204210141271

然后在 hannote-auth服务中导入我们定义的starter:

image-20260204210332965

这个时候我们重启一下项目,试验一下!

image-20260204210928757

入参也没问题:

image-20260204211037886

AOP切面日志也没问题:

image-20260204211118357