news 2026/9/14 21:46:09

Spring 与 JSON 序列化:Jackson 配置陷阱与性能优化实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring 与 JSON 序列化:Jackson 配置陷阱与性能优化实战

Spring 与 JSON 序列化:Jackson 配置陷阱与性能优化实战

1. 从一个线上故障说起:为什么 JSON 序列化会引发 500?

先看一个真实场景:你负责一个订单服务,接口返回给前端的订单对象里有一个createdAt字段。某天升级后,前端突然收到一串奇怪的数字:1691234567890。前端说“我要的是2023-08-05 12:34:56,这串毫秒数我没法用”。后端同学查了半天,发现是 Jackson 默认把LocalDateTime序列化成了时间戳,而不是 ISO 字符串。

更糟的是,另一个接口在返回用户列表时,响应时间从 200ms 涨到了 2 秒。日志里没有异常,但 CPU 飙高。查下去发现是某个对象的字段特别多,每次序列化都要反射解析所有getter,并且开了FAIL_ON_EMPTY_BEANS,导致大量警告和异常处理开销。

这些问题的根源,在于 Spring Boot 默认使用 Jackson 作为 JSON 序列化工具,而 Jackson 的默认配置、Spring 的自动装配,以及我们自己对业务对象的建模方式,三者叠加后产生了很多隐蔽的坑。

本文会顺着这些故障,先帮你建立 Jackson 的整体认知框架,再逐个拆解核心机制,最后给出三个可以运行的示例,覆盖自定义序列化、多态处理和性能优化,让你在真实项目里少踩坑。

2. 一句话模型与整体框架

先记住一句话模型:

JSON 序列化,就是把 Java 对象“翻译”成 JSON 字符串的过程,Jackson 是翻译官,Spring 负责安排翻译官上岗。

把翻译过程拆开看,有三个角色:

  • 源对象:你要输出的 Java 对象,比如OrderUser
  • Jackson 序列化器:负责读取对象的属性,并决定如何把属性值转换成 JSON 节点。它内部包含ObjectMapper(翻译总控)和一系列JsonSerializer(具体翻译员)。
  • 输出流:序列化结果写入的地方,可能是HttpServletResponse的输出流,也可能是字符串。

一次典型的 JSON 输出流程可以画成下面的图(用 ASCII):

+---------------+ 读属性 +----------------+ 生成JSON +--------------+ | Java 对象 | -------------> | ObjectMapper | ---------------> | JSON 输出 | | (Order) | (反射/getter) | (序列化核心) | (写入Generator) | (字符串/流) | +---------------+ +----------------+ +--------------+ ^ | | 遇到特殊类型? | v | +----------------+ +-------------------------| 自定义序列化器 | (SerializerProvider) | (如处理日期/多态) | +----------------+

一次用户请求会经历:Controller 方法返回对象 → Spring 调用MappingJackson2HttpMessageConverter→ 内部创建/复用ObjectMapper→ 调用writeValue()序列化 → 写入 HTTP 响应体。

理解这个链路后,我们就知道:要改变输出格式,要么改对象(用注解),要么改序列化器(自定义),要么改全局配置(配置ObjectMapper),三者对应了不同的影响范围。

3. Jackson 核心机制剖析

3.1 ObjectMapper 的角色与配置优先级

ObjectMapper是整个 JSON 序列化的中央处理器。它负责分配序列化器、读取配置、执行写入。Spring Boot 自动配置了一个ObjectMapperbean,但你可以通过三种方式覆盖它:

  1. application.yml中配置标准属性:例如日期格式、时区、缩进输出等。
  2. 自定义Jackson2ObjectMapperBuilderCustomizer:以编程方式微调ObjectMapper,适合复杂逻辑。
  3. 直接定义ObjectMapperbean:完全接管,风险是丢失 Spring 的默认配置(如 JavaTimeModule 注册)。

理解配置优先级有助于排查“我改了配置怎么没生效”的问题:属性文件最低,Customizer次之,显式 Bean 最高(但可能关掉了默认)。

3.2 默认序列化流程与反射开销

ObjectMapper.writeValue()被调用时,它经历以下步骤(简化):

  1. 检查目标对象的类型,找到对应的JsonSerializer
  2. 如果没有缓存,则通过反射分析类结构,收集可序列化属性(通常是getter方法)。
  3. 调用序列化器,生成 JSON 字段名和值。
  4. 对于复杂对象,递归处理其字段值,直到所有字段都是基本类型、数组或容器。

这里的开销主要在于首次反射分析和后续的方法调用。Jackson 默认会缓存已分析类的序列化器,但如果你频繁创建新ObjectMapper(比如每次请求都 new),或者用了动态代理类,缓存就会失效,导致性能下降。

3.3 常见序列化器:JavaTimeModule 与 JSR310

Java 8 时间类型(LocalDateLocalDateTime)默认不被 Jackson 支持,需要注册JavaTimeModule。Spring Boot 会自动注册它,但默认把时间序列化为数组或时间戳。要输出成 ISO 字符串,必须配置write-dates-as-timestamps: false。区别如下:

配置项默认值输出示例适用场景
spring.jackson.serialization.write-dates-as-timestamps: true是(Spring Boot 2.x)1691234567890传输效率高,前端处理麻烦
false"2023-08-05T12:34:56"可读性好,符合大多数 API 规范

许多项目想输出yyyy-MM-dd HH:mm:ss,则必须自定义格式,我们会在示例中演示。

3.4 注解驱动的自定义序列化

Jackson 提供了丰富的注解来定制序列化,比如@JsonProperty重命名字段,@JsonFormat格式化日期,@JsonIgnore忽略字段,@JsonSerialize指定自定义序列化器。它们可以精确控制单一字段,是最直接的方式。

3.5 多态处理:@JsonTypeInfo 与 @JsonSubTypes

多态是指接口或父类引用指向不同子类对象。JSON 序列化时,Jackson 默认只序列化引用类型的声明字段,丢失子类特有属性;反序列化时更无法知道具体类。

解决方式是使用@JsonTypeInfo在 JSON 中嵌入类型信息,配合@JsonSubTypes列出子类。我们会在示例 2 中展示如何让子类字段正确输出,并能在反序列化时还原。

4. 完整示例:最小配置与自定义序列化器

示例一:全局日期格式与自定义字段序列化

目标:展示如何通过全局配置和@JsonFormat把日期输出成yyyy-MM-dd HH:mm:ss,并演示自定义序列化器的编写与注册。

前置环境:Spring Boot 2.5+,JDK 8+,Maven/Gradle。

步骤

  1. 创建一个 Spring Boot 项目,加入spring-boot-starter-web依赖。
  2. application.yml中配置全局日期格式:
spring:jackson:date-format:yyyy-MM-dd HH:mm:sstime-zone:Asia/Shanghaiserialization:write-dates-as-timestamps:false
  1. 写一个订单类,在其中使用@JsonFormat指定特定格式:
