背景
2021年第一天早上,客户突然投诉说系统的一个功能出了问题,紧急排查后发现后端系统确实出了bug,原因为前端传输的JSON报文,后端反序列化成JavaBean后部分字段的值丢失了。
查看git提交历史记录,前端和后端近期并未对该功能的接口字段做任何修改,联想到上个版本升级了后端的FastJSON的版本,怀疑是后端系统对FastJSON升级导致的问题。
复现
1 2 3 4 5 6 7 8 9 10 11 12
| @Data static class Label { @JSONField(name = "label_id") private Integer labelId; private String labelName; }
public static void main(String[] args) { String value = "{'labelId': 1,'label_name':'name'}"; Label label = JSON.parseObject(value, Label.class); System.out.println(label); }
|
低版本
1 2 3 4 5
| <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.60</version> </dependency>
|
使用低版本FastJSON,如上使用1.2.60
版本,示例输出的结果如下,即两个字段JSON解析映射成功。虽然JavaBean中的字段和JSON中的key并不完全匹配(大小写不匹配以及下划线匹配),但得益于FastJSON的智能匹配,忽略了大小写和下划线,依然将JSON映射成功。
1
| Label(labelId=1, labelName=name)
|
高版本
1 2 3 4 5
| <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.71</version> </dependency>
|
使用高版本FastJSON,如上使用1.2.71
,示例输出结果如下,字段labelId
映射失败,即高版本FastJSON对智能匹配规则做了修改,并且未向前兼容而导致了部分字段映射失败导致了这次的bug。
1
| Label(labelId=null, labelName=name)
|
原理
解析高版本FastJSON字段智能匹配失败的原因,首先要先了解智能匹配的规则。
低版本
低版本的智能匹配规则的关键代码如下,翻译成人话就是:
- 如果JavaBean字段有
@JSONField
注解且name不空时,则对name的值忽略字母大小写和-
,_
两个字符
- 否则取JavaBean的字段名,忽略字母大小写和
-
,_
两个字符
- JSON中的key忽略
is
开头并忽略剩余字母大小写和-
,_
两个字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
long smartKeyHash = TypeUtils.fnv1a_64_lower(key); if (this.smartMatchHashArray == null) { long[] hashArray = new long[sortedFieldDeserializers.length]; for (int i = 0; i < sortedFieldDeserializers.length; i++) { hashArray[i] = TypeUtils.fnv1a_64_lower(sortedFieldDeserializers[i].fieldInfo.name); } Arrays.sort(hashArray); this.smartMatchHashArray = hashArray; }
int pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
boolean is = false; if (pos < 0 && (is = key.startsWith("is"))) { smartKeyHash = TypeUtils.fnv1a_64_lower(key.substring(2)); pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash); }
|

高版本
高版本的智能匹配规则的关键代码如下,翻译成人话就是:
- 如果JavaBean字段有
@JSONField
注解且name不空时,则取name的值
- 否则取JavaBean的字段名,忽略字母大小写和
-
,_
两个字符
- JSON中的key忽略
is
开头并忽略剩余字母大小写和-
,_
两个字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| if (this.smartMatchHashArray == null) { long[] hashArray = new long[sortedFieldDeserializers.length]; for (int i = 0; i < sortedFieldDeserializers.length; i++) { hashArray[i] = sortedFieldDeserializers[i].fieldInfo.nameHashCode; } Arrays.sort(hashArray); this.smartMatchHashArray = hashArray; }
long smartKeyHash = TypeUtils.fnv1a_64_extract(key); int pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
if (pos < 0) { long smartKeyHash1 = TypeUtils.fnv1a_64_lower(key); pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash1); }
boolean is = false; if (pos < 0 && (is = key.startsWith("is"))) { smartKeyHash = TypeUtils.fnv1a_64_lower(key.substring(2)); pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash); }
|
1 2 3 4 5 6 7 8 9
|
private long nameHashCode64(String name, JSONField annotation) { if (annotation != null && annotation.name().length() != 0) { return TypeUtils.fnv1a_64_extract(name); } return TypeUtils.fnv1a_64_lower(name); }
|

区别
高版本与低版本的智能匹配规则差异就是:如果JavaBean字段有@JSONField
注解且name不空时,低配版对name的值会忽略字母大小写和-
,_
两个字符,而高版本则直接取name的值不会做忽略操作。
因此示例中加了@JSONField
注解的labelId
字段才会因为FastJSON版本不同而导致反序列化结果的不同。
在对FastJSON的最新几个版本挨个排查后定位出智能匹配规则发生修改的版本为1.2.71
,所以如果代码中使用了智能匹配,那么建议谨慎升级到1.2.71
及其更高的版本。
另外这么明显的未向前兼容的规则修改,应该有很多人会踩坑。于是去FastJSON的GitHub上查看后果然已经有人提出了issues:1.2.71以上版本加了JSONField的字段无法反序列化。