跳至正文

个性化配置jackson序列化与反序列化–仿大麦网的微服务项目(2)



在上一篇中,我们探讨了项目的基础构建,而本篇我将详细剖析项目的common模块的设置,我们将深入一个在微服务开发中至关重要的环节——Jackson 的个性化配置。在任何一个真实的、尤其是高并发的线上项目中,默认的 JSON 序列化行为往往无法满足我们对API 健壮性、统一性和前端友好性的全部要求。

本文将通过分析项目中的 JacksonCustom 配置类,将我们之前遇到的所有疑惑串联起来,带你彻底搞懂为什么要以及如何深度定制 Jackson。

1. 为什么需要个性化 Jackson?一切从“不满足”开始

开箱即用的 Jackson 功能强大,但它的默认设置在真实场景下会带来几个典型问题:

日期格式混乱:java.util.Date 默认被序列化为一长串数字时间戳(如 1720854619000),而 java.time.LocalDateTime 则被序列化为 ISO 标准字符串(如 “2025-07-13T16:10:19″)。这种不统一的格式会给前后端联调带来影响。

恼人的 null 值:默认情况下,对象中为 null 的字段在序列化后也会在 JSON 中显示为 null。这不仅增加了数据传输量,还要求前端对每一个可能为 null 的字段都做一次判断,非常繁琐。我们更期望一个 null 的字符串返回 “”,一个 null 的列表返回 []。

为了解决这些“不满足”,提供一个统一、干净、可预期的 API,深度定制 Jackson 势在必行。

2. 定制化的两种武器:addSerializer vs setSerializerModifier

Jackson 提供了多种定制手段,在本项目中主要使用了两种定制手段,它们分别是:

addSerializer (针对单个bean): 一对一替换。它针对某一个特定的类(如 Date.class),提供一个全新的序列化方案来完全替代 Jackson 的默认行为。它的作用范围精确,指哪打哪。

setSerializerModifier (针对所有bean): 一对多修改。它不直接替换某个类的序列化器,而是像一个“质检经理”,在 Jackson 为各种对象(尤其是 POJO Beans)创建好默认的序列化器之后,对它们进行统一的、全局的修改或增强。它的作用范围广,适合应用通用规则。

3. 实战解析:仿大麦网的 JacksonCustom 配置

让我们深入代码,看看这些武器是如何协同作战的。

第一战:用 setSerializerModifier 统一处理 null 值

Java
simpleModules[0] = new SimpleModule().setSerializerModifier(new JsonCustomSerializer());
Java
public class JsonCustomSerializer extends BeanSerializerModifier {
    @Override
    public List<BeanPropertyWriter> changeProperties(...) {
        for (BeanPropertyWriter writer : beanProperties) {
            // ...判断 writer 的类型(String, List, Number...)
            if (js != null) {
                // 核心:为这个字段的 null 值指定一个特殊的序列化器
                writer.assignNullSerializer(js);
            }
        }
        return beanProperties;
    }
}

public com.fasterxml.jackson.databind.JsonSerializer<Object> judgeType(BeanPropertyWriter writer) {
    JavaType javaType = writer.getType();
    Class<?> clazz = javaType.getRawClass();
    if (String.class.isAssignableFrom(clazz)) {
        return new com.fasterxml.jackson.databind.JsonSerializer<Object>() {
            @Override
            public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
                    throws IOException {
                gen.writeString("");
            }
        };
    }
    if (Number.class.isAssignableFrom(clazz)) {
        return new com.fasterxml.jackson.databind.JsonSerializer<Object>() {
            @Override
            public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
                    throws IOException {
                gen.writeString("");
            }
        };
    }
    if (Boolean.class.isAssignableFrom(clazz)) {
        return new com.fasterxml.jackson.databind.JsonSerializer<Object>() {
            @Override
            public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
                    throws IOException {
                gen.writeBoolean(false);
            }
        };
    }
    if (java.util.Date.class.isAssignableFrom(clazz)) {
        return new com.fasterxml.jackson.databind.JsonSerializer<Object>() {
            @Override
            public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
                    throws IOException {
                gen.writeString("");
            }
        };
    }
    if (clazz.equals(DateTime.class)) {
        return new com.fasterxml.jackson.databind.JsonSerializer<Object>() {
            @Override
            public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
                    throws IOException {
                gen.writeString("");
            }
        };
    }
    if (clazz.isArray() || clazz.equals(List.class) || clazz.equals(Set.class)) {
        return new com.fasterxml.jackson.databind.JsonSerializer<Object>() {
            @Override
            public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
                    throws IOException {
                gen.writeStartArray();
                gen.writeEndArray();
            }
        };
    }
    return null;
}

这段代码是整个定制化方案的精髓。

我们注册了一个 BeanSerializerModifier(通过 JsonCustomSerializer 实现)。

Jackson 在序列化任何一个对象前,都会把该对象所有“字段的序列化指令清单” (List<BeanPropertyWriter> beanProperties) 交给我们的 changeProperties 方法。

我们遍历这个清单,检查每一个字段的类型。如果字段是 String,我们就为它的 null 值指定一个输出 “” 的序列化器;如果字段是 List,就指定一个输出 [] 的序列化器,以此类推。

通过这种方式,我们用一个“质检经理”就实现了对所有对象 null 值的全局、统一处理,优雅且高效。

4.对beanProperties的介绍

beanProperties 就是一个装满了上述“字段序列化指令”(BeanPropertyWriter) 的 List 集合。

