Featured image of post 由 jackson 序列化循环引用所带来的启发

由 jackson 序列化循环引用所带来的启发

最近工作中使用 jackson 进行序列化过程中出来循环引用而导致序列化失败的情况,综合分析之后将自己感想进行记录

最近在开发过程中需要将一段构建好的树状数据序列化之后传递给前端,但是发现前段请求之后,后端一直卡在返回给前端数据那一步,之后后端报错:java.io.IOException: 你的主机中的软件中止了一个已建立的连接,起初没有特别在意,想着估计是数据量太大,返回时间超时,前端主动断开了连接,为了证实自己的想法,我使用fastjson将整个树状数据在后台序列化并统计了大小,发现数据量只有区区几百kb大小,那么之前认为数据量太大的想法便是错误的,而其真正的原因是什么呢?咱们接着往下看……

序列化过程中的循环引用

在发现返回给前端的数据量并不是十分大之后,我便将fastjson序列化成json的数据保存到了本地,在这些数据当中,我发现了在字节点中有很多以$ref开头的数据,而不是真实的字符串类型的数据。那么这些$ref开头的数据是什么呢?

通过查阅fastjson的项目主页文档,看到在循环引用章节有说到这些特殊字符串的含义,大概内容如下图:

循环引用

图中的意思是如果在序列化过程中fastjson发现存在循环引用的情况,那么便会使用以$ref开头的字符串替代接下来的解析,即它会主动帮助我们解决出现循环引用的情况。而jackson我猜其序列化过程中遇到循环引用的情况时,会直接抛出异常终止程序运行,至于为什么我没有在命令行看到相关代码抛出的异常,我猜应该是项目中的其它程序有捕获到该异常,然后没有抛出,导致我仅仅看到显示连接被断开。

思考验证

为了验证上述猜想,我们用下面的一个简单程序来进行测试:

 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
//People.java
package cn.yinan.jackson;

/**
 * @author yinan
 * @date 2019/11/10
 */
public class People {
    private String name;

    private People people;


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public People getPeople() {
        return people;
    }

    public void setPeople(People people) {
        this.people = people;
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// TestJson.java
package cn.yinan.jackson;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * @author yinan
 * @date 2019/11/10
 */
public class TestJson {
    public static void main(String[] args) throws JsonProcessingException {
        People father = new People();
        People son = new People();
        father.setName("father");
        son.setName("son");
        father.setPeople(son);
        son.setPeople(father);
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(father);
        System.out.println(json);
    }
}

运行上述代码,我们可以看到在序列化成字符串过程中直接抛出了如下异常:

循环引用异常

上述异常的原因是因为程序中存在引用链,导致循环得序列化过程中栈溢出。

解析来我们修改一下主函数中的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.yinan.jackson;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;

/**
 * @author yinan
 * @date 2019/11/10
 */
public class TestJson {
    public static void main(String[] args) throws JsonProcessingException {
        People father = new People();
        People son = new People();
        father.setName("father");
        son.setName("son");
        father.setPeople(son);
        son.setPeople(father);

        String fastJson = JSON.toJSONString(father);
        System.out.println(fastJson);
    }
}

继续运行程序,发现程序没有抛出异常,输出结果如下:

1
{"name":"father","people":{"name":"son","people":{"$ref":".."}}}

可以清晰看到,输出结果中出现了$ref,说明fastjson在序列化过程中遇到了循环引用问题,但是它帮助我们处理了这样的问题。这也证明了我刚才的猜想没有错,jackson会在序列化过程中遇到循环引用是抛出异常,而fastjson会自己帮我们先处理。

思考

借鉴上面的分析结果,我这里又有了一个问题,如果我们在开发过程中遇到一个这样的数据:已知一棵树,这棵树上有根结点-1,子结点p-1p-2,有孙子结点s-1s-2,其中子结点p-1的孩子结点是s-1,子结点p-2的孩子结点是s-2,而孙子结点s-1的孩子结点是p-2,孙子结点s-2的孩子结点是p-1;那么这棵树便是下面所示的样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-1
    - p-1
        - s-1
            - p-2
                - s-2
                    - ……
    - p-2
        - s-2
            - p-1
                - ……

这样的一棵树便是没有终点的树,那么如果我们遇到这样的树该如何处理?

从经验角度考虑,一般是两种解决方案,第一种是在构建树过程中限定递归层数,确保不会出现无限递归情况,而另外一种是判断是否出现环即出现A-B-C-A的情况,如果出现这样的情况那么不继续执行下去,以此解决递归出现的问题。

当然,限制递归深度的做法存在一个问题,那就是如果数据规模比较大的话,限制递归深度可能导致数据不完整,那么可以考虑自己使用一个栈来用非递归的方式实现上述逻辑,毕竟递归本质也是方法调用,最终还是数据的出栈和入栈。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus