ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot 中 Jackson 实战指南:从 ObjectMapper 配置到 JSON 解析避坑

Spring Boot 中 Jackson 实战指南:从 ObjectMapper 配置到 JSON 解析避坑 Spring Boot 接口写多了就会发现很多让你头皮发麻的报错最后都指向同一个地方——JSON 解析。而 Spring Boot 默认用的这个 JSON 解析库就是 Jackson。它在项目里的存在感很低平时接口正常时你几乎感受不到它但一旦遇到 LocalDateTime 序列化成了数组、前端传了下划线字段后端接收不到、反序列化时莫名多出 LinkedHashMap 这类问题你才会意识到这个默认解析库值得好好摸透。这篇文章就把 Spring Boot 里的 Jackson 从底层机制到实战配置、从经典坑位到性能优化完整梳理一遍内容面向正在写 Java 接口的开发者尤其是被 JSON 格式化问题折腾过、想系统搞清楚 Jackson 用法的朋友。下面讲到的每个点都是我踩过坑之后验证过的东西不是照着文档念一遍。1. 为什么 Spring Boot 默认选 Jackson而不是 Gson 或 Fastjson1.1 Jackson 在 Spring Boot 项目里的三个角色很多开发者对 Jackson 的第一印象是“一个能把对象转成 JSON 的库”但在 Spring Boot 里它承担的角色比想象中要广。首先是 HTTP 请求体的反序列化前端用 POST 提交一段 JSONRequestBody 注解配合 Jackson 就能把它变成 Java 对象其次是 HTTP 响应体的序列化Controller 方法返回一个对象ResponseBody 注解再配合 Jackson 把它变成 JSON 字符串回给前端还有一个很容易被忽略的用途是 RestTemplate 或者 WebClient 在做远程调用时也依赖 Jackson 完成消息转换。这三件事背后的实现机制核心都是 Spring MVC 里的 HttpMessageConverter具体到 JSON 场景就是 MappingJackson2HttpMessageConverter。这个类内部持有一个 ObjectMapper 实例Spring Boot 通过自动配置把它准备好你只要在 pom 里引入 spring-boot-starter-webJackson 相关依赖就会被带进来根本不需要手动加。也正因为这种默认集成很多项目从一开始就没仔细看过 Jackson 的配置直到线上接口出现问题才追到这里。1.2 Jackson 比 Gson、Fastjson 强在哪儿我经常被人问到一个问题既然 Java 生态里有 Gson、有 FastjsonSpring Boot 为什么偏偏选了 Jackson这个问题其实得从好几个维度看。第一是标准性。Jackson 几乎已经成了 Java 生态的“默认 JSON 实现”很多重量级框架和中间件都拿它做内置 JSON 支持Spring、Kafka、Elasticsearch 客户端、Cassandra 驱动清一色默认 Jackson。这意味着你学会 Jackson不只在 Spring Boot 里用得上接触其它中间件时几乎不用额外学习成本。第二是扩展能力。Jackson 有一个非常清晰的 Module 机制想扩展某种类型的序列化行为只需要写一个模块注册进去。典型的例子就是 jackson-datatype-jsr310专门解决 Java 8 时间类型 LocalDateTime、LocalDate、Instant 的序列化问题。如果项目用了 Kotlin还有 jackson-module-kotlin对象序列化、反序列化都能跟着语言特性适配。反观 Gson也能用但在 Spring Boot 生态里总有一种“外来务工”的感觉能干活但融入程度没那么高。第三是安全性。这个点比较敏感但值得说透。Fastjson 虽然性能好、API 也方便但历史上爆出过多起反序列化漏洞很多安全团队在项目里明令禁用。Jackson 也并非绝对安全反序列化攻击的防御同样需要关注但总体上它的漏洞应对流程更成熟主仓库的修复速度也比较稳定。对企业项目来说维护成本和安全口碑是选型时很重要的因素。第四是性能。关于 JSON 库的性能争论一直没停过不同基准测试、不同 JDK 版本、不同数据模型下结果会有波动。但在多数业务场景里Jackson 的性能是够用的而且在稳定性上表现很好不会因为你多加了一个注解或者字段就出现明显的性能拐点。还有第 5 章会提到的 ObjectMapper 复用机制这是影响性能的关键因素Jackson 的线程安全设计让它可以放心地全局复用这一点比一些早期版本的第三方库要踏实。选型建议是如果你的项目本来就在 Spring Boot 生态里那就老老实实用 Jackson不要为了“少写几行代码”引入别的库。Gson 适合独立的工具类项目Fastjson 适合对编码速度要求极高且团队能管控好安全风险的场景但多数情况下默认集成的东西反而是长期维护成本最低的。2. ObjectMapper 工作流程序列化和反序列化底层发生了什么2.1 三大核心模块各管什么要理解 Jackson得先知道它拆成了哪几个 jar 包。很多初学者只知道一个 ObjectMapper实际上 Jackson 是由 jackson-core、jackson-annotations、jackson-databind 三个核心模块组成的。jackson-core 是最底层的流式 API它提供 JsonParser 和 JsonGenerator。JsonParser 负责把 JSON 字符串逐字拆成一个个 token从左到右依次读到 JSON 对象的边界符号、字段名、字符串值、数字、布尔值JsonGenerator 则反过来把一个个 token 拼接成合法的 JSON 文本。你平时不会直接操作这两个类但 ObjectMapper 底层全靠它们干活。jackson-annotations 是注解包像 JsonProperty、JsonIgnore、JsonFormat 都在这里面。它的作用是在编译期和运行期给 ObjectMapper 提供“字段怎么映射、哪些字段忽略、格式怎么约定”的线索。jackson-databind 是对象映射层它在上面的基础上提供了 ObjectMapper、ObjectReader、ObjectWriter。ObjectReader 和 ObjectWriter 是偏底层的只读、只写视图日常开发用的最多的还是 ObjectMapper它是整个 Jackson 的门面。依赖关系上一个链条是databind 依赖 core 和 annotations所以创建 maven 项目时只需要引入 jackson-databind 这一个依赖另外两个会自动被传递依赖带进来。如果是在 Spring Boot 项目里spring-boot-starter-web 已经把相关的 jackson-databind 也包含好了。2.2 序列化的底层流程拆解我用一个最典型的场景来拆解序列化过程Controller 里返回了一个用户对象 userService.findUser(1L)返回值类型是 UserDTOSpring Boot 最终要把这个对象变成 JSON 字符串回给前端。这个过程大致走三步。第一步ObjectMapper 的 writeValue 方法拿到目标对象分析它的运行时类型。这里注意Jackson 是基于运行时类型来序列化的所以哪怕你声明的返回类型是父类实际传进去一个子类实例序列化出来的内容也会带上子类特有的字段。第二步遍历目标对象内部的属性。Jackson 会通过反射查找对象的 getter 方法、字段、setter 方法然后按照配置决定哪些属性要输出、哪些要忽略。优先级大概是这样用 JsonProperty 显式声明的字段无论在 getter 上还是字段上都会优先被识别没有注解时就看 getter 方法如果既没有 getter 也没有 JsonProperty就看公共字段。这也是为什么有些 Java Bean 只写了字段没写 getter序列化时字段照样能出来因为它退而求其次用了字段反射。第三步决定每个属性在 JSON 里该叫什么名字、值如何表示。比如 JsonProperty(user_name) 会让字段 userName 输出为 user_nameJsonFormat 处理日期格式JsonInclude(NON_NULL) 控制 NULL 值不输出。这些规则收集完之后ObjectMapper 会把这些规则传给底层的 JsonGenerator一层层写出字段名、冒号、值、逗号最终拼接成完整的 JSON 文本。类比一下序列化就相当于把你的 Java 对象信息登记到一张表格里每个栏目对应一个 JSON 字段名。注解决定了栏目叫什么、哪些保密栏目不打出来JsonIgnore、日期栏目用什么格式写而表格最终长什么样则完全受 ObjectMapper 的全局配置影响。2.3 反序列化的底层流程拆解反序列化是相反的操作但它的流程比序列化要复杂一些因为需要处理类型转换、边界情况、空值策略。我拆解一下 RequestBody UserDTO user 这个场景。第一步Spring MVC 拿到 HTTP 请求体里的 JSON 字符串交给 MappingJackson2HttpMessageConverter它内部调用 ObjectMapper 的 readValue 方法。第二步JsonParser 把 JSON 字符串拆成 token 流。比如 {user_name:张三,create_time:2024-05-12 10:30:00} 会被解析成 START_OBJECT、FIELD_NAME、VALUE_STRING、END_OBJECT 等 token。Jackson 根据 token 类型逐步读取内容。第三步进入对象绑定环节这一步最核心。Jackson 拿到目标类型 UserDTO 后会看这个类有没有默认构造器或默认反序列化入口。具体来说有三种策略优先查找无参构造器如果没有无参构造器但有一个带参构造器它需要配合 JsonCreator 或 ConstructorProperties 才能识别参数映射还有一种是通过静态工厂方法结合 JsonCreator 来创建实例。实际项目里绝大多数 DTO 都有无参构造器所以走的是反射创建对象、再给字段赋值这条路。第四步赋值阶段。Jackson 会把每个 FIELD_NAME token 对应的字段名跟目标类的属性映射规则做匹配找到映射后把 VALUE_STRING 等值经过类型转换赋给字段。比如字符串 123 赋给 Long 类型的字段时Jackson 会自动完成字符串到 Long 的转换。如果字段类型是 LocalDateTime还需要时间格式化器参与转换如果字段类型是 List Jackson 会继续递归创建子对象。需要特别注意的是Jackson 并不是只认 getter 和 setter。反序列化时如果目标类只有字段、没有 setter但字段是 public 的Jackson 也能直接赋进去。如果字段是 private 且没有 setter但存在无参构造器Jackson 也可以通过反射直接操作字段这在一些老项目中很常见。但推荐的做法还是规范编写 setter或者在字段上加注解让 Jackson 的匹配逻辑更清晰减少底层反射的魔法。理解了这套流程就能解释很多奇怪现象了。比如反序列化一个 JSON 字符串到一个没有无参构造器的类你会遇到 InvalidDefinitionException再比如字段名对不上你会得到 UnrecognizedPropertyException还有泛型擦除导致反序列化结果变成 LinkedHashMap根本原因就在第四步的目标类型解析环节。这些问题在下文会一个个展开。3. Spring Boot 中的 Jackson 配置与注解实战3.1 高频注解逐个拆解Jackson 注解很多但日常项目里翻来覆去就那七八个。我把它们按使用频率排个序每个都配上典型场景。JsonProperty 是最常用的作用是在序列化和反序列化两个方向同时指定 JSON 字段名。典型场景是 Java 侧用驼峰命名 userName数据库字段和前端协议里却是 user_name这时候在字段上加 JsonProperty(user_name)两个方向就都通了。public class UserDTO { JsonProperty(user_id) private Long userId; JsonProperty(user_name) private String userName; }JsonIgnore 也是高频注解通常用来隐藏敏感字段或内部字段。比如密码、数据库主键自增 ID、内部缓存标记这些字段如果直接输出到接口响应里要么泄露信息要么污染协议。在字段上添加 JsonIgnore序列化时不输出反序列化时如果 JSON 里传了这个字段也会被忽略。JsonFormat 主要解决时间格式问题。项目里接口统一用 yyyy-MM-dd HH:mm:ss 而不是 ISO 字符串或者数组直接在时间字段上写JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;需要注意在 Spring Boot 中这个注解对 java.util.Date 和 LocalDateTime 都生效但 LocalDateTime 场景下还需要配合 JavaTimeModule。另外 timezone 属性建议写清楚避免服务器时区与业务时区不一致导致时间偏差。JsonInclude 控制字段什么时候参与序列化。最常见的用法是 JsonInclude(JsonInclude.Include.NON_NULL)值为 NULL 的字段直接不输出。它可以用在类级别也可以用在字段级别。需要注意的是NON_NULL 不等于 NON_EMPTY空字符串和空集合在 NON_NULL 下仍然会输出如果希望对空字符串也隐藏用 NON_EMPTY。JsonAlias 是个容易被忽略但很实用的注解它只在反序列化时生效给字段添加一个或多个“别名”。典型场景是接口兼容比如原来前端传的字段叫 mobile后来改成 phone在新旧版本共存期间可以在字段上声明JsonAlias({mobile, telephone}) private String phone;这样前端不管传 mobile 还是 phone后端都能正确绑定。但注意序列化时输出的字段名始终是 phone不会输出别名。JsonSerialize 和 JsonDeserialize 用于自定义序列化器和反序列化器属于进阶用法。比如金额字段要脱敏、手机号中间补星号、字符串要 trim 再入库这些可以用自定义序列化器处理。实际开发中我建议除非逻辑足够简单否则优先用全局配置或封装工具方法处理自定义序列化器写多了会分散注意力。还有一个 JsonCreator专门用于没有无参构造器、又需要自定义创建逻辑的类。比如带参构造器配合 JsonCreator(mode JsonCreator.Mode.PROPERTIES) JsonProperty 参数告诉 Jackson 用哪些参数创建对象。3.2 Spring Boot 全局配置从 application.yml 到配置类Spring Boot 对 Jackson 的配置有两条出路简单的属性写在 application.yml 里复杂规则写在配置类里。application.yml 这种方式适合设置基础项。比如spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 default-property-inclusion: non_null deserialization: fail-on-unknown-properties: false serialization: fail-on-empty-beans: false这里要注意一个很经典的误区spring.jackson.date-format 对 java.util.Date 和 java.util.Calendar 生效但对 LocalDateTime、LocalDate 这类 Java 8 时间类型是不生效的。原因在于 date-format 走的是 SimpleDateFormat 那套机制而 LocalDateTime 依赖的是 jsr310 模块。很多人在 yml 里配了日期格式发现接口返回的 LocalDateTime 还是数组就是这个机制没对上。更推荐的方案是写一个配置类用 Jackson2ObjectMapperBuilderCustomizer 定制全局 ObjectMapperConfiguration public class JacksonConfiguration { Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { JavaTimeModule javaTimeModule new JavaTimeModule(); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer( DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer( DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); return builder - { builder.serializationInclusion(JsonInclude.Include.NON_NULL); builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); builder.modules(javaTimeModule); builder.featuresToDisable( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, SerializationFeature.FAIL_ON_EMPTY_BEANS ); }; } }这段配置做的事有全局忽略 NULL 字段统一时间格式注册 JavaTimeModule 并给 LocalDateTime 指定明确的格式关闭未知字段报错禁止时间戳输出允许空对象序列化为 {}。这里有一个实操经验Spring Boot 的自动配置允许多个 Jackson2ObjectMapperBuilderCustomizer 共存它们按照 Order 注解或声明的先后顺序生效。所以团队可以拆出多个 Customizer一个管时间格式一个管命名策略一个管特性开关互不干扰。这比把几百行配置堆在一个类里要好维护得多。还有一个非常容易踩的坑不要在代码里手动 new ObjectMapper()然后以为改的是 Spring Boot 正在用的那个实例。Spring Boot 里的 ObjectMapper 是容器管理的单例HttpMessageConverter 持有的是容器创建的实例。如果你在业务代码里 new 一个做了不同配置就会出现“接口输出的格式和你配置文件里的不一样”这种灵异现象。正确做法是把 ObjectMapper 注入进来RestController public class UserController { private final ObjectMapper objectMapper; public UserController(ObjectMapper objectMapper) { this.objectMapper objectMapper; } }3.3 实战示例用户 DTO 的完整序列化与反序列化直接上一个我在项目里用过的 UserDTO把上面的注解串起来public class UserDTO { JsonProperty(user_id) private Long userId; JsonProperty(user_name) JsonAlias(name) private String userName; JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime; JsonIgnore private String password; private ListOrderDTO orders; // 省略 getter / setter }假设有个用户 id1001、userName张三、createTime2024-05-12 10:30:00、passwordabc123、orders 包含两个订单那么序列化输出大概是{ user_id: 1001, user_name: 张三, createTime: 2024-05-12 10:30:00, orders: [ { orderNo: NO001, amount: 99.9 } ] }注意几点password 因为 JsonIgnore 不会出现在输出里user_name 通过 JsonProperty 指定但序列化不会输出别名 nameorders 会递归序列化成数组。反序列化时如果前端传来{ user_id: 1001, name: 李四, createTime: 2024-05-12 11:30:00, password: hack, orders: [] }password 会被忽略name 通过 JsonAlias 映射到 userNamecreateTime 按 yyyy-MM-dd HH:mm:ss 格式解析成 LocalDateTime。这组行为组合起来基本覆盖了一个常规接口的全部 JSON 处理需求。4. 实操踩坑我遇到的几个 Jackson 经典问题4.1 LocalDateTime 序列化成数组问题如果你是自己创建 ObjectMapper 而没有配置 JavaTimeModule时间字段序列化出来会是一个数组比如 createTime 变成 [2024, 5, 12, 10, 30, 0]这是在把 LocalDateTime 当作一个包含年月日时分秒的结构化对象输出。我的项目里发生过一次线上问题前端拿到数组后直接渲染时间显示成了 2024-5-12后面时分秒全部丢失排查一圈才发现是团队里有人单独写了个工具类内部 new ObjectMapper() 没有注册 JavaTimeModule导致工具类序列化和接口序列化行为不一致。从此以后我要求所有 ObjectMapper 统一从 Spring 容器注入禁止手动 new。在自定义配置里最稳妥的做法就是注册 JavaTimeModule 并禁用 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS再通过格式化器统一 LocalDateTime 的 pattern。这样保证 Spring Boot 接口内部和业务代码里的序列化行为完全一致。另外JSON 里出现 2024-05-12T10:30:00 这种带 T 的 ISO 字符串也是 Jackson 的常见输出之一如果前端能接受那不需要额外配置但大多数国内项目约定俗成用 yyyy-MM-dd HH:mm:ss建议在配置层面统一。4.2 循环引用导致序列化栈溢出双向关联是 ORM 实体类里很容易出现的设计。比如部门实体里有 List 员工实体里又有一个所属部门字段查询部门时带出员工员工又带出部门Jackson 序列化时就会递归下去最后报 JsonMappingException 或者 StackOverflowError。我在以前的项目里用过三种处理方式。第一种是在字段上加 JsonIgnoreProperties在关联的字段上声明忽略反向属性这种方法见效快但代码写起来啰嗦而且本质上还是在用注解硬编码结构。第二种是 JsonManagedReference 和 JsonBackReference前者在父类维护子类的字段后者在子类维护父类字段Jackson 序列化父类时正常输出子类列表序列化子类时不再返回父类引用这个方案在父子关系明确的模型里比较好用。第三种是 JsonIdentityInfo它会把对象改成按 ID 引用输出比如第一次输出完整对象后续遇到同样对象只输出 ID避免重复嵌套同时保留循环引用的信息但代价是前端拿到的数据需要自己拼装协议复杂度明显上升。我的建议是优先在设计层面避免双向引用。把 DO 和 DTO 分离实体类不加 Json* 注解专门创建视图对象来控制序列化结构。如果实在需要在实体上处理用 JsonIgnoreProperties 或 JsonIdentityInfo 选一个别混用混用后排查起来特别痛苦。4.3 泛型反序列化时类型信息丢失这个问题在接口调用场景里太常见了。比如对方服务返回一个 JSON 数组你直接用 objectMapper.readValue(json, List.class)编译不报错运行时也不报错但拿到 List 里的元素类型是 LinkedHashMap不是期望的 UserDTO。等你循环里强转或者调用 getXxx() 方法时ClassCastException 就来了。根因是 Java 泛型擦除。Jackson 在运行期只能拿到 List.class不知道元素具体类型只能退化成 LinkedHashMap然后原样保留 KV。要拿到 List 必须显式告诉 Jackson 完整泛型链ListUserDTO list objectMapper.readValue(json, new TypeReferenceListUserDTO() {});TypeReference 是 Jackson 用来捕获泛型类型的工具类通过匿名内部类的 super 类型参数把 List 完整信息传递进去。如果你的项目有统一响应包装类比如 R 这种也需要用 TypeReference 来构造RListUserDTO result objectMapper.readValue(json, new TypeReferenceRListUserDTO() {});还有一个常见做法是把反序列化逻辑封装成泛型工具方法避免每个调用点都写 TypeReference 匿名类。我自己项目里就封装了一个 JsonUtils.fromJson(String json, TypeReference type)内部直接调用容器注入的 ObjectMapper统一处理异常。4.4 字段命名策略驼峰和下划线之争Java 端习惯小驼峰数据库和前端协议常常用下划线这个矛盾从后端接口设计第一天就存在。最直接的解决方案有两种。一种是在字段上逐个加 JsonProperty 指定 JSON 字段名比如 private String mobileNo 配合 JsonProperty(mobile_no) Completed。这种方式最大的优点是精确可控缺点是字段多的时候很啰嗦而且容易漏。另一种方式是全局配置命名策略在自定义 Customizer 里设置builder.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);这样 Java 字段 mobileNo 会自动序列化为 mobile_no反序列化时前端传 mobile_no 也能映射回 mobileNo不需要在字段上写注解。如果团队内部明确规定接口 JSON 全用下划线这个方案效率最高。需要留意的是命名策略会影响所有字段包括第三方类库传进来的字段。比如某个第三方请求对象字段叫 orderNo策略改成 SNAKE_CASE 后输出自动变成 order_no后端接收前端传的 order_no 也能正常转化但如果你混合使用了手写 JsonProperty 的字段和全局策略两个规则相遇时显式 JsonProperty 优先级更高。所以团队切换命名策略时最好提前扫一遍代码里显式声明的 JsonProperty确认没有冲突。4.5 未知字段导致反序列化失败Spring Boot 默认情况下Jackson 反序列化时如果 JSON 里出现了一个目标类不认识的字段会直接抛 UnrecognizedPropertyException。这个行为在生产环境经常引发事故比如接口兼容性升级时给请求体加了一个新字段但服务端代码还没有发布就会导致整段请求失败。处理方式有两种。全局在配置里关闭 FAIL_ON_UNKNOWN_PROPERTIES 特性或者在需要的类上直接加 JsonIgnoreProperties(ignoreUnknown true)。我个人的倾向是对外提供接口时全局关闭该特性因为这样可以最大程度保证兼容性新增字段不会破坏老版本调用方。如果某个内部模块对字段完整性有严格要求比如从消息队列消费消息时想及时发现协议不匹配那就单独在消息类上开启严格校验做成局部行为而不是全全局一刀切。5. 性能优化与常见问题速查5.1 ObjectMapper 最快使用方式关于 Jackson 性能讲一个很多人不知道但影响最大的点ObjectMapper 是线程安全的配置好之后可以全局复用千万不要每次调用都 new 一个。我在代码评审里见过很多次这样的写法public String toJson(Object obj) { ObjectMapper mapper new ObjectMapper(); return mapper.writeValueAsString(obj); }每次调用都创建一个新对象分配内存、加载配置、反射缓存全部重来一遍高并发场景下会产生巨大的不必要开销。正确姿势是项目里定义一个静态常量或通过 Spring 容器注入Component public class JsonUtils { private static final ObjectMapper MAPPER new ObjectMapper(); static { MAPPER.registerModule(new JavaTimeModule()); MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } public static String toJson(Object obj) throws Exception { return MAPPER.writeValueAsString(obj); } public static T T fromJson(String json, TypeReferenceT type) throws Exception { return MAPPER.readValue(json, type); } }在 Spring Boot 项目里我推荐直接注入容器管理的 ObjectMapper这样你改配置类的效果才能同步到工具方法。如果团队里想用静态工具类也可以从容器中取一次 ObjectMapper 然后赋值给 static 字段启动时初始化运行期复用。另一个优化点是通过 Jackson 的 afterburner 或 blackbird 模块做字节码增强减少反射调用带来的开销。afterburner 在 2.12 之后被标记为 deprecate推荐用 blackbird 替代。真正常规业务中如果序列化对象不是海量级别这个优化收益不明显更适合做网关、日志采集这类高频序列化场景。生产环境还有一个小建议确认没有开启 SerializationFeature.INDENT_OUTPUT这个特性会让输出带上缩进和换行适合调试但会明显增加响应体积。还有 FAIL_ON_EMPTY_BEANS 在生产环境建议关闭否则一个空对象可能触发异常而不是输出 {}。5.2 主流 JSON 库选型对比这里把 Jackson、Gson、Fastjson 放在一张表里对比方便大家做技术选型时快速参考维度JacksonGsonFastjsonSpring Boot 集成度默认集成无需额外依赖需要配置 HttpMessageConverter需要配置 HttpMessageConverter扩展机制Module 机制生态成熟自定制较少有但生态较封闭安全性相对稳定漏洞响应流程成熟良好历史上多次反序列化安全漏洞性能均衡稳定一般高峰期性能较好但波动大泛型支持TypeReference 方案成熟TypeToken 方案成熟内置 TypeReference维护活跃度高中等高但口碑波动选择建议如果是新项目且基于 Spring Boot直接使用 Jackson如果是老项目想迁移到 Spring Boot也可以保留其它库但需要考虑安全审查和兼容性成本。总而言之不要为了换而换。5.3 常见问题速查表把上面所有踩过的坑整理成一张速查表平时遇到问题对着查就行问题现象根因解决方案LocalDateTime 序列化为数组未注册 JavaTimeModule 或未禁用时间戳注册 JavaTimeModule关闭 WRITE_DATES_AS_TIMESTAMPS时间格式不是想要的 patternJava 8 时间类型不受 spring.jackson.date-format 控制自定义 LocalDateTimeSerializer / Deserializer反序列化得到 LinkedHashMap泛型擦除只传了 List.class使用 TypeReference 指定泛型类型循环引用导致栈溢出双向关联无限递归使用 JsonIgnoreProperties、JsonManagedReference 或 JsonIdentityInfo未知字段反序列化报错FAIL_ON_UNKNOWN_PROPERTIES 默认开启全局关闭该特性或类上加 JsonIgnoreProperties(ignoreUnknown true)字段名对不上驼峰命名与下划线协议不一致JsonProperty 或全局 PropertyNamingStrategies.SNAKE_CASENULL 字段也被输出未配置 NON_NULL类级或全局设置 JsonInclude.Include.NON_NULL密码字段被接口返回未忽略敏感字段字段加 JsonIgnore 或使用视图对象隔离手动 new ObjectMapper 但配置不生效容器管理的 ObjectMapper 才是 Spring 正在用的注入容器 ObjectMapper不要自己 new关于第 5.3 这部分值得多说一句的是问题排查一定要从 ObjectMapper 的实例来源开始。先确认当前代码用的是哪个实例再去看实例上的配置最后才考虑注解。很多时候你觉得“Jackson 怎么这么蠢”其实是配置没生效或者多个 ObjectMapper 实例行为不一致导致的。聊聊我实际用下来的感受。Jackson 最让我放心的地方是它几乎不会在复杂场景下给出一个“奇怪的默认行为”却不给退路。时间格式不对可以配格式化器循环引用可以换注解策略泛型丢失可以 TypeReference 显式声明这些方案虽然多多少少需要一点学习成本但每一条都是清晰、可控、可测试的。还是想强调一下团队规范的价值。这个库本身不难难的是项目里每个人对 Jackson 配置的理解不一样。有人依赖 Spring Boot 自动配置有人维护工具类自行定制有人直接在实体类上加了一堆注解最后这些规则交织在一起谁都不敢动一动就炸。所以我在团队里收到新项目的第一件事就是先把全局 ObjectMapper 的统一配置和 JsonUtils 工具类定下来明确一条约定业务代码不得直接 new ObjectMapper注解只在 DTO 里使用实体类不加序列化相关注解。这个库后续还能玩的方向也不少比如自定义序列化器做字段脱敏、集成 protobuf 的序列化桥接、处理超大 JSON 时改用 JsonParser 流式读取、在测试里用 ObjectMapper 快速构造断言数据。但基础打牢那些都是水到渠成的事。
返回列表