当前位置: 代码网 > it编程>编程语言>Java > SpringBoot中Jackson高级用法教学:序列化脱敏、日期格式统一与流式JSON性能优化

SpringBoot中Jackson高级用法教学:序列化脱敏、日期格式统一与流式JSON性能优化

2026年09月17日 Java 我要评论
用 spring boot 写接口,天天跟 json 打交道。jackson 是默认的 json 库,用起来方便,但一旦遇到日期格式、精度、脱敏、大数据量这些场景,光靠默认配置经常不够用。这篇文章从

用 spring boot 写接口,天天跟 json 打交道。jackson 是默认的 json 库,用起来方便,但一旦遇到日期格式、精度、脱敏、大数据量这些场景,光靠默认配置经常不够用。这篇文章从 objectmapper 入手,把一些经验整理出来,特别说一下脱敏和流式写入。

1. objectmapper 核心机制与 spring boot 自动配置

1.1 objectmapper 的角色

objectmapper 是 jackson 的核心入口,所有序列化和反序列化都经过它。内部维护了 jsonserializerjsondeserializer、注解信息、类型解析,以及一堆 provider。简单说,它就是 json 操作的总管家。

序列化流程大概是这样:

  1. 调用 objectmapper.writevalue(...),传入目标对象。
  2. objectmapper 根据对象的运行时类型,从 serializerfactory 找到对应的 jsonserializer
  3. jsonserializer 把对象的每个字段写到 jsongenerator(后台是 outputstreamstringwriter)。
  4. 最后输出 json 字符串。

一开始不理解这个流程也没关系,后面定制脱敏时就得跟 beanserializermodifier 打交道了。

1.2 spring boot 的自动配置

spring boot 通过 jacksonautoconfiguration 自动帮你做了大部分事情:

  • 类路径里有 jackson 就自动生成一个 objectmapper bean。
  • spring.jackson.* 配置(日期格式、时区、命名策略等)初始化这个 bean。
  • 注册 javatimemodule,让 java.time 包下的类型能正常序列化/反序列化。
  • 允许通过 jackson2objectmapperbuildercustomizer 进一步定制。
  • 在 web mvc 里,mappingjackson2httpmessageconverter 也用的是这个 objectmapper

所以一般情况下,不用自己 new objectmapper,只需要在配置文件或自定义配置里调整就行。但是注意,一旦你自己声明了 objectmapper bean,自动配置的那套东西很可能就不再生效,容易踩坑。优先用 jackson2objectmapperbuildercustomizer

1.3 自定义 objectmapper 的方式

方式一:配置文件(application.yml)

spring:
  jackson:
    date-format: yyyy-mm-dd hh:mm:ss
    time-zone: asia/shanghai
    default-property-inclusion: non_null

这个简单,但对 localdatetime 不生效——date-format 只管 java.util.date。项目里要是用了 localdatetime,还得靠下面的方式。

方式二:jackson2objectmapperbuildercustomizer

这是官方推荐的定制入口,能在自动配置的基础上加东西。比如全局统一 localdatetime 的格式:

@configuration
public class jacksonconfig {
    @bean
    public jackson2objectmapperbuildercustomizer jacksoncustomizer() {
        return builder -> {
            builder.serializerbytype(localdatetime.class,
                    new localdatetimeserializer(datetimeformatter.ofpattern("yyyy-mm-dd hh:mm:ss")));
            builder.deserializerbytype(localdatetime.class,
                    new localdatetimedeserializer(datetimeformatter.ofpattern("yyyy-mm-dd hh:mm:ss")));
            builder.serializationinclusion(jsoninclude.include.non_null);
        };
    }
}

方式三:直接定义 objectmapper bean

不推荐。除非有特殊需求,否则别这么干。因为自己 new objectmapper 会丢掉 spring boot 自动配置的一大堆好东西,比如模块自动注册、属性文件等。真需要,建议用 jackson2objectmapperbuilder 来构建,但依然要谨慎。

2. 日期格式、bigdecimal 精度与空值处理

2.1 localdatetime 的格式化

说实话,我第一次看到 localdatetime 默认序列化出来的 2024-06-01t10:30:00 时,第一反应是这玩意前端能认识吗?果然,前端同学抱怨过。解决办法最简单的就是在字段上加 @jsonformat

public class userdto {
    @jsonformat(pattern = "yyyy-mm-dd hh:mm:ss", timezone = "asia/shanghai")
    private localdatetime createtime;
}

但每个日期字段都加,太啰嗦。全局配一个 localdatetimeserializer 比较省心,可以参考上一节的方式二。

2.2 bigdecimal 精度问题

