Jackson序列化踩坑实录:本地调试正常,线上JSON报错、字段丢失、类型错乱、数据泄露

Jackson序列化踩坑实录:本地调试正常,线上JSON报错、字段丢失、类型错乱、数据泄露
前言
在Java后端项目中,Jackson几乎是标配,Spring‑MVC默认使用Jackson完成对象与JSON之间的序列化、反序列化工作。大部分开发者日常只需要写业务实体,依靠框架自动完成JSON转换,很少关心Jackson底层行为。

本地单元测试、开发环境接口调用一切正常,一旦上线,就会遇到各种诡异问题:接口返回JSON字段莫名消失、时间格式乱码、BigDecimal精度丢失、循环引用栈溢出、内部敏感字段意外泄露、泛型反序列化类型转换异常。

很多时候并不是框架bug,而是实体注解、对象类型、序列化配置理解不到位。测试环境数据简单,边界场景没有触发,线上真实业务数据来了之后故障集中爆发。下面全部来自线上真实故障复盘,包含错误代码、现象、根因、生产修复方案。
QQ20260810-113713.png
坑1:对象存在循环引用,序列化直接抛出StackOverflowError栈溢出
故障现象
实体存在双向关联,例如订单实体包含用户对象,用户实体内部又持有订单集合。接口输出JSON直接500,堆栈信息为StackOverflowError。本地简单测试对象没有互相赋值,不会复现,线上真实关联数据才会触发。

❌错误代码
java
@Data
public class User {
private Long id;
private String username;
//一对多,用户持有订单列表
private List orderList;
}

@Data
public class Order {
private Long id;
private String orderNo;
//多对一,订单持有用户
private User user;
}

根因:Jackson序列化时会递归读取对象属性。User→Order→User,无限递归,最终栈溢出。很多做数据库一对多映射直接把关联实体放入返回VO,线上直接爆炸。

✅修复方案
方案1:@JsonIgnore忽略其中一端的关联字段,打断循环;
java
@Data
public class Order {
private Long id;
private String orderNo;
@JsonIgnore
private User user;
}

方案2:使用@JsonManagedReference@JsonBackReference处理双向引用;
方案3:业务层不要直接返回数据库实体,组装VO视图对象,VO层剔除循环关联字段,这是生产最推荐方案。

实战提醒:不要直接把MyBatis实体直接当做接口返回对象,这是很多序列化问题的源头。

坑2:BigDecimal序列化变成科学计数法,前端解析出错
故障现象
金额字段使用BigDecimal存储,本地测试小数位数少,输出正常;线上遇到大金额,JSON输出变成1.23E+4科学计数法,前端JS解析出错,金额显示异常。

❌复现代码
java
@Data
public class GoodsVO {
private BigDecimal price;
}

根因:Jackson默认对大数BigDecimal会输出科学计数法字符串,JS的Number对科学计数法解析容易丢失精度,造成金额错乱。

✅修复方案
全局配置Jackson,将BigDecimal序列化为普通数字字符串,关闭科学计数输出。
java
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = converter.getObjectMapper();
mapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
return converter;
}

坑3:时间日期序列化各种乱码、时区偏移,时间差8小时
故障现象
DateLocalDateTime输出时间,本地时间正确,线上服务器时区不同,返回时间比实际时间偏移8小时;或者输出时间戳数字,前端想要格式化字符串,前后端联调大量报错。

❌错误情况:不做任何Jackson时间配置,依赖框架默认行为。

根因:Jackson对java8新时间类LocalDateTime默认不会做格式化;服务器操作系统时区和业务时区不一致,没有指定时区,序列化时间发生偏移。

✅修复方案
全局统一时间格式化,指定东八区时区。
java
@Bean
public ObjectMapper objectMapper(){
ObjectMapper mapper = new ObjectMapper();
//设置时区
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy‑MM‑dd HH:mm:ss")));
module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy‑MM‑dd HH:mm:ss")));
mapper.registerModule(module);
return mapper;
}

坑4:反序列化时,无参构造缺失,JSON转对象直接报错
故障现象
入参实体只写了有参构造函数,没有无参构造。接口接收JSON请求体,请求直接400,报找不到默认构造器异常。本地单元测试手动new对象一切正常,接口接收JSON才会报错。

