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

 更新时间:2026年09月17日 08:26:21   作者:(farerboy)  
用 Spring Boot 写接口,天天跟 JSON 打交道,Jackson 是默认的 JSON 库,用起来方便,但一旦遇到日期格式、精度、脱敏、大数据量这些场景,光靠默认配置经常不够用,本文讲从 ObjectMapper 入手,把一些经验整理出来,特别说一下脱敏和流式写入,希望对大家有所帮助

用 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用法的资料请关注脚本之家其它相关文章!

相关文章

  • dubbo自定义异常的完整步骤与测试

    dubbo自定义异常的完整步骤与测试

    最近在项目上遇到一个有关dubbo的问题,想着给大家总结下,这篇文章主要给大家介绍了关于dubbo自定义异常的完整步骤与测试的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-06-06
  • Java C++题解leetcode904水果成篮

    Java C++题解leetcode904水果成篮

    这篇文章主要为大家介绍了Java C++题解leetcode904水果成篮示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • java实现调用http请求的五种常见方式

    java实现调用http请求的五种常见方式

    在实际开发过程中,我们经常需要调用对方提供的接口或测试自己写的接口是否合适,本文主要介绍了java实现调用http请求的四种常见方式,感兴趣的可以了解一下
    2024-07-07
  • nodejs连接dubbo服务的java工程实现示例

    nodejs连接dubbo服务的java工程实现示例

    这篇文章主要介绍了在项目迁移中,nodejs连接dubbo服务的java工程实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-03-03
  • SpringBoot内嵌tomcat处理有特殊字符转义的问题

    SpringBoot内嵌tomcat处理有特殊字符转义的问题

    这篇文章主要介绍了SpringBoot内嵌tomcat处理有特殊字符转义的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06
  • 关于BigDecimal类型之间比较问题

    关于BigDecimal类型之间比较问题

    这篇文章主要介绍了关于BigDecimal类型之间比较问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • java7 简化变参方法调用实例方法

    java7 简化变参方法调用实例方法

    在本篇文章里我们给大家整理的是关于java7 简化变参方法调用实例方法以及实例代码,需要的朋友们学习下。
    2019-11-11
  • Spring+MyBatis多数据源配置实现示例

    Spring+MyBatis多数据源配置实现示例

    本篇文章主要介绍了Spring+MyBatis多数据源配置实现示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • Java全面解析IO流相关知识

    Java全面解析IO流相关知识

    这篇文章主要介绍了IO流相关知识,包括File,字节流,字符流,特殊操作流(标准输入流,标准输出流,对象序列化与反序列化,properties与IO流结合)相关知识的总结
    2021-08-08
  • SpringBoot文件上传接口并发性能调优

    SpringBoot文件上传接口并发性能调优

    在一个项目现场,文件上传接口(文件500K)QPS只有30,这个并发性能确实堪忧,此文记录出坑过程,文中通过代码示例讲解的非常详细,具有一定的参考价值,需要的朋友可以参考下
    2024-06-06

最新评论