Skip to content

Fastjson的$ref在接口参数兼容上的隐患

oldclass_newclass.jpg 假设应用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,没有任何数据。

直接原因

cause.jpg 可以很容易复现这个问题:

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进行替代,这样可以减少很多数据量

循环引用

circleReference.jpg

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)

重复引用

duplicatedData.jpg

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"}]

总结

  1. 使用FastJson时,考虑关闭循环引用检测功能(DisableCircularReferenceDetect),避免引发此类问题。虽然循环引用会报错,但是本来就不应该写出循环引用这样的接口DTO类。
  2. 如果不想关闭循环引用检测,那么在升级接口时,应尽量破坏重复引用出现的条件之一:
  • 复制对象时,采用深拷贝的方式,避免复制Java引用
  • 如果是Map,引用的变量名,字典序靠前(很难做到)
转载请注明出处https://bananaoven.com/articles/35291.html | 香蕉微波炉
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。