假设应用1给应用2提供了一个接口,需要更新参数,将Map变为List<Map>,很容易写出这样的兼容代码:
java
@Data
public static class OldClass {
private Map<String, String> bbbb;
}
@Data
public static class NewClass {
private List<Map<String,String>> aaaa;
@Deprecated
private Map<String, String> bbbb;
}然而在部署后发现,应用2拿到的数据对象中的bbbb,没有任何数据。
直接原因
可以很容易复现这个问题:
java
public class JsonIssue {
public static void main(String[] args) {
issue();
}
private static void issue() {
Map<String,String> map = new HashMap<>();
map.put("key", "value");
List<Map<String,String>> aaaa = Collections.singletonList(map);
NewClass newClass = new NewClass();
newClass.setAaaa(aaaa);
newClass.setBbbb(aaaa.get(0));
String json1 = JSON.toJSONString(newClass);
System.out.println(json1);
// {"aaaa" : [{"key": "value"}],"bbbb": {"$ref":"$.aaaa[0]"}}
OldClass oldClass = JSON.parseObject(json1, OldClass.class);
String json2 = JSON.toJSONString(oldClass);
System.out.println(json2);
// {"bbbb":{"$ref";"$.aaaa[0]"}}
System.out.println(oldClass.getBbbb().get("key"));
// null
}
@Data
public static class OldClass {
private Map<String, String> bbbb;
}
@Data
public static class NewClass {
private List<Map<String,String>> aaaa;
@Deprecated
private Map<String, String> bbbb;
}
}当兼容数据NewClass传递过去变成旧格式0ldClass后,bbbb原本期望的数据是:{"key": "value"},然而实际上却是{"$ref": "$.aaaa[0]"},而且此时旧数据并没有aaaa这个变量,数据就丢失了。
详细分析FastJson的引用$ref
fastjson默认对两类引用做了优化:
- 循环引用:A参数里有B,B参数里有A,引用就用$ref替代,避免循环解析
- 重复引用:一个相同对象出现多次,就用$ref进行替代,这样可以减少很多数据量
循环引用

java
public class JsonIssue {
public static void main(String[] args) {
circleReference();
}
private static void circleReference() {
ClassA a = new ClassA();
a.setName("aaaa");
ClassB b = new ClassB();
b.setName("bbbb");
b.setA(a);
a.setB(b);
String json1 = JSON.toJSONString(a);
System.out.println(json1);
// {"b":{"a": {"$ref":".."}, "name": "bbbb"}, "name": "aaaa"}
String json2 = JSON.toJSONString(a, SerializerFeature.DisableCircularReferenceDetect);
System.out.println(json2);
// Exception in thread "main" java.lang.StackOverflowError
// at com.alibaba.fastjson,serializer.SerializeWriter.writeFieldNameDirect(SerializeWriter,java:1609)
// at com.alibaba,fastjson,serializer,ASMSerializer_2_ClassB.writeDirectNonContext(Unknown Source)
// at com.alibaba,fastjson,serializer,ASMSerializer_1_ClassA.writeDirectNonContext(Unknown Source)
// at com.alibaba,fastjson.serializer,ASMSerializer_2_ClassB.writeDirectNonContext(Unknown Source)
}
@Data
public static class ClassA {
private String name;
private ClassB b;
}
@Data
public static class ClassB {
private String name;
private ClassA a;
}
}ClassA引用了ClassB,ClassB引用了ClassA,我们打印A,可以看到其被FastJson处理为:
{"b":{"a": {"$ref":".."}, "name": "bbbb"}, "name": "aaaa"}这里的..表是上一级,也就是A本身。更多地:
| 引用 | 描述 |
|---|---|
| "$ref":".." | 上一级 |
| "$ref":"@" | 当前对象,也就是自引用 |
| "$ref":"$" | 根对象 |
| "$ref":"$.children.0" | 基于路径的引用,相当于root.getChildren().get(0) |
如果我们用SerializerFeature.DisableCircularReferenceDetect去除检测,则会报错:
java
String json2 = JSON.toJSONString(a, SerializerFeature.DisableCircularReferenceDetect);
System.out.println(json2);
// Exception in thread "main" java.lang.StackOverflowError
// at com.alibaba.fastjson,serializer.SerializeWriter.writeFieldNameDirect(SerializeWriter,java:1609)
// at com.alibaba,fastjson,serializer,ASMSerializer_2_ClassB.writeDirectNonContext(Unknown Source)
// at com.alibaba,fastjson,serializer,ASMSerializer_1_ClassA.writeDirectNonContext(Unknown Source)
// at com.alibaba,fastjson.serializer,ASMSerializer_2_ClassB.writeDirectNonContext(Unknown Source)重复引用

java
public class JsonIssue {
public static void main(String[] args) {
duplicatedData();
}
private static void duplicatedData() {
ClassC c1 = new ClassC();
c1.setName("cccc");
ClassC c2 = c1;
ClassC c3 = new ClassC();
String json1 = JSON.toJSONString(Arrays.asList(c1, c2, c3));
System.out.println(json1);
// [{"name": "cccc"}, {"$ref":"$[0]"},{"name":"cccc"}]
String json2 = JSON.toJSONString(Arrays.asList(c1,c2, c3),SerializerFeature.DisableCircularReferenceDetect);
System.out.println(json2);
// [{"name":"cccc"}, {"name":"cccc"}, {"name":"cccc"}]
}
@Data
public static class ClassC {
private String name;
}
}c2直接引用了c1,c3和c1数据一样,可以看到c2被处理为了引用,c3却没有。所以FastJson处理重复引用条件之一是,Java中也要直接引用。
另外,对于issue的问题,假设我们把aaaa和bbbb换个名字,旧数据叫aaaa,新数据叫bbbb:
java
@Data
public static class OldClass {
private Map<String, String> aaaa;
}
@Data
public static class NewClass {
private List<Map<String,String>> bbbb;
@Deprecated
private Map<String, String> aaaa;
}则旧数据aaaa不会出现$ref,反而是新数据bbbb出现了$ref。
java
// 改名前,aaaa是新数据,bbbb是旧数据
{"aaaa":"[{"key": "value"}], "bbbb":{"$ref":"$.aaaa[0]"}}
// 改名后,aaaa是旧数据,bbbb是新数据
{"aaaa": {"key": "value"}, "bbbb":[{"$ref": "$.aaaa"}]}说明如果是map数据,如果要产生被引用$ref的效果,成员属性的变量名需要在字典序之后。
如果我们用SerializerFeature.DisableCircularReferenceDetect去除检测,则会恢复正常数据:
java
String json2 = JSON.toJSONString(Arrays.asList(c1,c2,c3),SerializerFeature.DisableCircularReferenceDetect);
System.out.println(json2);
// [{"name":"cccc"}, {"name": "cccc"}, {"name":"cccc"}]总结
- 使用FastJson时,考虑关闭循环引用检测功能(DisableCircularReferenceDetect),避免引发此类问题。虽然循环引用会报错,但是本来就不应该写出循环引用这样的接口DTO类。
- 如果不想关闭循环引用检测,那么在升级接口时,应尽量破坏重复引用出现的条件之一:
- 复制对象时,采用深拷贝的方式,避免复制Java引用
- 如果是Map,引用的变量名,字典序靠前(很难做到)



粤公网安备44030602007943号