假设应用1给应用2提供了一个接口,需要更新参数,将Map
变为List<Map>
,很容易写出这样的兼容代码:
1 2 3 4 5 6 7 8 9 10 11 @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,没有任何数据。
直接原因 可以很容易复现这个问题:
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 27 28 29 30 31 32 33 34 35 36 37 38 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); OldClass oldClass = JSON.parseObject(json1, OldClass.class); String json2 = JSON.toJSONString(oldClass); System.out.println(json2); System.out.println(oldClass.getBbbb().get("key" )); } @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进行替代,这样可以减少很多数据量
循环引用
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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); String json2 = JSON.toJSONString(a, SerializerFeature.DisableCircularReferenceDetect); System.out.println(json2); } @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处理为:
1 {"b":{"a": {"$ref":".."}, "name": "bbbb"}, "name": "aaaa"}
这里的..
表是上一级,也就是A本身。更多地:
引用
描述
“$ref”:”..”
上一级
“$ref”:”@”
当前对象,也就是自引用
“$ref”:”$”
根对象
“$ref”:”$.children.0”
基于路径的引用,相当于root.getChildren().get(0)
如果我们用SerializerFeature.DisableCircularReferenceDetect去除检测,则会报错:
1 2 3 4 5 6 7 String json2 = JSON.toJSONString(a, SerializerFeature.DisableCircularReferenceDetect);System.out.println(json2);
重复引用
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 27 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); String json2 = JSON.toJSONString(Arrays.asList(c1,c2, c3),SerializerFeature.DisableCircularReferenceDetect); System.out.println(json2); } @Data public static class ClassC { private String name; } }
c2直接引用了c1,c3和c1数据一样,可以看到c2被处理为了引用,c3却没有。所以FastJson处理重复引用条件之一是,Java中也要直接引用。
另外,对于issue的问题,假设我们把aaaa和bbbb换个名字,旧数据叫aaaa,新数据叫bbbb:
1 2 3 4 5 6 7 8 9 10 11 @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。
1 2 3 4 {"aaaa" :"[{"key" : "value" }], "bbbb" :{"$ref" :"$.aaaa[0]" }} {"aaaa" : {"key" : "value" }, "bbbb" :[{"$ref" : "$.aaaa" }]}
说明如果是map数据,如果要产生被引用$ref的效果,成员属性的变量名需要在字典序之后。
如果我们用SerializerFeature.DisableCircularReferenceDetect去除检测,则会恢复正常数据:
1 2 3 String json2 = JSON.toJSONString(Arrays.asList(c1,c2,c3),SerializerFeature.DisableCircularReferenceDetect);System.out.println(json2);
总结
使用FastJson时,考虑关闭循环引用检测功能(DisableCircularReferenceDetect),避免引发此类问题。虽然循环引用会报错,但是本来就不应该写出循环引用这样的接口DTO类。
如果不想关闭循环引用检测,那么在升级接口时,应尽量破坏重复引用出现的条件之一:
复制对象时,采用深拷贝的方式,避免复制Java引用
如果是Map,引用的变量名,字典序靠前(很难做到)