遇到过 bigdecimal 序列化后变成科学计数法的情况吗?比如 0.00000001 直接变成 1e-8,在金融系统里这是要出事的。两种方式能解决:

  • @jsonserialize(using = tostringserializer.class)bigdecimal 转成字符串;
  • @jsonformat(shape = jsonformat.shape.string)

两个其实效果一样,都是输出字符串,避免精度丢失。推荐用第二种,省得写序列化类。

2.3 空值处理

默认情况下,null 字段会输出 "field": null,响应体臃肿,前端还要判空。全局配置 jsoninclude.include.non_null 后,null 就直接过滤掉了。要是想让 null 变成空字符串,就得自己写序列化器了——这个和后面脱敏的思路差不多,都是替换默认序列化器。先放这儿,后面有类似代码。

3. 自定义序列化器实现敏感字段动态脱敏

用户手机号、身份证号这种敏感信息,可不能直接塞到接口返回里。脱敏的需求很常见,管理员要看完整,普通用户只能看打码后的。要是每个接口都手动处理,太烦,也不容易统一。我习惯在序列化阶段自动干掉。

3.1 需求分析

  • 定义脱敏类型:手机号、身份证、银行卡号等。
  • 支持动态策略:根据当前用户的角色决定是否脱敏。
  • 对业务代码无侵入,dto 照常写。

3.2 自定义注解和序列化方案

先定义一个注解 @sensitivefield,标注在需要脱敏的字段上:

@retention(retentionpolicy.runtime)
@target(elementtype.field)
public @interface sensitivefield {
    sensitivestrategy strategy() default sensitivestrategy.mobile;
}

还有脱敏策略枚举:

public enum sensitivestrategy {
    mobile("手机号脱敏"),
    id_card("身份证号脱敏");
    // ...
}

关键是:jsonserializer 默认按类型找序列化器,拿不到字段上的注解。那就要借助 beanserializermodifier 在构建 bean 序列化器时,对每个属性进行检查,如果发现有 @sensitivefield,就把默认序列化器替换成我们的脱敏序列化器。这里需要理解 jackson 对 bean 序列化的构建过程。

3.3 利用 beanserializermodifier 拦截字段序列化器

写一个 sensitivefieldserializer,持有默认序列化器和脱敏策略。序列化时根据上下文判断是否脱敏,脱敏就写遮蔽后的字符串,否则就委托给默认序列化器:

public class sensitivefieldserializer extends jsonserializer<object> {
    private final jsonserializer<object> delegate;
    private final sensitivestrategy strategy;

    public sensitivefieldserializer(jsonserializer<object> delegate, sensitivestrategy strategy) {
        this.delegate = delegate;
        this.strategy = strategy;
    }

    @override
    public void serialize(object value, jsongenerator gen, serializerprovider serializers) throws ioexception {
        if (value == null) {
            gen.writenull();
            return;
        }
        if (sensitivecontext.isadmin()) {
            delegate.serialize(value, gen, serializers);
            return;
        }
        string text = value.tostring();
        gen.writestring(mask(text, strategy));
    }

    private string mask(string value, sensitivestrategy strategy) {
        switch (strategy) {
            case mobile:
                return value.replaceall("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
            case id_card:
                return value.replaceall("(\\d{4})\\d{10}(\\w{4})", "$1**********$2");
            default:
                return value;
        }
    }
}

这里 sensitivecontext 是个 threadlocal,用来存放当前用户是否是管理员的信息。具体在哪儿设置,可以放到拦截器或过滤器里,注意在 aftercompletion 里 remove,不然线程池复用会出问题。

然后实现 beanserializermodifier

public class sensitivebeanserializermodifier extends beanserializermodifier {
    @override
    public jsonserializer<?> changepropertyserializer(serializationconfig config,
            beandescription beandesc, beanpropertydefinition property,
            jsonserializer<?> defaultserializer) {
        annotatedfield field = property.getfield();
        if (field != null) {
            sensitivefield annotation = field.getannotation(sensitivefield.class);
            if (annotation != null) {
                return new sensitivefieldserializer((jsonserializer<object>) defaultserializer, annotation.strategy());
            }
        }
        return super.changepropertyserializer(config, beandesc, property, defaultserializer);
    }
}

beanserializermodifier 挂到 objectmapper 上,用 jackson2objectmapperbuildercustomizer 集成:

@configuration
public class jacksonsensitiveconfig {
    @bean
    public jackson2objectmapperbuildercustomizer sensitivecustomizer() {
        return builder -> builder.postconfigurer(mapper -> {
            mapper.setserializerfactory(mapper.getserializerfactory().withserializermodifier(new sensitivebeanserializermodifier()));
            return mapper;
        });
    }
}

注意 postconfigurer 的 lambda 要返回 objectmapper,别忘写 return mapper;

3.4 动态脱敏上下文

sensitivecontextthreadlocal 保存:

public class sensitivecontext {
    private static final threadlocal<boolean> admin = new threadlocal<>();