❌错误代码
java
public class UserDTO {
private Long id;
private String name;
//只有有参构造,没有无参构造
public UserDTO(Long id,String name){
this.id = id;
this.name = name;
}
}

根因:Jackson反序列化,默认需要调用无参构造实例化对象,再通过setter赋值。如果只存在有参构造,缺少无参构造,直接抛出异常。
注意:使用lombok的@Data,默认会生成无参构造;如果手写全参构造,lombok不再生成无参构造,线上极易踩坑。

✅修复方案

  1. 显式增加无参构造函数;
  2. 使用@NoArgsConstructor注解;
  3. 使用@JsonCreator指定构造器用于反序列化。

    坑5:字段使用transient修饰,序列化字段直接丢失
    故障现象
    实体字段加transient关键字,业务期望JSON输出该字段,接口返回直接缺失该字段。本地打印对象可以看到字段值,JSON输出直接消失。

根因:Java原生transient关键字,代表该字段不参与Java序列化(ObjectOutputStream)。Jackson默认同样会忽略transient修饰字段。很多开发混淆Java原生序列化与JSON序列化,踩坑。

✅修复方案
如果业务需要JSON输出transient字段,添加注解@JsonInclude,或者使用@JsonProperty强制序列化。
java
@JsonProperty
private transient String tempField;

坑6:@JsonIgnore使用时机错误,入参接收不到前端传过来的字段
故障现象
在DTO字段上加@JsonIgnore,本意是接口返回时不输出该字段;结果前端传参过来,该字段也接收不到,赋值为null。

根因:@JsonIgnore默认同时作用序列化、反序列化双向。序列化输出忽略,同时接收JSON入参的时候也忽略该字段。很多人只知道它用来屏蔽输出,不知道反序列化同样生效。

✅修复方案
只控制序列化输出忽略,反序列化允许接收,使用:
java
@JsonIgnoreProperties(value = {"secret"}, allowSetters = true)

allowSetters=true:允许反序列化setter赋值,序列化输出忽略此字段,完美适配密码、密钥这类敏感字段。

坑7:泛型反序列化丢失类型,强转报ClassCastException
故障现象
使用ObjectMapper.readValue(json, new TypeReference<List<UserVO>>(){})反序列化集合;或者缓存取出JSON字符串,转泛型对象。本地测试正常,线上运行一段时间抛出ClassCastException,无法强转为目标对象。

根因:泛型类型擦除,如果TypeReference使用不当,或者直接传入原始Class,Jackson只能解析成LinkedHashMap,而不是目标VO对象,后续业务强转直接报错。偶现bug,不容易复现。

✅修复方案
集合泛型反序列化,必须使用TypeReference,不能直接传Class。
java
String jsonStr = "[{\"id\":1}]";
List list = objectMapper.readValue(jsonStr, new TypeReference<List>() {});

线上实战总结:Jackson开发几条生产准则

  1. 禁止直接把数据库实体作为接口返回VO,避免循环引用、敏感字段泄露;
  2. 处理BigDecimal金额一定要关闭科学计数法输出;
  3. java8时间类型必须全局配置格式化+指定时区,防止时间偏移;
  4. DTO实体不要随便删除无参构造,手写构造函数要注意lombok行为;
  5. transient关键字会被Jackson忽略,不要混淆Java序列化与JSON序列化;
  6. @JsonIgnore双向生效,敏感字段优先使用allowSetters模式;
  7. 泛型集合反序列化务必使用TypeReference,不要直接强转;
  8. 线上日志打印Jackson序列化异常,很多接口500根源都来自JSON转换。

JSON看着只是简单的数据转换,但是底层大量细节。本地简单对象测试不会暴露问题,线上复杂嵌套对象、金额、时间、泛型场景全部爆发。遇到JSON相关问题,优先打印原始JSON字符串,定位是输入问题还是序列化配置问题。

友情链接:
凡尘博客
凡尘影院
凡尘乡音|凡尘街坊
凡尘博客|雨落凡尘博客|羽落凡尘博客
凡尘博客|雨落凡尘博客|羽落凡尘博客

凡尘版权

本文原创,转载请完整保留版权与友链。

标签: none

添加新评论

  • 上一篇:
  • 下一篇: