SpringBoot中Jackson高级用法教学:序列化脱敏、日期格式统一与流式JSON性能优化
用 Spring Boot 写接口,天天跟 JSON 打交道。Jackson 是默认的 JSON 库,用起来方便,但一旦遇到日期格式、精度、脱敏、大数据量这些场景,光靠默认配置经常不够用。这篇文章从 ObjectMapper 入手,把一些经验整理出来,特别说一下脱敏和流式写入。
1. ObjectMapper 核心机制与 Spring Boot 自动配置
1.1 ObjectMapper 的角色
ObjectMapper 是 Jackson 的核心入口,所有序列化和反序列化都经过它。内部维护了 JsonSerializer 和 JsonDeserializer、注解信息、类型解析,以及一堆 Provider。简单说,它就是 JSON 操作的总管家。
序列化流程大概是这样:
- 调用
objectMapper.writeValue(...),传入目标对象。 ObjectMapper根据对象的运行时类型,从SerializerFactory找到对应的JsonSerializer。JsonSerializer把对象的每个字段写到JsonGenerator(后台是OutputStream或StringWriter)。- 最后输出 JSON 字符串。
一开始不理解这个流程也没关系,后面定制脱敏时就得跟 BeanSerializerModifier 打交道了。
1.2 Spring Boot 的自动配置
Spring Boot 通过 JacksonAutoConfiguration 自动帮你做了大部分事情:
- 类路径里有 Jackson 就自动生成一个
ObjectMapperBean。 - 读
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 动态脱敏上下文
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();
}
}
这样同一个接口,管理员返回完整数据,普通用户返回打码后的数据,业务代码完全不用改。
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-Type和CharacterEncoding,然后从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用法的资料请关注脚本之家其它相关文章!
相关文章
SpringBoot内嵌tomcat处理有特殊字符转义的问题
这篇文章主要介绍了SpringBoot内嵌tomcat处理有特殊字符转义的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2023-06-06


最新评论