当前位置: 首页 > 后端技术 > Java

SpringMVC循环引用的bug讨论

时间:2023-04-01 18:16:44 Java

准备两个带循环引用的对象:@DatapublicclassPerson{privateStringname;privateIdCardidCard;}@DatapublicclassIdCard{privateStringid;privatePersonperson;}直接带循环返回对象SpringMVC控制器中的引用:@RestControllerpublicclassHelloController{@RequestMapping("/hello")publicPersonhello(){Personperson=newPerson();person.setName("桐人");IdCardidCard=newIdCard();idCard.setId("xxx19950102xxx");person.setIdCard(idCard);idCard.setPerson(人);returnperson;}}执行curllocalhost:8080/hello,发现直接报了一个StackOverFlowError:问题分析不难理解中间发生了什么。您应该从堆栈和常识中了解一个事实。SpringMVC默认使用jackson作为HttpMessageConverter,这样当我们返回对象时,会通过jackson的serializer序列化成json字符串,java训练的另一个事实是jackson在java中无法解析循环引用,嵌套套娃-风格分析最终导致StackOverFlowError。有人会说,你怎么会有循环引用呢?天知道业务场景有多诡异。由于Java不限制循环引用的存在,因此必须存在存在这种可能性的合理场景。如果你有一个一直运行流畅的在线接口,你就知道有一天,你会遇到一个包含循环引用的Objects,你看着打印出来的StackOverFlowError的堆栈,开始怀疑人生,哪个小(大)克(S)爱(B)做了这种事!我们先假设循环引用的存在是合理的,如何解决这个问题呢?最简单的方案:单向维护关联,参考Hibernate中OneToMany关联中单向映射的思想,需要去掉IdCard中的Person成员变量。或者,借助jackson提供的注解,指定忽略循环引用的字段,例如:@DatapublicclassIdCard{privateStringid;@JsonIgnoreprivatePersonperson;}当然,我也看了一些资料,试图找到一个更jacksonway的优雅解决方案,比如这两个注解:@JsonManagedReference@JsonBackReference但是在我看来,好像用处不大。当然,如果你不反感经常存在安全漏洞的fastjson,你也可以选择使用FastJsonHttpMessageConverter来替代jackson的默认实现,像这样:@BeanpublicHttpMessageConvertersfastJsonHttpMessageConverters(){//1。定义一个转换消息的对象FastJsonHttpMessageConverterfastConverter=newFastJsonHttpMessageConverter();//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,//日期转换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反序列化,还是可以解析成同一个对象。其实这个功能我在之前的文章《gson 替换 fastjson 引发的线上问题分析》已经介绍过了。使用FastJsonHttpMessageConverter可以完全避免循环引用的问题,这对于返回类型不固定的场景很有帮助,而@JsonIgnore只能作用于那些结构固定的循环引用对象。思考问题值得一提的是,为什么一般的标准JSON类库都没有那么重视循环引用的问题呢?Fastjson似乎反而是一个特例。我认为主要原因是JSON的序列化格式是通用的。$ref等契约信息在JSON规范中没有定义。fastjson可以保证$ref被序列化,反序列化时可以正常解析,但如果是跨框架、跨系统、跨语言等场景,这一切都是未知数。归根结底,这就是Java语言中的循环引用和JSON通用规范中没有包含的概念的差距(可能JSON规范描述了这个特性,但我没有找到,有问题请指正).我应该选择@JsonIgnore还是使用FastJsonHttpMessageConverter?经过以上思考,我想大家应该可以根据自己的场景选择合适的方案了。综上所述,如果选择FastJsonHttpMessageConverter,变化会很大。如果存量接口较多,建议做一个回归,确认在解决循环引用问题的同时,不要引入其他不兼容的变化。此外,您需要根据您的使用场景评估解决方案。如果存在循环引用,fastjson会使用$ref记录引用信息。请确保您的前端或接口可以识别此信息,因为这可能不是标准的JSON规范。也可以选择@JsonIgnore来实现最小的改动,但是也需要注意,如果根据序列化结果再次反序列化,引用信息不会自动恢复。