    public static void setadmin(boolean admin) {
        admin.set(admin);
    }

    public static boolean isadmin() {
        return boolean.true.equals(admin.get());
    }

    public static void clear() {
        admin.remove();
    }
}

这样同一个接口,管理员返回完整数据,普通用户返回打码后的数据,业务代码完全不用改。

4. jsongenerator 流式写入:应对大数据量

有一次做报表导出,几万条数据全放到 list 里再序列化,结果 jvm 直接 oom。后来用 jsongenerator 流式写,内存占用立马降下来了。思路很简单:不把数据一次性装进 list,而是从数据库游标里一条一条取,边取边写。

4.1 基本用法

objectmapper mapper = new objectmapper();
try (jsongenerator generator = mapper.getfactory().creategenerator(outputstream, jsonencoding.utf8)) {
    generator.writestartarray();
    while (rs.next()) {
        generator.writestartobject();
        generator.writestringfield("id", rs.getstring("id"));
        generator.writestringfield("name", rs.getstring("name"));
        generator.writenumberfield("amount", rs.getbigdecimal("amount"));
        generator.writeendobject();
    }
    generator.writeendarray();
}

这里 rs 是数据库游标,也可以换成 mybatis 的 cursor 或迭代器。核心是:写一个对象就释放一个对象,内存占用基本恒定。

4.2 与 objectmapper 序列化器配合

不想手写每个字段的话,可以让 objectmapper 的序列化器直接写到 jsongenerator

try (jsongenerator generator = mapper.getfactory().creategenerator(outputstream)) {
    generator.writestartarray();
    while (cursor.hasnext()) {
        mapper.writevalue(generator, cursor.next());
    }
    generator.writeendarray();
}

这样 mapper.writevalue 会走正常的序列化逻辑,包括之前定制的脱敏和日期格式,全都生效。所以流式输出的脱敏是天然支持的。

4.3 注意事项

  • jsongenerator 一定要关,用 try-with-resources 或者 finally 关,不然输出可能不完整。
  • 如果直接在 spring mvc 响应里写,要设置 content-typecharacterencoding,然后从 httpservletresponse.getoutputstream() 创建 jsongenerator。注意别让 spring 再去处理返回值,否则可能重复写入。一般可以直接把输出写到 response 里,然后返回 null 或使用 responseentity 不适合,得用 void 方法。

5. yaml 与 json 性能对比

有同事问过我,yaml 也支持结构化数据,看着比 json 清晰,能不能用它做接口返回?我说你要是真在生产环境这么干,性能肯定得骂娘。yaml 是给配置文件准备的,解析器需要处理缩进、锚点、类型转换,状态多,速度自然慢。json 语法简单,解析器就是一个状态机,jackson 又做了大量优化,快一个数量级很正常。所以数据交换老老实实用 json,配置文件用 yaml 完全没问题,启动时解析一次,成本可以接受。

6. 字段命名策略整合技巧

前后端字段命名风格经常不统一。java 后端一般用驼峰 username,有的前端团队习惯下划线 user_name,如果不想跟他们对齐,就在 jackson 里全局改:

spring:
  jackson:
    property-naming-strategy: snake_case

这样 username 自动映射成 user_name。要是某个字段想保留驼峰,就用 @jsonproperty 显式指定名字,优先级最高。比如:

public class userdto {
    @jsonproperty("emailaddress")
    private string emailaddress;
}

这种组合用法很常见,全局策略 + 局部覆盖,既省事又灵活。自定义命名策略的场景很少,除非有特别奇葩的要求,才需要继承 propertynamingstrategies.namingbase,然后重写 translate。比如:

public class customnamingstrategy extends propertynamingstrategies.namingbase {
    @override
    public string translate(string propertyname) {
        // 自定义转换逻辑
        return propertyname;
    }
}

然后在 jackson2objectmapperbuildercustomizer 里设置 builder.propertynamingstrategy(new customnamingstrategy()) 就行。

7. 总结

jackson 定制这块儿,基本就是理解 objectmapper,然后记住几个关键扩展点:配置、自定义序列化器、beanserializermodifier、流式 jsongenerator。日期格式和 bigdecimal 精度属于常规操作,用注解或全局配置就能搞定;脱敏和流式写入稍微复杂,但原理不深。命名策略看团队规范,别为了追求完美把项目搞复杂。写代码嘛,够用就行。

以上就是springboot中jackson高级用法教学:序列化脱敏、日期格式统一与流式json性能优化的详细内容,更多关于springboot jackson用法的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com