文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

浅析 SpringMVC 中返回对象的循环引用问题

2024-12-03 01:44

关注

问题发现

今天这个话题还是比较轻松的,可能很多朋友也都遇到过这个问题。

@RestController、@ResponseBody 等注解是我们在写 Web 应用时打交道最多的注解了,我们经常有这样的需求:返回一个对象给前端,SpringMVC 帮助我们序列化成 JSON 对象。而今天我要分享的话题也不是什么高深的内容,那就是返回对象中存在循环引用时问题的探讨。

该问题非常简单容易复现,直接上代码。

准备两个存在循环引用的对象:

  1. @Data 
  2. public class Person { 
  3.     private String name
  4.     private IdCard idCard; 
  5.  
  6. @Data 
  7. public class IdCard { 
  8.     private String id; 
  9.     private Person person; 

在 SpringMVC 的 controller 中直接返回存在循环引用的对象:

  1. @RestController 
  2. public class HelloController { 
  3.  
  4.     @RequestMapping("/hello"
  5.     public Person hello() { 
  6.         Person person = new Person(); 
  7.         person.setName("kirito"); 
  8.  
  9.         IdCard idCard = new IdCard(); 
  10.         idCard.setId("xxx19950102xxx"); 
  11.  
  12.         person.setIdCard(idCard); 
  13.         idCard.setPerson(person); 
  14.  
  15.         return person; 
  16.     } 

执行 curl localhost:8080/hello 发现,直接报了一个 StackOverFlowError:

StackOverFlow

问题剖析

不难理解这中间发生了什么,从堆栈和常识中都应当了解到一个事实,SpringMVC 默认使用了 jackson 作为 HttpMessageConverter,这样当我们返回对象时,会经过 jackson 的 serializer 序列化成 json 串,而另一个事实便是 jackson 是无法解析 java 中的循环引用的,套娃式的解析,最终导致了 StackOverFlowError。

有人会说,为什么你会有循环引用呢?天知道业务场景有多奇葩,既然 Java 没有限制循环引用的存在,那就肯定会有某一合理的场景存在该可能性,如果你在线上的一个接口一直平稳运行着,知道有一天,碰到了一个包含循环引用的对象,你看着打印出来的 StackOverFlowError 的堆栈,开始怀疑人生,是哪个小(大)可(S)爱(B)干的这种事!

我们先假设循环引用存在的合理性,如何解决该问题呢?最简单的解法:单向维护关联,参考 Hibernate 中的 OneToMany 关联中单向映射的思想,这需要干掉 IdCard 中的 Person 成员变量。或者,借助于 jackson 提供的注解,指定忽略循环引用的字段,例如这样:

  1. @Data 
  2. public class IdCard { 
  3.     private String id; 
  4.     @JsonIgnore 
  5.     private Person person; 

当然,我也翻阅了一些资料,尝试寻求 jackson 更优雅的解决方式,例如这两个注解:

  1. @JsonManagedReference 
  2. @JsonBackReference 

但在我看来,似乎他们并没有什么大用场。

当然,你如果不嫌弃经常出安全漏洞的 fastjson,也可以选择使用 FastJsonHttpMessageConverter 替换掉 jackson 的默认实现,像下面这样:

  1. @Bean 
  2. public HttpMessageConverters fastJsonHttpMessageConverters() { 
  3.     //1、定义一个convert转换消息的对象 
  4.     FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); 
  5.  
  6.     //2、添加fastjson的配置信息 
  7.     FastJsonConfig fastJsonConfig = new FastJsonConfig(); 
  8.  
  9.     SerializerFeature[] serializerFeatures = new SerializerFeature[]{ 
  10.         //    输出key是包含双引号 
  11.         //                SerializerFeature.QuoteFieldNames, 
  12.         //    是否输出为null的字段,若为null 则显示该字段 
  13.         //                SerializerFeature.WriteMapNullValue, 
  14.         //    数值字段如果为null,则输出为0 
  15.         SerializerFeature.WriteNullNumberAsZero, 
  16.         //     List字段如果为null,输出为[],而非null 
  17.         SerializerFeature.WriteNullListAsEmpty, 
  18.         //    字符类型字段如果为null,输出为"",而非null 
  19.         SerializerFeature.WriteNullStringAsEmpty, 
  20.         //    Boolean字段如果为null,输出为false,而非null 
  21.         SerializerFeature.WriteNullBooleanAsFalse, 
  22.         //    Date的日期转换器 
  23.         SerializerFeature.WriteDateUseDateFormat, 
  24.         //    循环引用 
  25.         //SerializerFeature.DisableCircularReferenceDetect, 
  26.     }; 
  27.  
  28.     fastJsonConfig.setSerializerFeatures(serializerFeatures); 
  29.     fastJsonConfig.setCharset(Charset.forName("UTF-8")); 
  30.  
  31.     //3、在convert中添加配置信息 
  32.     fastConverter.setFastJsonConfig(fastJsonConfig); 
  33.  
  34.     //4、将convert添加到converters中 
  35.     HttpMessageConverter converter = fastConverter; 
  36.  
  37.     return new HttpMessageConverters(converter); 

你可以自定义一些 json 转换时的 feature,当然我今天主要关注 SerializerFeature.DisableCircularReferenceDetect 这一属性,只要不显示开启该特性,fastjson 默认就能处理循环引用的问题。

如上配置后,让我们看看效果:

  1. {"idCard":{"id":"xxx19950102xxx","person":{"$ref":".."}},"name":"kirito"

已经正常返回了,fastjson 使用了"$ref":".." 这样的标识,解决了循环引用的问题,如果继续使用 fastjson 反序列化,依旧可以解析成同一对象,其实我在之前的文章中已经介绍过这一特性了《gson 替换 fastjson 引发的线上问题分析》。

使用 FastJsonHttpMessageConverter 可以彻底规避掉循环引用的问题,这对于返回类型不固定的场景十分有帮助,而 @JsonIgnore 只能作用于那些固定结构的循环引用对象上。

问题思考

值得一提的是,为什么一般标准的 JSON 类库并没有如此关注循环引用的问题呢?fastjson 看起来反而是个特例,我觉得主要还是 JSON 这种序列化的格式就是为了通用而存在的,$ref 这样的契约信息,并没有被 JSON 的规范去定义,fastjson 可以确保 $ref 在序列化、反序列化时能够正常解析,但如果是跨框架、跨系统、跨语言等场景,这一切都是个未知数了。说到底,这还是 Java 语言的循环引用和 JSON 通用规范不包含这一概念之间的 gap(可能 JSON 规范描述了这一特性,但我没有找到,如有问题,烦请指正)。

我到底应该选择 @JsonIgnore 还是使用 FastJsonHttpMessageConverter 呢?经历了上面的思考,我觉得各位看官应该能够根据自己的场景选择合适的方案了。

 

总结下,如果选择 FastJsonHttpMessageConverter ,改动较大,如果有较多的存量接口,建议做好回归,以确认解决循环引用问题的同时,别引入了其他不兼容的改动。并且,需要基于你的使用场景评估方案,如果出现了循环引用,fastjson 会使用 $ref 来记录引用信息,请确认你的前端或者接口方能够识别该信息,因为这可能并不是标准的 JSON 规范。你也可以选择 @JsonIgnore 来实现最小改动,但也同时需要注意,如果根据序列化的结果再次反序列化,引用信息可不会自动恢复。

 

来源:Kirito的技术分享内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