循环引用致使的问题与解决方案

2021年09月09日 阅读数:21
这篇文章主要向大家介绍循环引用致使的问题与解决方案,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

返回结果中存在循环引用可能致使的问题。php


前言css


在公司的测试平台上,对新写的RPC接口进行测试,可是发现返回的是没法转换POJO的异常:


最初觉得只是业务代码写得有问题,结果发现问题并无那么简单!

排查思路前端


  业务代码问题


第一时间认为是本身业务代码的问题,因而使用公司开源的arthas工具初步确认接口返回的结果异常。然而事情并不如我所料,arthas显示个人接口屡次调用均正确地返回告终果。摘取两次显示的结果片断:vue



这个结果是出乎我意料的,因而我使用idea远程调试以进一步确认,结果也正常返回。所以能够判定不是业务代码出现问题。ios


  RPC框架问题


既然不是业务代码出现问题,那是否多是我服务端使用的RPC框架出现了问题呢?而且仔细观察测试平台显示的异常信息,能够看到异常是在hsf(公司的RPC框架)中抛出的,异常信息也是POJO对象转化异常,所以我大胆猜想是服务端RPC框架的序列化出现了问题。c++



因而根据异常信息,我在异常抛出的位置打下了断点进行调试,结果再次出乎个人意料——断点处并无进入并抛异常。所以排除服务端序列化问题。es6


  测试平台服务端问题


既然不是我服务端的序列化问题,那会不会是测试平台服务器这边的序列化问题呢?测试平台的服务器须要将远程调用结果以json的形式穿给前端进行展现,而咱们使用的RPC框架序列化协议是hessain2,所以在测试平台服务器在获得接口返回值后,还须要将其序列化为json给前端展现。web


经过询问RPC框架开发人员,得知测试平台的json序列化目前采用的jackson,故在本地使用jackson对结果进行序列化,结果抛出异常:sql



缘由是返回的类内部存在循环引用,致使jackson序列化时栈溢出。
npm


为了进一步确认采用hessain2序列化协议在服务间调用没有问题,我在另外一个服务中远程调用本文测试接口,发现结果是正常的。


至此,异常问题基本定位,产生的罪魁祸首在于返回结果中存在循环引用!


解决方案


因为后续咱们也须要对这个接口的返回值透出至前端展现,而且目前该类修改为本很大,它包含很是多领域的数据,能够说基本不可能修改,因此须要探寻其它的json序列化方案处理该复杂类以透出到前端展现。


  Gson


在使用Gson的过程当中,暴露了这个复杂类的另外一个问题——类中存在某成员与其父类成员同名,致使Gson抛出IllegalArgumentException异常,而目前该类是没法修改的,所以排除Gson方案。


  fastjson


另外一个就是经常使用的就是公司开源的fastjson了,最初直接使用toJSONString接口进行序列化,结果抛出了空指针异常。为了一探究竟,开始扒一扒它的源码。


阅读com.alibaba.fastjson.serializer.JavaBeanSerializer代码能够看到:



fastjson经过getter获取成员并进行序列化,而这里的getter阅读com.alibaba.fastjson.util.TypeUtils.computeGetters代码:



能够看到getter是根据getXxx方法获得,所以,它会调用getXxx方法,而咱们的类中存在与成员无关的get方法,同时该方法并无作好异常处理,所以在调用该方法时抛出了空指针异常。


为了解决这个问题,fastjson在1.2.7以后支持SerializerFeature.IgnoreNonFieldGetter参数,直接在toJSONString接口中添加便可。


至此,序列化结果顺利透出,问题获得解决。对于咱们这种复杂返回类型,也暴露了json序列化容易踩的坑,将来须要序列化的对象尽可能遵循几个原则:


  1. 避免循环依赖;

  2. 子类不与父类定义相同名称的成员;

  3. 避免定义非成员变量的getter/setter方法。


循环引用解决原理


循环引用形成问题的场景主要能够分为序列化和对象建立两种,假定类A对象引用类B对象,类B对象引用类A对象:


  1. 序列化A对象的时候,同时又会序列化B对象,序列化B对象的时候,又会反过来序列化A对象,若是采用递归实现,最终会致使堆栈溢出,若是采用循环实现,则会致使死循环;

  2. 建立A对象的时候,须要装载B对象,而B对象又反过来须要装载A,最终致使对象建立失败。


为了解决循环引用带来的上述问题,本质上须要将循环引用中的其中一个对象缓存起来,以免重复地序列化或建立。具体案例以下:


  fastjson/hessian


前文提到的jackson和Gson是不支持存在循环引用对象的序列化的,fastjson/hessain则是解决序列化场景循环引用的典型。阅读com.alibaba.fastjson.serializer.JavaBeanSerializer代码能够看到:



它有专门针对循环引用的方法,这里实际是对对象进行了缓存,若是引用的对象在缓存中,则不进一步进行序列化,而是以ref符号代替,规则以下:


语法

描述

{“$ref”:”\$”}

引用根对象

{“$ref”:”@”}

引用本身

{“$ref”:”..”}

引用父对象

{“$ref”:”../..”}

引用父对象的父对象

{“$ref”:\$.members[0].reportTo”}

基于路径的引用


hessian实际也是采用的相同的方式解决循环引用问题的,阅读com.caucho.hessian.io.AbstractSerializer便可看到:



这里采用的方式是同样的,每次序列化前先判断是否在缓存中便可。


  Spring


Spring是自动建立对象场景的典型,它采用三级缓存的方式解决循环引用对象的建立。


一级缓存:已经彻底建立好的对象的缓存;

二级缓存:正在建立中,某些成员还未装载的对象的缓存;

三级缓存:存放建立对象方法的缓存(即存放工厂,而非对象的缓存)。


假定类A对象引用类B对象,类B对象引用类A对象,在建立类A对象的过程当中,须要装载B对象,这时首先会在一级缓存中寻找B对象,若没有,则在二级缓存在找,若依然没有,则会从三级缓存找到建立B的方法,并建立一个"裸"bean(未装载成员对象的bean),放进二级缓存,而后将这个对象装载给A对象,同时还会将三级缓存中建立B的方法移除,防止重复建立,最后将A对象放入一级缓存。建立B对象时,直接在一级缓存中便可找到A对象进行装载,最后再将本身放入一级缓存中。


实际整个过程当中,二级缓存承担的是解决循环引用问题的角色,我的理解三级缓存主要是为了实现上的优雅而存在的,没有也不影响循环引用问题的解决。


总结


从开发侧和工具侧两个方面作一些总结:


  • 开发侧:json序列化是很容易踩坑的,将来须要序列化的对象尽可能作到避免循环依赖、子类不与父类定义相同名称的成员、避免定义非成员变量的getter/setter方法。

  • 工具侧:若是涉及到序列化和对象建立工具的开发,那么须要考虑循环引用问题的解决,主要方法即将循环引用中的其中一个对象缓存起来,以免重复地序列化或建立


✿  拓展阅读


做者| 谨寻
编辑| 橙子君
出品|阿里巴巴新零售淘系技术

本文分享自微信公众号 - 淘系技术(AlibabaMTT)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。