9人参与 • 2026-09-17 • Java
用 spring boot 写接口,天天跟 json 打交道。jackson 是默认的 json 库,用起来方便,但一旦遇到日期格式、精度、脱敏、大数据量这些场景,光靠默认配置经常不够用。这篇文章从 objectmapper 入手,把一些经验整理出来,特别说一下脱敏和流式写入。
objectmapper 是 jackson 的核心入口,所有序列化和反序列化都经过它。内部维护了 jsonserializer 和 jsondeserializer、注解信息、类型解析,以及一堆 provider。简单说,它就是 json 操作的总管家。
序列化流程大概是这样:
objectmapper.writevalue(...),传入目标对象。objectmapper 根据对象的运行时类型,从 serializerfactory 找到对应的 jsonserializer。jsonserializer 把对象的每个字段写到 jsongenerator(后台是 outputstream 或 stringwriter)。一开始不理解这个流程也没关系,后面定制脱敏时就得跟 beanserializermodifier 打交道了。
spring boot 通过 jacksonautoconfiguration 自动帮你做了大部分事情:
objectmapper bean。spring.jackson.* 配置(日期格式、时区、命名策略等)初始化这个 bean。javatimemodule,让 java.time 包下的类型能正常序列化/反序列化。jackson2objectmapperbuildercustomizer 进一步定制。mappingjackson2httpmessageconverter 也用的是这个 objectmapper。所以一般情况下,不用自己 new objectmapper,只需要在配置文件或自定义配置里调整就行。但是注意,一旦你自己声明了 objectmapper bean,自动配置的那套东西很可能就不再生效,容易踩坑。优先用 jackson2objectmapperbuildercustomizer。
方式一:配置文件(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 来构建,但依然要谨慎。
说实话,我第一次看到 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 比较省心,可以参考上一节的方式二。
遇到过 bigdecimal 序列化后变成科学计数法的情况吗?比如 0.00000001 直接变成 1e-8,在金融系统里这是要出事的。两种方式能解决:
@jsonserialize(using = tostringserializer.class) 把 bigdecimal 转成字符串;@jsonformat(shape = jsonformat.shape.string)。两个其实效果一样,都是输出字符串,避免精度丢失。推荐用第二种,省得写序列化类。
默认情况下,null 字段会输出 "field": null,响应体臃肿,前端还要判空。全局配置 jsoninclude.include.non_null 后,null 就直接过滤掉了。要是想让 null 变成空字符串,就得自己写序列化器了——这个和后面脱敏的思路差不多,都是替换默认序列化器。先放这儿,后面有类似代码。
用户手机号、身份证号这种敏感信息,可不能直接塞到接口返回里。脱敏的需求很常见,管理员要看完整,普通用户只能看打码后的。要是每个接口都手动处理,太烦,也不容易统一。我习惯在序列化阶段自动干掉。
先定义一个注解 @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 序列化的构建过程。
写一个 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;。
sensitivecontext 用 threadlocal 保存:
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();
}
}
这样同一个接口,管理员返回完整数据,普通用户返回打码后的数据,业务代码完全不用改。
有一次做报表导出,几万条数据全放到 list 里再序列化,结果 jvm 直接 oom。后来用 jsongenerator 流式写,内存占用立马降下来了。思路很简单:不把数据一次性装进 list,而是从数据库游标里一条一条取,边取边写。
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 或迭代器。核心是:写一个对象就释放一个对象,内存占用基本恒定。
不想手写每个字段的话,可以让 objectmapper 的序列化器直接写到 jsongenerator:
try (jsongenerator generator = mapper.getfactory().creategenerator(outputstream)) {
generator.writestartarray();
while (cursor.hasnext()) {
mapper.writevalue(generator, cursor.next());
}
generator.writeendarray();
}
这样 mapper.writevalue 会走正常的序列化逻辑,包括之前定制的脱敏和日期格式,全都生效。所以流式输出的脱敏是天然支持的。
jsongenerator 一定要关,用 try-with-resources 或者 finally 关,不然输出可能不完整。content-type 和 characterencoding,然后从 httpservletresponse.getoutputstream() 创建 jsongenerator。注意别让 spring 再去处理返回值,否则可能重复写入。一般可以直接把输出写到 response 里,然后返回 null 或使用 responseentity 不适合,得用 void 方法。有同事问过我,yaml 也支持结构化数据,看着比 json 清晰,能不能用它做接口返回?我说你要是真在生产环境这么干,性能肯定得骂娘。yaml 是给配置文件准备的,解析器需要处理缩进、锚点、类型转换,状态多,速度自然慢。json 语法简单,解析器就是一个状态机,jackson 又做了大量优化,快一个数量级很正常。所以数据交换老老实实用 json,配置文件用 yaml 完全没问题,启动时解析一次,成本可以接受。
前后端字段命名风格经常不统一。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()) 就行。
jackson 定制这块儿,基本就是理解 objectmapper,然后记住几个关键扩展点:配置、自定义序列化器、beanserializermodifier、流式 jsongenerator。日期格式和 bigdecimal 精度属于常规操作,用注解或全局配置就能搞定;脱敏和流式写入稍微复杂,但原理不深。命名策略看团队规范,别为了追求完美把项目搞复杂。写代码嘛,够用就行。
以上就是springboot中jackson高级用法教学:序列化脱敏、日期格式统一与流式json性能优化的详细内容,更多关于springboot jackson用法的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论