当前位置: 首页 > 科技观察

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

时间:2023-03-12 00:27:03 科技观察

本文转载自微信公众号《桐人的技术分享》,作者kiritomoe。转载本文请联系桐人的技术分享公众号。题发现今天的题目比较容易,可能很多小伙伴都遇到过这个问题。@RestController、@ResponseBody等注解是我们在编写Web应用时接触最多的注解。我们经常有这样的需求:返回一个对象给前端,SpringMVC帮我们序列化成一个JSON对象。而我今天要分享的话题也不是很深刻,就是对返回对象存在循环引用时的问题的探讨。问题很简单,也很容易重现,直接上代码。准备两个循环引用的对象:@DatapublicclassPerson{privateStringname;privateIdCardidCard;}@DatapublicclassIdCard{privateStringid;privatePersonperson;}在SpringMVC控制器中直接返回循环引用的对象:@RestControllerpublicclassHelloController{@RequestMapping("/hello")publicPersonhello(){Personperson=newPerson();person.setName("kirito");IdCardidCard=newIdCard();idCard.setId("xxx19950102xxx");person.setIdCard(idCard);idCard.setPerson(person);returnperson;}}执行curllocalhost:8080/hello发现直接报了个StackOverFlowError:StackOverFlow问题分析不难理解中间发生了什么,从堆栈和常识上应该明白SpringMVC默认使用jackson作为HttpMessageConverter,所以当我们return对象,会通过jackson的serializer序列化成json字符串,还有一个就是jackson在java中不能解析循环引用,一个d套娃式的解析最终导致StackOverFlowError。有人会说,你怎么会有循环引用呢?天知道业务场景有多诡异。由于Java不限制循环引用的存在,因此必须存在存在这种可能性的合理场景。如果你有一个在线接口一直运行顺利直到有一天,当你遇到一个包含循环引用的对象时,你看着打印出来的StackOverFlowError的堆栈,开始怀疑人生,哪个小(大)克(S)爱(B)做了这件事!我们假设循环引用的合理性,如何解决这个问题?最简单的方案:单向维护关联,参考Hibernate中OneToMany关联中单向映射的思想,需要去掉IdCard中的Person成员变量。或者,借助jackson提供的注解,指定忽略循环引用的字段,比如这样:@DatapublicclassIdCard{privateStringid;@JsonIgnoreprivatePersonperson;}当然,我也浏览了一些资料,试图为jackson找到更优雅的解决方案,比如这两个Annotation:@JsonManagedReference@JsonBackReference但是在我看来,好像用处不大。当然,如果你不反感经常存在安全漏洞的fastjson,你也可以选择使用FastJsonHttpMessageConverter来替代jackson的默认实现,像这样:@BeanpublicHttpMessageConvertersfastJsonHttpMessageConverters(){//1。定义一个转换消息的对象FastJsonHttpMessageConverterfastConverter=newFastJsonHtterp;//2.添加fastjson配置信息FastJsonConfigfastJsonConfig=newFastJsonConfig();SerializerFeature[]serializerFeatures=newSerializerFeature[]{//输出key为包含双引号的字段//SerializerFeature.QuoteFieldNames,//输出是否为null字段,如果为Null显示字段//SerializerFeature.WriteMapNullValue,//如果value字段为null,则输出0SerializerFeature.WriteNullNumberAsZero,//如果List字段为null,则输出为[],不为nullSerializerFeature.WriteNullListAsEmpty,//字符类型字段如果为null,则输出为""而不是nullSerializerFeature.WriteNullStringAsEmpty,//如果布尔字段为null,则输出为false,而不是nullSerializerFeature.WriteNullBooleanAsFalse,//Date的日期转换器SerializerFeature.WriteDateUseDateFormat,//循环引用//SerializerFeature.DisableCircularReferenceDetect,};fastJsonConfig.setSerializerFeatures(serializerFeatures);fastJsonConfig.setCharset(Charset.forName("UTF-8"));//3.添加配置信息转换fastConverter.setFastJsonConfig(fastJsonConfig);//4.添加converttoconvertersHttpMessageConverterconverter=fastConverter;returnnewHttpMessageConverters(converter);}转换json时可以自定义一些特性。当然,今天我主要关注属性SerializerFeature.DisableCircularReferenceDetect。只要不显示和启用这个特性,fastjson默认就可以处理循环引用的问题。经过上面的配置,让我们看看效果:{"idCard":{"id":"xxx19950102xxx","person":{"$ref":".."}},"name":"kirito"}已经正常返回了,fastjson使用标记"$ref":".."解决了循环引用的问题。如果继续使用fastjson反序列化,还是可以解析成同一个对象。其实我在之前的文章中已经介绍过这个功能。?。使用FastJsonHttpMessageConverter可以完全避免循环引用的问题,这对于返回类型不固定的场景很有帮助,而@JsonIgnore只能作用于那些结构固定的循环引用对象。思考问题值得一提的是,为什么一般的标准JSON类库都没有那么重视循环引用的问题呢?Fastjson似乎是一个特例。我认为主要原因是JSON的序列化格式是通用的。$ref等契约信息在JSON规范中没有定义。fastjson可以保证$ref在序列化和反序列化的时候能够正常解析,但是如果是跨框架、跨系统、跨语言等场景,这一切都是一个未知数。归根结底,这就是Java语言中循环引用与JSON通用规范之间的差距,不包含这个概念(JSON规范可能描述了这个特性,但我没有找到,有问题请指正).我应该选择@JsonIgnore还是使用FastJsonHttpMessageConverter?经过以上思考,我想大家应该可以根据自己的场景选择合适的方案了。综上所述,如果选择FastJsonHttpMessageConverter,变化会很大。如果存量接口较多,建议做一个回归,确认在解决循环引用问题的同时,不要引入其他不兼容的变化。此外,您需要根据您的使用场景评估解决方案。如果存在循环引用,fastjson会使用$ref记录引用信息。请确保您的前端或接口可以识别此信息,因为这可能不是标准的JSON规范。也可以选择@JsonIgnore来实现最小的改动,但是也需要注意,如果根据序列化结果再次反序列化,引用信息不会自动恢复。