Json数据的重复引用/循环引用($ref)

引用符号

引用描述
"$ref":".."上一级
"$ref":"@"当前对象,也就是自引用
"$ref":"$"根对象
"$ref":"$.children.0"基于路径的引用,相当于 root.getChildren().get(0)

1、什么是Json的重复引用和循环引用?

  • 重复引用:一个对象的多个属性同时引用同一个对象,或一个集合中同时添加了同一个对象。

在下方的代码中我们将同一个对象向一个集合中添加了两次(实际开发会有这样的需求),然后使用FastJson将集合转换成Json字符串。我们期待的结果应该是 [{"name":"test"},{"name":"test"},但是却得到了 [{"name":"test"},{"$ref":"$[0]"}]。在后端语言中是可以正常解析出来的,但是当涉及到前后端的数据交互时,前端语言就无法正确解析了,此时就需要取消重复引用。在进行转换时,可以关闭引用检测。

[{"name":"test"},{"$ref":"$[0]"}]:表示当前集合的第二个元素与第一个元素相同,引用第一个元素。

  • $ref:当前元素引用其它元素

  • $[0]:引用当前集合的0号元素

    // 默认开启检测
    @Test
    public void test1() {
        // 存储集合
        ArrayList<One> ones = new ArrayList<>();

        // 引用对象
        One one = new One();
        one.setName("test");

        // 将同一个对象添加到集合中
        ones.add(one);
        ones.add(one);

        // 使用FastJson将集合转换成Json字符串
        String json = JSON.toJSONString(ones);
        System.out.println(json); // 打印Json结果:[{"name":"test"},{"$ref":"$[0]"}]
    }


    // 开启引用检测
    @Test
    public void test1() {
        // 存储集合
        ArrayList<One> ones = new ArrayList<>();

        // 引用对象
        One one = new One();
        one.setName("test");

        // 将同一个对象添加到集合中
        ones.add(one);
        ones.add(one);

        // 使用FastJson将集合转换成Json字符串
        String json = JSON.toJSONString(ones, SerializerFeature.DisableCircularReferenceDetect);
        System.out.println(json); // 打印Json结果:[{"name":"test"},{"name":"test"}]
    }

 

  • 循环引用:多个对象/集合之间存在相互引用,比如A对象引用B对象,同时B对象引用A对象。

创建两个Map集合,使两者之间相互引用,然后使用FastJson转换成Json字符串,在转换成时就会出现两者循环调用的问题。即在转换Map1时,发现Map1引用着Map2,然后将Map2引入,引入Map2时发现Map2又引用着Map1......,从此进入死循环之中。在FastJson中,默认对这种死循环的相互调用进行了处理(默认开启了循环引用的检测,遇到循环引用就使用引用符号代替),如果关闭循环引用检测,FastJson就不会再使用引用符号替代引用对象,这样就会导致无限死循环的相互引用,会造成java.lang.StackOverflowError(栈内存溢出错误)。

循环引用在实际开发中是会使用到的,比如使用基于角色的访问控制中,会涉及到用户、角色、权限这三者之间的多对多的对应的关系。此时,使用实体类建立三者之间的对应关系时,就会使用到循环引用:一个用户可以拥有多个角色,一个角色可以被多个用户拥有、同时一个角色也可以拥有多种权限,一种权限又可以被多个角色拥有。

    // 默认开启循环检测
    @Test
    public void test3() {
       Map map1 = new HashMap();
        map1.put("test1", "Map1测试数据");
        Map map2 = new HashMap();
        map2.put("test2", "Map2测试数据");

        // 循环引用
        map1.put("Map1引用Map2", map2);
        map2.put("Map2引用Map1", map1);

        // 使用FastJson转换成Json字符串
        String json = JSON.toJSONString(map1);

        // {"引用map2":{"map2":"test2","引用map1":{"$ref":".."}},"map1":"test1"}
        System.out.println(json); 

    }
    
    // 关闭循环检测
    @Test
    public void test4() {
        Map<String, Object> map1 = new HashMap<>();
        map1.put("map1", "test1");

        Map<String, Object> map2 = new HashMap<>();
        map2.put("map2", "test2");


        // 循环引用
        map1.put("引用map2", map2);
        map2.put("引用map1", map1);

        // SerializerFeature:序列化器特性;DisableCircularReferenceDetect:禁用循环引用检测
        String json = JSON.toJSONString(map1,SerializerFeature.DisableCircularReferenceDetect);
        
        // java.lang.StackOverflowError
        System.out.println(json);
    }

 

2、怎么解决Json的重复引用和循环引用问题?

在将数据转换成Json数据时,可以通过关闭循环引用的检测来消除Json数据的引用符号,而使用真正的对象数据来显示。但是当遇到循环引用时,就会导致不断的进行引用进入死循环,就会导致程序崩溃而抛出栈内存溢出错误。

所以,FastJson的循环引用一般情况下因该使其保持默认的开启检测状态。如果涉及到循环引用,因该将其拆分开来进行存储。

下面使用基于角色的访问控制种用户和角色实体类之间的对应关系来演示引用问题的解决(简单的对应关系)。

// 用户实体类
public class User implements Serializable {

    private String username; // 用户名
    private String password; // 密码
    private List<Role> roles; // 角色集合

}


// 角色实体类
public class Role implements Serializable {

    private String roleName; // 权限名称
    private List<User> users; // 对应的用户集合

}


    @Test
    public void test() {
        // 建立实体类对象
        Use user = new Use("jack", "123"); // 用户对象
        Role role = new Role("管理员"); // 角色对象

        // 建立关系
        List<Role> roles = new ArrayList<>(); // 用户的角色集合
        roles.add(role);
        user.setRoles(roles);

        List<Use> users = new ArrayList<>(); // 角色的用户集合
        users.add(user);
        role.setUsers(users);

        // 转换成Json数据
        String json = JSON.toJSONString(user); // 默认开启循环引用检测,使用引用符号
        // {"password":"123","roles":[{"roleName":"管理员","users":[{"$ref":"$"}]}],"username":"jack"}
        System.out.println(json);
    }

循环引用导致出现引用符号和栈内存溢出的问题根本原因就是(以User和Role为列):user对象引用了roles集合,而roles集合中的role对象中又引用了user对象。转换过程如下:

  1. 先转换user对象。
  2. 然后转换roles集合,进而转换roles集合中的role对象。
  3. 由于role对象又引用了user对象,所以再次转换user对象。
  4. 重复1~3步。

从上可知,问题就出现在第三步的引用上,同时Json数据的转换是根据对象的属性进行转换的。因此,我们只需要在第三步转换role对象时,使其忽略role对现象中的对user引用,就可以对当前对象的引用和避免进入死循环的引用关系。

忽略对象属性,在进行Json数据转换时不进行转换的方法:

1、@JSONField注解(静态)

  1. 作用:fastJson提供的注解,为实体类的属性进行一些Json转换的配置。
  2. serialize属性:标明该属性在进行Json转换时,是否进行转换。默认为true进行转换,false表示忽略不进行转换。
  3. 弊端:直接使用注解修饰实体类的属性,直接写死在源代码中,当需求变更时需要修改实体类的源代码(实体类在不同的场景有不同的需求)。

如下方代码,使用@JSONField注解修饰Role对象的users属性,并指定serialize属性为false。然后在开启循环检测的情况下再次进行转换,当转换到role中的users对象时,就会自动忽略users对象不进行转换,这样就把不会出现引用问题了。

// 用户实体类
public class User implements Serializable {

    private String username; // 用户名
    private String password; // 密码
    private List<Role> roles; // 角色集合

}


// 角色实体类
public class Role implements Serializable {

    private String roleName; // 权限名称

    @JSONField(serialize = false) // 忽略属性,不进行转换。
    private List<User> users; // 对应的用户集合

}


    @Test
    public void test() {
        // 建立实体类对象
        Use user = new Use("jack", "123"); // 用户对象
        Role role = new Role("管理员"); // 角色对象

        // 建立关系
        List<Role> roles = new ArrayList<>(); // 用户的角色集合
        roles.add(role);
        user.setRoles(roles);

        List<Use> users = new ArrayList<>(); // 角色的用户集合
        users.add(user);
        role.setUsers(users);

        // 转换成Json数据
        String json = JSON.toJSONString(user); // {"password":"123","roles":[{"roleName":"管理员"}],"username":"jack"}
        System.out.println(json);
    }

 

2、使用过滤器指定需要转换的属性(动态)

定义一个过滤器(SimplePropertyPreFilter),指定我们需要进行Json转换的实体类名称。这样,在使用FastJson转换对象时,只有在过滤器中指定的属性才会进行转换,没有指定的自动忽略。

注意:这种方式把动态的指定需要转换的对象属性,但是要求不同的属性名称不能相同。

    @Test
    public void test() {
        // 建立实体类对象
        Use user = new Use("jack", "123"); // 用户对象
        Role role = new Role("管理员"); // 角色对象

        // 建立关系
        List<Role> roles = new ArrayList<>(); // 用户的角色集合
        roles.add(role);
        user.setRoles(roles);

        List<Use> users = new ArrayList<>(); // 角色的用户集合
        users.add(user);
        role.setUsers(users);

         // 定义过滤器,指定需要进行转换的属性。
        SimplePropertyPreFilter filter = new SimplePropertyPreFilter("username", "password", "roles", "roleName");

        // 转换成Json数据
        String json = JSON.toJSONString(user, filter);
        System.out.println(json); // {"password":"123","roles":[{"roleName":"管理员"}],"username":"jack"}
    }

SimplePropertyPreFilter的构造函数的参数是可变参数类型的,可以直接传递参数,也可以将需要进行转化的属性存储到String[]数组中,然后将数组传递进去。

 public SimplePropertyPreFilter(String... properties){
        this(null, properties);
}

 

  • 11
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值