Fastjson 1.x 隐形坑:加一个 boolean getter,序列化直接炸,报错 fieldName:0
本文基于真实生产事故整理,适合所有还在用 Fastjson 1.x 的 Java 后端同学避坑。
一、背景:我栽在了“加一个方法”上
最近在做成本分摊系统的迭代时,我在
CostShare实体类里加了一个纯内存计算的方法,用来判断是否为负数成本确认场景:public boolean getNegativeConfirmedFlag() { return (newPurchaseOriginFlag || getInstallFlag()) && Objects.nonNull(shareAmount) && shareAmount.compareTo(BigDecimal.ZERO) < 0; }
这个方法没有对应的类字段,纯粹是基于其他属性计算的“虚拟属性”。
但这个类里早就有另一个类似的 boolean getter:
public boolean getInstallFlag() { return Objects.nonNull(repaySeq) && repaySeq != 0; }
之前跑了快一年都没问题,偏偏加了 getNegativeConfirmedFlag()之后,一序列化 List<CostShare>就直接抛异常:
com.alibaba.fastjson.JSONException: write javaBean error, class com.gdfl.report.model.prd.CostShare, fieldName : 0 at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:364) at com.alibaba.fastjson.serializer.ListSerializer.write(ListSerializer.java:126)
最诡异的是:
toString()打印出来的 CostShare对象干干净净,根本没有叫 0的字段,Redis 缓存里的数据也查了个遍,没有任何脏数据。二、排查误区:我差点把 Map 翻烂了
一开始我和大多数开发者一样,看到
fieldName : 0第一反应是:肯定是哪个 Map 里混进了 key 为 "0" 的脏数据。于是我做了这些排查:
-
全局搜索
CostShare里的Map/JSONObject字段 —— 没有 -
打印缓存里的所有
CostShare对象的toString()—— 没有异常字段 -
甚至怀疑是 Fastjson 的反序列化残留 —— 清空缓存重新加载,依然报错直到我盯着异常栈里的
ListSerializer.write看了十分钟,突然反应过来:Fastjson 序列化的是 List,而 List 里的每个对象,Fastjson 会扫描所有 getter 方法,不管有没有对应的字段。
三、根因:Fastjson 1.x 的两个“祖传坑”
这个异常的本质是 Fastjson 1.x 的两个底层设计缺陷叠加导致的:
坑1:无字段 getter 默认被当成序列化属性
Fastjson 序列化 JavaBean 的核心逻辑是:扫描所有
getXxx()/isXxx()方法,把它们当成类的属性,不管有没有对应的成员变量。所以哪怕你的方法只是个纯计算逻辑,比如
getInstallFlag(),Fastjson 也会默认生成一个叫 installFlag的属性,序列化时一定会调用这个方法。坑2:多个 boolean 无字段 getter 触发 ASM 序列化 Bug
Fastjson 默认开启 ASM(字节码生成)来提升序列化性能,它生成的序列化代码对 单个 boolean 无字段 getter 是兼容的,但多个 boolean 无字段 getter + List 序列化 时,逻辑会直接错乱:
-
Fastjson 错误地把
getInstallFlag()/getNegativeConfirmedFlag()返回的true/false当成了可索引的对象(比如数组、集合) -
尝试访问这个“对象”的第 0 个元素,于是抛出
fieldName : 0的异常
为什么之前单个getInstallFlag()没事?因为单个 boolean getter 时,ASM 生成的代码刚好能“蒙混过关”,不会触发递归解析逻辑;一旦加了第二个,逻辑直接崩盘。
额外彩蛋:为什么 getInstallKey()(返回 String)没事?
因为 String 有独立的
StringSerializer,Fastjson 不会把它当成需要递归解析的 Bean,所以哪怕是无字段的 String getter,也不会触发这个 Bug。只有 boolean/Boolean 类型的无字段 getter 会踩中这个坑。四、为什么 @Transient没用?
很多同学会和我一样,以为 JPA 的
@Transient能让 Fastjson 忽略这个字段——大错特错:
|
注解
|
作用域
|
|---|---|
@Transient |
仅 JPA/Hibernate 生效,控制是否映射数据库字段
|
@JSONField(serialize = false) |
仅 Fastjson 生效,控制是否序列化该属性
|
@JsonIgnore |
仅 Jackson 生效
|
@Transient对 Fastjson 完全无效,这也是我一开始排查的另一个误区。五、解决方案(按优先级)
方案1:最小改动(生产首选)
给所有无字段 getter 加
@JSONField(serialize = false),明确告诉 Fastjson 不要序列化这个虚拟属性:@Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "cost_share") public class CostShare { // ... 原有字段 ... @JSONField(serialize = false) public String getInstallKey() { return orderNo + StringUtil.SEPARATOR + repaySeq; } @JSONField(serialize = false) public boolean getInstallFlag() { return Objects.nonNull(repaySeq) && repaySeq != 0; } @JSONField(serialize = false) public boolean getNegativeConfirmedFlag() { return (newPurchaseOriginFlag || getInstallFlag()) && Objects.nonNull(shareAmount) && shareAmount.compareTo(BigDecimal.ZERO) < 0; } }
✅ 优点:不动业务逻辑,不影响
toString(),风险极低✅ 适用场景:老项目、不能改序列化逻辑的紧急修复
方案2:临时止血(不用改代码)
如果暂时不能修改实体类,可以在序列化时关闭 ASM,强制用反射序列化,绕过 Bug:
String json = JSON.toJSONString( costShareList, SerializerFeature.DisableASM // 关闭 ASM,用反射序列化 );
⚠️ 缺点:序列化性能略有下降,但对后台批处理场景几乎无感知
方案3:长期根治(升级 Fastjson2)
Fastjson2 彻底重构了 Bean 序列化逻辑,完全修复了这个 Bug,性能和安全性也大幅提升:
<!-- 替换 fastjson 依赖 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version>
</dependency>
代码中把
import com.alibaba.fastjson.*替换为 import com.alibaba.fastjson2.*即可,API 基本兼容。六、避坑指南(亲测有效)
从这个坑里爬出来后,我总结了四条铁律,现在团队的代码都按这个规范来:
-
所有纯计算的虚拟属性 getter,一律加
@JSONField(serialize = false)不管是 boolean、String 还是其他类型,只要没有对应的成员变量,就明确禁止序列化。 -
不要用
getXxx命名纯计算方法虚拟属性的方法名改成calculateXxx()/computeXxx(),比如calculateNegativeConfirmedFlag(),Fastjson 不会扫描非get/is开头的方法,从根源上避免问题。 -
老项目尽快升级 Fastjson2Fastjson 1.x 已经停止维护,除了这个坑,还有很多安全漏洞,升级 Fastjson2 是长期最优解。
-
序列化 List 时出现诡异的
fieldName错误,第一时间检查无字段 getter90% 的概率是虚拟属性的锅,别去翻 Map 和缓存数据浪费时间。
七、总结
这个坑最恶心的地方在于:它不是你代码写错了,而是 Fastjson 1.x 的底层 Bug,而且只在特定组合下触发。
之前单个 boolean getter 没事,只是侥幸;加了第二个,刚好踩中了组合拳。希望这篇博客能帮你少熬两个排查的夜 😊
浙公网安备 33010602011771号