importcom.fasterxml.jackson.annotation.JsonFormat;importjava.math.BigDecimal;importjava.time.LocalDateTime;publicclassOrder{privateStringorderId;@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")privateLocalDateTimecreatedAt;// 注意:不要用 this.amount 的 getter,容易造成冲突privateBigDecimalamount;// 构造函数、getter/setter 略。}
  1. 写一个自定义序列化器,用于脱敏手机号:PhoneSerializer继承JsonSerializer<String>,重写serialize()方法,把中间四位用*替换。
importcom.fasterxml.jackson.core.JsonGenerator;importcom.fasterxml.jackson.databind.JsonSerializer;importcom.fasterxml.jackson.databind.SerializerProvider;importjava.io.IOException;publicclassPhoneSerializerextendsJsonSerializer<String>{@Overridepublicvoidserialize(Stringphone,JsonGeneratorgen,SerializerProviderserializers)throwsIOException{if(phone!=null&&phone.length()==11){gen.writeString(phone.substring(0,3)+"****"+phone.substring(7));}else{gen.writeString(phone);}}}
  1. 在用户类中使用该序列化器:
importcom.fasterxml.jackson.databind.annotation.JsonSerialize;publicclassUser{privateStringname;@JsonSerialize(using=PhoneSerializer.class)privateStringphone;// getter/setter 略。}
  1. 编写测试 Controller:
importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importjava.time.LocalDateTime;@RestControllerpublicclassTestController{@GetMapping("/test")publicUsertest(){Useruser=newUser();user.setName("张三");user.setPhone("13812345678");returnuser;}@GetMapping("/order")publicOrderorder(){Orderorder=newOrder();order.setOrderId("20230805001");order.setCreatedAt(LocalDateTime.now());order.setAmount(newBigDecimal("199.90"));returnorder;}}
  1. 启动应用,访问/test得到:{"name":"张三","phone":"138****5678"};访问/order得到:{"orderId":"20230805001","createdAt":"2023-08-05 12:34:56","amount":199.90}

关键步骤与预期输出:全局配置所有DateLocalDateTime都按指定格式输出;@JsonFormat覆盖全局配置;手机号被脱敏。

适用场景:定制敏感信息脱敏、时间格式调整、特殊字段逻辑。

容易改错的地方@JsonFormat的 pattern 对LocalDateTime有效,但对Date需要yyyy-MM-dd HH:mm:ss;如果不注册 JavaTimeModule,LocalDateTime会报异常;脱敏时注意 null 判断。

5. 完整示例:多态类型信息处理

示例二:让 JSON 携带类型信息,序列化与反序列化还原子类

目标:演示@JsonTypeInfo@JsonSubTypes如何使接口/抽象类序列化时包含类型字段,并在反序列化时解析为正确子类。

前置环境:同示例一。

步骤

  1. 定义消息接口Message,标注类型信息:
importcom.fasterxml.jackson.annotation.JsonSubTypes;importcom.fasterxml.jackson.annotation.JsonTypeInfo;@JsonTypeInfo(use=JsonTypeInfo.Id.NAME,include=JsonTypeInfo.As.PROPERTY,property="type")@JsonSubTypes({@JsonSubTypes.Type(value=TextMessage.class,name="text"),@JsonSubTypes.Type(value=ImageMessage.class,name="image")})publicinterfaceMessage{}
  1. 定义两个子类:
publicclassTextMessageimplementsMessage{privateStringcontent;// getter/setter 略。}publicclassImageMessageimplementsMessage{privateStringurl;privateintwidth;// getter/setter 略。}
  1. 写一个 Controller 返回消息列表:
importjava.util.Arrays;importjava.util.List;@RestControllerpublicclassMessageController{@GetMapping("/messages")publicList<Message>messages(){TextMessagetext=newTextMessage();text.setContent("你好");ImageMessageimage=newImageMessage();image.setUrl("http://example.com/a.jpg");image.setWidth(100);returnArrays.asList(text,image);}}
  1. 启动访问,得到 JSON:
[{"type":"text","content":"你好"},{"type":"image","url":"http://example.com/a.jpg","width":100}]

可以看到每个对象都带上了type字段,并且ImageMessagewidth字段被正确输出。

反序列化验证:写一个测试方法,用ObjectMapper解析上面的 JSON,断言类型:

importcom.fasterxml.jackson.databind.ObjectMapper;publicclassDeserializeDemo{publicstaticvoidmain(String[]args)throwsException{Stringjson="[{\"type\":\"text\",\"content\":\"你好\"},{\"type\":\"image\",\"url\":\"http://example.com/a.jpg\",\"width\":100}]";ObjectMappermapper=newObjectMapper();List<Message>messages=mapper.readValue(json,newTypeReference<List<Message>>(){});System.out.println(messages.get(0).getClass());// class TextMessageSystem.out.println(messages.get(1).getClass());// class ImageMessage}}

关键步骤与结果:序列化时添加type属性,反序列化时根据type值构造对应子类。

适用场景:消息队列、异构事件、插件化架构等需要多态 JSON 传输的场景。

容易改错的地方@JsonSubTypesname必须与前端/消费者约定一致;如果子类不在列表,会抛异常;要让 this 类型信息同时被序列化,记得在接口上声明;反序列化时泛型要用TypeReference而不是Message[].class(否则类型信息可能丢失)。

6. 完整示例:性能优化实战

示例三:通过属性过滤与静态类型化减少开销

目标:演示如何使用@JsonIgnorePropertiesObjectMapperwriterWithViewfilter减少序列化字段,从而在大对象列表中显著提升性能。

前置环境:一个包含几十个字段的大对象类HeavyObject,内存足够。

步骤

  1. 定义一个大对象:
publicclassHeavyObject{privateStringfield01;privateStringfield02;// 省略 30 个字段privateStringfield30;// getter/setter 略。}
  1. 使用@JsonIgnoreProperties静态忽略某些字段(例如内部大字符串):
@JsonIgnoreProperties({"field20","field21"})publicclassHeavyObject{// 同上}
  1. 动态过滤:使用 Jackson 的FilterProvider。在 Controller 中创建一个ObjectMapper(或注入默认的),设置过滤器:
@RestControllerpublicclassPerformanceController{@GetMapping("/heavy")publicObjectheavy(){// 模拟 1000 个大对象List<HeavyObject>list=newArrayList<>();for(inti=0;i<1000;i++){HeavyObjectobj=newHeavyObject();obj.setField01("value"+i);// 填充 30 个字段list.add(obj);}// 使用 ObjectMapper 的 filter 特性(需要 SimpleBeanPropertyFilter)SimpleBeanPropertyFilterfilter=SimpleBeanPropertyFilter.serializeAllExcept("field20","field21");FilterProviderfilters=newSimpleFilterProvider().addFilter("myFilter",filter);// 注意:HeavyObject 需要 @JsonFilter("myFilter") 才能启用过滤ObjectWriterwriter=objectMapper.writer(filters);returnwriter.writeValueAsString(list);}}

但上面的代码要求HeavyObject标注@JsonFilter。为了简单,我们改用@JsonView

publicclassViews{publicinterfaceSummary{};publicinterfaceFullextendsSummary{};}@JsonView(Views.Summary.class)publicclassHeavyObject{@JsonView(Views.Summary.class)privateStringfield01;@JsonView(Views.Full.class)privateStringfield20;// 默认不输出}

然后通过writerWithView(Views.Summary.class)只输出部分字段:

// 控制器中ObjectMapperobjectMapper=newObjectMapper();Stringjson=objectMapper.writerWithView(Views.Summary.class).writeValueAsString(list);

注意@JsonView在类上标注会影响所有属性,只输出标记了该视图的属性。这是更干净的动态过滤。

运行并从控制台观察时间:循环执行 100 次,对比完整序列化和视图序列化的耗时,后者通常快 30%-50%(字段减少)。另外,可开启 Jackson 的MapperFeature.INFER_PROPERTY_MUTATORS等,但提升有限。

关键步骤与结果:减少序列化字段数量是提高性能最直接的手段;使用视图或过滤器动态控制输出字段,同时保持代码清晰。

适用场景:列表接口需要轻量返回,详情接口需要完整返回时;或者大量实时日志序列化。

容易改错的地方@JsonIgnoreProperties是静态的,无法按请求条件变化;@JsonView需要在每个字段上标注,容易漏;过滤器 ID 必须匹配;性能测试要预热 JIT,否则不准;不要滥用过滤器,增加了复杂度。

性能对比表(参考值)

方案配置/代码输出字段数量耗时(相对)适用场景
完整序列化无额外注解30100ms详情页
@JsonIgnoreProperties类上静态忽略2895ms固定忽略字段
@JsonView按接口动态选择1060ms同一对象多种视图

注意:数据为示例,实际依赖字段数量和类型。核心思路是减少反射调用和写出字符。

7. 常见误区与生产实践建议

常见误区

误区真相避坑建议
每次请求都 new ObjectMapper非常昂贵,缓存丢失,建议复用单例注入 Spring 管理的 ObjectMapper
把实体类直接返回给前端可能暴露敏感字段(密码、内部字段)使用 DTO 或 @JsonIgnore
以为设置spring.jackson.date-format对 LocalDateTime 有效只对 java.util.Date 生效,对 Java 8 时间类型无效需配置serialization.write-dates-as-timestamps: false并使用@JsonFormat
@JsonFormat与全局配置冲突时不知所措注解优先级高于全局了解优先级,注解用于特定字段
接口返回对象中包含循环引用Jackson 会无限递归导致栈溢出使用@JsonIgnoreProperties("user")@JsonManagedReference

生产实践建议

  1. 复用 ObjectMapper:Spring Boot 已提供单例,直接注入使用,除非有特殊要求。
  2. 定义 DTO:不直接暴露实体,防止字段泄漏和循环引用。
  3. 全局配置日期格式:统一 API 风格,建议输出 ISOyyyy-MM-dd'T'HH:mm:ss,前端易解析。
  4. 使用视图或过滤器控制大数据量字段:避免一次性返回大对象,尤其列表。
  5. 开启缩进输出:生产环境关闭(会增大体积),调试时开启。
  6. 监控序列化瓶颈:使用 JMC 或 YourKit 分析writeValue热点。

8. 排障清单

当出现 JSON 相关问题时,按以下清单逐项检查:

  1. 输出时间戳而非字符串?→ 检查是否配置write-dates-as-timestamps: false,并且有 JavaTimeModule。
  2. 反序列化时类型错误?→ 检查是否缺少@JsonTypeInfo或子类注册。
  3. 字段缺失?→ 是不是 getter 返回 null 且配置了NON_NULL,或者被@JsonIgnore了。
  4. 循环引用爆栈?→ 检查双向关联,使用忽略注解。
  5. 性能下降?→ 检查是否每次 new ObjectMapper;是否返回了超大对象。
  6. 拿到空对象{}?→ 可能是FAIL_ON_EMPTY_BEANS和没有 getter。
  7. 自定义序列化器不生效?→ 检查注解位置是否正确(字段/类?),以及是否与全局配置冲突。
  8. 配置不生效?→ 检查是否自定义了 ObjectMapper 但没有调用super或应用旧配置。

9. 总结:回到问题,建立决策模型

回顾开头的两个故障:日期格式混乱和性能问题,根源分别是默认配置没改和序列化范围过大。通过本文的知识,你应该能够搭建如下思维模型:

  • 先看“谁在翻译”:是不是同一个 ObjectMapper?
  • 再看“翻译规则”:全局配置、注解、自定义序列化器分别管什么?
  • 最后看“翻译给谁用”:是给内部服务(可压缩、用时间戳)还是给外部前端(可读性好)?

选型时可以遵循条件判断:

  • 当需要统一日期格式时,优先在application.yml配置write-dates-as-timestamps: falsedate-format;若对个别字段特殊格式,用@JsonFormat
  • 当存在多态引用时,在基类/接口上加@JsonTypeInfo@JsonSubTypes
  • 当列表接口需要精简字段时,使用@JsonView或自定义序列化器;避免静态@JsonIgnoreProperties影响所有场景。
  • 当遇到循环引用时,用@JsonIgnoreProperties或 DTO 打破。

最终,你可以在自己的项目中画一张 JSON 序列化链路图,标注配置点和优化点,遇到问题能够快速定位。

10. 参考资料

  • Jackson 官方文档:https://github.com/FasterXML/jackson-docs
  • Jackson 注解参考:https://github.com/FasterXML/jackson-annotations
  • Spring Framework 官方文档 Web MVC:https://docs.spring.io/spring-framework/reference/web/webmvc.html
  • Spring Boot 官方文档 JSON:https://docs.spring.io/spring-boot/how-to/spring-mvc.html#howto.spring-mvc.jackson-objectmapper
  • Java SE 8 DateTime JavaDoc:https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 21:45:44

智能合同管理:从文档处理到风险防控的数字化转型

## 1. 合同管理现状&#xff1a;我们正在用石器时代工具处理数字时代的契约合同管理这个看似基础的企业职能&#xff0c;正经历着前所未有的撕裂感。2023年某第三方机构调研显示&#xff0c;超过67%的企业仍在使用"邮件Excel纸质归档"的原始工作流&#xff0c;而同时…

作者头像 李华
网站建设 2026/9/14 21:44:50

Python脚本打包EXE进阶:PyInstaller优化与实战

1. Python脚本打包成EXE的进阶指南去年我分享过一篇基础版Python打包教程&#xff0c;没想到后台收到两百多条实操问题咨询。这次我们直接上硬货&#xff0c;结合大家反馈的高频痛点&#xff0c;深度解析PyInstaller的进阶用法。先看一个真实案例&#xff1a;上周我用PyInstall…

作者头像 李华
网站建设 2026/9/14 21:44:22

嵌入式C语言二维数组实战:内存布局、传参与核心应用场景

1. 从菜鸟到入门&#xff1a;为什么二维数组是嵌入式开发绕不开的坎接触嵌入式开发半年多的朋友&#xff0c;十有八九会在二维数组上卡一下。说它难吧&#xff0c;无非就是“数组的数组”&#xff0c;教科书上三句话就能讲完&#xff1b;说它简单吧&#xff0c;等到你真正要在S…

作者头像 李华
网站建设 2026/9/14 21:43:40

Docker - 快速通关 (3h)

安装命令存储网络 4.1. Redis主从同步集群 4.2. 启动MySQLDocker Compose 5.1. 命令式安装 5.2. compose.yaml 5.3. 特性Dockerfile附录 - 一键安装超多中间件 7.1. yaml 7.2. 启动 7.3. 访问 1. 安装 WindTerm下载&#xff1a; https://github.com/kingToolbox/WindTerm/relea…

作者头像 李华
网站建设 2026/9/14 21:41:36

网络安全不可小觑,这些网络安全基础知识要牢记!

网络安全基础学习笔记 目录 一、引论 二、风险分析 三、网络信息安全服务 四、安全体系结构 拓展资料 网络安全基础知识问答 小结 一、引论 提到网络安全&#xff0c;一般人们将它看作是信息安全的一个分支&#xff0c;信息安全是更加广义的一个概念&#xff1a;防止对知识…

作者头像 李华
网站建设 2026/9/14 21:41:25

工业边缘异常检测:Transformer模型落地实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华