当 Jackson 准备序列化一个对象时(比如一个 Car 对象,它有 colortype 两个字段),它会执行以下流程:

  1. 分析对象: Jackson 首先分析 Car.class,找出所有需要被序列化的公开字段。
  2. 创建指令清单: 为 colortype 这两个字段,分别创建两个默认的 BeanPropertyWriter 对象,并将它们放入一个列表中。这个列表就是 beanProperties
  3. 调用 changeProperties 方法: 在进行真正的序列化之前,Jackson 会调用你的 BeanSerializerModifier 中的 changeProperties 方法,并将刚刚创建的那个 beanProperties 清单作为参数传给你。
  4. 给予你修改的机会: 这是最关键的一步。你现在拿到了完整的“指令清单”,你可以在这个清单上做任何你想做的事情:
    • 修改指令:遍历清单,改变某个或某些字段的序列化行为。
    • 删除指令:从清单中移除一个 BeanPropertyWriter,这样对应的字段就不会出现在最终的 JSON 中。
    • 重排指令:改变清单中元素的顺序,从而控制 JSON 中字段的顺序。
    • 添加指令:在清单中加入一个新的 BeanPropertyWriter,动态添加一个原本不存在的字段。

用 addSerializer 精确格式化日期

Java
// 处理遗留的 Date 类
simpleModules[2] = new SimpleModule().addDeserializer(Date.class, new DateJsonDeserializer());

// 处理现代的 LocalDateTime 类
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
simpleModules[3] = new SimpleModule().addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dtf));

对于 LocalDateTime:我们并非完全重写,而是配置了 Jackson 自带的 LocalDateTimeSerializer。

对于 Date:由于 Jackson 默认将它处理为数字时间戳,与我们想要的字符串格式差异巨大,我们必须完全重写一个新的 DateJsonDeserializer 来实现我们的逻辑。

这就引出了一个经典问题……

5.处理历史的“债”:为何同时存在 Date 和 LocalDateTime?

在一个项目中看到这两种类型并存,你可能会觉得“多此一举”。但这往往是历史遗留问题和兼容性妥协的真实写照。

历史原因:项目的老代码诞生于 Java 8 之前,使用了 Date。新代码则拥抱了更优秀的 LocalDateTime。

兼容性原因:一些老旧的第三方库或外部 API 可能只认 Date。

最佳实践不是强行替换所有 Date,而是有效隔离:在系统的边缘(如 DAO 层)完成 Date 与 LocalDateTime 的转换,在核心业务逻辑中,永远只使用现代的 java.time 类型。

总结

经过本篇博客的介绍,我们不仅理解了仿大麦网项目中 Jackson 配置的每一行代码,更重要的是建立了一套解决实际问题的思维框架:

识别痛点:认识到默认配置在日期和 null 处理上的不足。

选择武器:理解 addSerializer(针对特定类)和 setSerializerModifier(应用全局规则)的适用场景。

拥抱现实:学会通过隔离和适配来优雅地处理像 Date 这样的历史遗留问题。

有关jackson个性配置的文章推荐:https://www.baeldung.com/jackson-object-mapper-tutorial

文章末尾提供了一本作者编纂的免费电子书(介绍jackson的相关使用技巧),下载链接:https://nihiler.cn/wp-content/uploads/2025/07/DoJSONwithJackson.pdf

接下来是一些jackson常用feature:

分类 Feature / 配置项 作用 (Effect) 推荐配置 (Recommended Setting)
解析特性 (Parser) 📖 ALLOW_SINGLE_QUOTES 允许使用单引号包裹字段名和值。 true (启用) 理由:增强对不规范 JSON 的兼容性。
解析特性 (Parser) 📖 ALLOW_UNQUOTED_FIELD_NAMES 允许字段名(key)不使用引号。 true (启用) 理由:同上,提高健壮性。
解析特性 (Parser) 📖 ALLOW_TRAILING_COMMA 允许数组或对象末尾有多余的逗号。 true (启用) 理由:方便开发,兼容现代编码习惯。
反序列化 (Deserialization) 🧩 FAIL_ON_UNKNOWN_PROPERTIES 当 JSON 有未知字段时,是否抛出异常。 false (禁用) 理由:强烈推荐。提高 API 向后兼容性。
反序列化 (Deserialization) 🧩 ACCEPT_EMPTY_STRING_AS_NULL_OBJECT 是否将空字符串 "" 视作 null 对象。 true (启用) 理由:统一处理前端空表单提交,简化后端逻辑。
序列化 (Serialization) ✍️ INDENT_OUTPUT 是否将输出的 JSON 格式化(美化输出)。 开发环境: true
生产环境: false 理由:便于调试,但生产环境需考虑性能和带宽。
序列化 (Serialization) ✍️ FAIL_ON_EMPTY_BEANS 序列化空对象时是否抛出异常。 false (禁用) 理由:允许传递空对象 {} 作为有效载荷。
序列化 (Serialization) ✍️ WRITE_DATES_AS_TIMESTAMPS 是否将 java.util.Date 序列化为数字时间戳。 false (禁用) 理由:通常倾向于输出可读性更好的字符串格式。
其他重要配置 (Other) ⚙️ serializationInclusion 控制值为 nullempty 的字段是否参与序列化。 JsonInclude.Include.NON_NULL 理由:保持 JSON 载荷干净、轻量。

感谢阅读!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注