ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/14 21:46:19 拓冰建站 浏览量
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 对象比如Order、User。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但你可以通过三种方式覆盖它在application.yml中配置标准属性例如日期格式、时区、缩进输出等。自定义Jackson2ObjectMapperBuilderCustomizer以编程方式微调ObjectMapper适合复杂逻辑。直接定义ObjectMapperbean完全接管风险是丢失 Spring 的默认配置如 JavaTimeModule 注册。理解配置优先级有助于排查“我改了配置怎么没生效”的问题属性文件最低Customizer次之显式 Bean 最高但可能关掉了默认。3.2 默认序列化流程与反射开销当ObjectMapper.writeValue()被调用时它经历以下步骤简化检查目标对象的类型找到对应的JsonSerializer。如果没有缓存则通过反射分析类结构收集可序列化属性通常是getter方法。调用序列化器生成 JSON 字段名和值。对于复杂对象递归处理其字段值直到所有字段都是基本类型、数组或容器。这里的开销主要在于首次反射分析和后续的方法调用。Jackson 默认会缓存已分析类的序列化器但如果你频繁创建新ObjectMapper比如每次请求都 new或者用了动态代理类缓存就会失效导致性能下降。3.3 常见序列化器JavaTimeModule 与 JSR310Java 8 时间类型LocalDate、LocalDateTime默认不被 Jackson 支持需要注册JavaTimeModule。Spring Boot 会自动注册它但默认把时间序列化为数组或时间戳。要输出成 ISO 字符串必须配置write-dates-as-timestamps: false。区别如下配置项默认值输出示例适用场景spring.jackson.serialization.write-dates-as-timestamps: true是Spring Boot 2.x1691234567890传输效率高前端处理麻烦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.5JDK 8Maven/Gradle。步骤创建一个 Spring Boot 项目加入spring-boot-starter-web依赖。在application.yml中配置全局日期格式spring:jackson:date-format:yyyy-MM-dd HH:mm:sstime-zone:Asia/Shanghaiserialization:write-dates-as-timestamps:false写一个订单类在其中使用JsonFormat指定特定格式importcom.fasterxml.jackson.annotation.JsonFormat;importjava.math.BigDecimal;importjava.time.LocalDateTime;publicclassOrder{privateStringorderId;JsonFormat(patternyyyy-MM-dd HH:mm:ss)privateLocalDateTimecreatedAt;// 注意不要用 this.amount 的 getter容易造成冲突privateBigDecimalamount;// 构造函数、getter/setter 略。}写一个自定义序列化器用于脱敏手机号PhoneSerializer继承JsonSerializerString重写serialize()方法把中间四位用*替换。importcom.fasterxml.jackson.core.JsonGenerator;importcom.fasterxml.jackson.databind.JsonSerializer;importcom.fasterxml.jackson.databind.SerializerProvider;importjava.io.IOException;publicclassPhoneSerializerextendsJsonSerializerString{Overridepublicvoidserialize(Stringphone,JsonGeneratorgen,SerializerProviderserializers)throwsIOException{if(phone!nullphone.length()11){gen.writeString(phone.substring(0,3)****phone.substring(7));}else{gen.writeString(phone);}}}在用户类中使用该序列化器importcom.fasterxml.jackson.databind.annotation.JsonSerialize;publicclassUser{privateStringname;JsonSerialize(usingPhoneSerializer.class)privateStringphone;// getter/setter 略。}编写测试 Controllerimportorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importjava.time.LocalDateTime;RestControllerpublicclassTestController{GetMapping(/test)publicUsertest(){UserusernewUser();user.setName(张三);user.setPhone(13812345678);returnuser;}GetMapping(/order)publicOrderorder(){OrderordernewOrder();order.setOrderId(20230805001);order.setCreatedAt(LocalDateTime.now());order.setAmount(newBigDecimal(199.90));returnorder;}}启动应用访问/test得到{name:张三,phone:138****5678}访问/order得到{orderId:20230805001,createdAt:2023-08-05 12:34:56,amount:199.90}。关键步骤与预期输出全局配置所有Date与LocalDateTime都按指定格式输出JsonFormat覆盖全局配置手机号被脱敏。适用场景定制敏感信息脱敏、时间格式调整、特殊字段逻辑。容易改错的地方JsonFormat的 pattern 对LocalDateTime有效但对Date需要yyyy-MM-dd HH:mm:ss如果不注册 JavaTimeModuleLocalDateTime会报异常脱敏时注意 null 判断。5. 完整示例多态类型信息处理示例二让 JSON 携带类型信息序列化与反序列化还原子类目标演示JsonTypeInfo和JsonSubTypes如何使接口/抽象类序列化时包含类型字段并在反序列化时解析为正确子类。前置环境同示例一。步骤定义消息接口Message标注类型信息importcom.fasterxml.jackson.annotation.JsonSubTypes;importcom.fasterxml.jackson.annotation.JsonTypeInfo;JsonTypeInfo(useJsonTypeInfo.Id.NAME,includeJsonTypeInfo.As.PROPERTY,propertytype)JsonSubTypes({JsonSubTypes.Type(valueTextMessage.class,nametext),JsonSubTypes.Type(valueImageMessage.class,nameimage)})publicinterfaceMessage{}定义两个子类publicclassTextMessageimplementsMessage{privateStringcontent;// getter/setter 略。}publicclassImageMessageimplementsMessage{privateStringurl;privateintwidth;// getter/setter 略。}写一个 Controller 返回消息列表importjava.util.Arrays;importjava.util.List;RestControllerpublicclassMessageController{GetMapping(/messages)publicListMessagemessages(){TextMessagetextnewTextMessage();text.setContent(你好);ImageMessageimagenewImageMessage();image.setUrl(http://example.com/a.jpg);image.setWidth(100);returnArrays.asList(text,image);}}启动访问得到 JSON[{type:text,content:你好},{type:image,url:http://example.com/a.jpg,width:100}]可以看到每个对象都带上了type字段并且ImageMessage的width字段被正确输出。反序列化验证写一个测试方法用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}];ObjectMappermappernewObjectMapper();ListMessagemessagesmapper.readValue(json,newTypeReferenceListMessage(){});System.out.println(messages.get(0).getClass());// class TextMessageSystem.out.println(messages.get(1).getClass());// class ImageMessage}}关键步骤与结果序列化时添加type属性反序列化时根据type值构造对应子类。适用场景消息队列、异构事件、插件化架构等需要多态 JSON 传输的场景。容易改错的地方JsonSubTypes的name必须与前端/消费者约定一致如果子类不在列表会抛异常要让 this 类型信息同时被序列化记得在接口上声明反序列化时泛型要用TypeReference而不是Message[].class否则类型信息可能丢失。6. 完整示例性能优化实战示例三通过属性过滤与静态类型化减少开销目标演示如何使用JsonIgnoreProperties和ObjectMapper的writerWithView或filter减少序列化字段从而在大对象列表中显著提升性能。前置环境一个包含几十个字段的大对象类HeavyObject内存足够。步骤定义一个大对象publicclassHeavyObject{privateStringfield01;privateStringfield02;// 省略 30 个字段privateStringfield30;// getter/setter 略。}使用JsonIgnoreProperties静态忽略某些字段例如内部大字符串JsonIgnoreProperties({field20,field21})publicclassHeavyObject{// 同上}动态过滤使用 Jackson 的FilterProvider。在 Controller 中创建一个ObjectMapper或注入默认的设置过滤器RestControllerpublicclassPerformanceController{GetMapping(/heavy)publicObjectheavy(){// 模拟 1000 个大对象ListHeavyObjectlistnewArrayList();for(inti0;i1000;i){HeavyObjectobjnewHeavyObject();obj.setField01(valuei);// 填充 30 个字段list.add(obj);}// 使用 ObjectMapper 的 filter 特性需要 SimpleBeanPropertyFilterSimpleBeanPropertyFilterfilterSimpleBeanPropertyFilter.serializeAllExcept(field20,field21);FilterProviderfiltersnewSimpleFilterProvider().addFilter(myFilter,filter);// 注意HeavyObject 需要 JsonFilter(myFilter) 才能启用过滤ObjectWriterwriterobjectMapper.writer(filters);returnwriter.writeValueAsString(list);}}但上面的代码要求HeavyObject标注JsonFilter。为了简单我们改用JsonViewpublicclassViews{publicinterfaceSummary{};publicinterfaceFullextendsSummary{};}JsonView(Views.Summary.class)publicclassHeavyObject{JsonView(Views.Summary.class)privateStringfield01;JsonView(Views.Full.class)privateStringfield20;// 默认不输出}然后通过writerWithView(Views.Summary.class)只输出部分字段// 控制器中ObjectMapperobjectMappernewObjectMapper();StringjsonobjectMapper.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并使用JsonFormatJsonFormat与全局配置冲突时不知所措注解优先级高于全局了解优先级注解用于特定字段接口返回对象中包含循环引用Jackson 会无限递归导致栈溢出使用JsonIgnoreProperties(user)或JsonManagedReference生产实践建议复用 ObjectMapperSpring Boot 已提供单例直接注入使用除非有特殊要求。定义 DTO不直接暴露实体防止字段泄漏和循环引用。全局配置日期格式统一 API 风格建议输出 ISOyyyy-MM-ddTHH:mm:ss前端易解析。使用视图或过滤器控制大数据量字段避免一次性返回大对象尤其列表。开启缩进输出生产环境关闭会增大体积调试时开启。监控序列化瓶颈使用 JMC 或 YourKit 分析writeValue热点。8. 排障清单当出现 JSON 相关问题时按以下清单逐项检查输出时间戳而非字符串→ 检查是否配置write-dates-as-timestamps: false并且有 JavaTimeModule。反序列化时类型错误→ 检查是否缺少JsonTypeInfo或子类注册。字段缺失→ 是不是 getter 返回 null 且配置了NON_NULL或者被JsonIgnore了。循环引用爆栈→ 检查双向关联使用忽略注解。性能下降→ 检查是否每次 new ObjectMapper是否返回了超大对象。拿到空对象{}→ 可能是FAIL_ON_EMPTY_BEANS和没有 getter。自定义序列化器不生效→ 检查注解位置是否正确字段/类以及是否与全局配置冲突。配置不生效→ 检查是否自定义了 ObjectMapper 但没有调用super或应用旧配置。9. 总结回到问题建立决策模型回顾开头的两个故障日期格式混乱和性能问题根源分别是默认配置没改和序列化范围过大。通过本文的知识你应该能够搭建如下思维模型先看“谁在翻译”是不是同一个 ObjectMapper再看“翻译规则”全局配置、注解、自定义序列化器分别管什么最后看“翻译给谁用”是给内部服务可压缩、用时间戳还是给外部前端可读性好选型时可以遵循条件判断当需要统一日期格式时优先在application.yml配置write-dates-as-timestamps: false和date-format若对个别字段特殊格式用JsonFormat。当存在多态引用时在基类/接口上加JsonTypeInfo和JsonSubTypes。当列表接口需要精简字段时使用JsonView或自定义序列化器避免静态JsonIgnoreProperties影响所有场景。当遇到循环引用时用JsonIgnoreProperties或 DTO 打破。最终你可以在自己的项目中画一张 JSON 序列化链路图标注配置点和优化点遇到问题能够快速定位。10. 参考资料Jackson 官方文档https://github.com/FasterXML/jackson-docsJackson 注解参考https://github.com/FasterXML/jackson-annotationsSpring Framework 官方文档 Web MVChttps://docs.spring.io/spring-framework/reference/web/webmvc.htmlSpring Boot 官方文档 JSONhttps://docs.spring.io/spring-boot/how-to/spring-mvc.html#howto.spring-mvc.jackson-objectmapperJava SE 8 DateTime JavaDochttps://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html