淺析 SpringMVC 中返回對象的循環(huán)引用問題
本文轉(zhuǎn)載自微信公眾號「Kirito的技術(shù)分享」,作者kiritomoe 。轉(zhuǎn)載本文請聯(lián)系Kirito的技術(shù)分享公眾號。
問題發(fā)現(xiàn)
今天這個話題還是比較輕松的,可能很多朋友也都遇到過這個問題。
@RestController、@ResponseBody 等注解是我們在寫 Web 應(yīng)用時打交道最多的注解了,我們經(jīng)常有這樣的需求:返回一個對象給前端,SpringMVC 幫助我們序列化成 JSON 對象。而今天我要分享的話題也不是什么高深的內(nèi)容,那就是返回對象中存在循環(huán)引用時問題的探討。
該問題非常簡單容易復(fù)現(xiàn),直接上代碼。
準(zhǔn)備兩個存在循環(huán)引用的對象:
- @Data
- public class Person {
- private String name;
- private IdCard idCard;
- }
- @Data
- public class IdCard {
- private String id;
- private Person person;
- }
在 SpringMVC 的 controller 中直接返回存在循環(huán)引用的對象:
- @RestController
- public class HelloController {
- @RequestMapping("/hello")
- public Person hello() {
- Person person = new Person();
- person.setName("kirito");
- IdCard idCard = new IdCard();
- idCard.setId("xxx19950102xxx");
- person.setIdCard(idCard);
- idCard.setPerson(person);
- return person;
- }
- }
執(zhí)行 curl localhost:8080/hello 發(fā)現(xiàn),直接報了一個 StackOverFlowError:
StackOverFlow
問題剖析
不難理解這中間發(fā)生了什么,從堆棧和常識中都應(yīng)當(dāng)了解到一個事實,SpringMVC 默認(rèn)使用了 jackson 作為 HttpMessageConverter,這樣當(dāng)我們返回對象時,會經(jīng)過 jackson 的 serializer 序列化成 json 串,而另一個事實便是 jackson 是無法解析 java 中的循環(huán)引用的,套娃式的解析,最終導(dǎo)致了 StackOverFlowError。
有人會說,為什么你會有循環(huán)引用呢?天知道業(yè)務(wù)場景有多奇葩,既然 Java 沒有限制循環(huán)引用的存在,那就肯定會有某一合理的場景存在該可能性,如果你在線上的一個接口一直平穩(wěn)運行著,知道有一天,碰到了一個包含循環(huán)引用的對象,你看著打印出來的 StackOverFlowError 的堆棧,開始懷疑人生,是哪個小(大)可(S)愛(B)干的這種事!
我們先假設(shè)循環(huán)引用存在的合理性,如何解決該問題呢?最簡單的解法:單向維護(hù)關(guān)聯(lián),參考 Hibernate 中的 OneToMany 關(guān)聯(lián)中單向映射的思想,這需要干掉 IdCard 中的 Person 成員變量?;蛘?,借助于 jackson 提供的注解,指定忽略循環(huán)引用的字段,例如這樣:
- @Data
- public class IdCard {
- private String id;
- @JsonIgnore
- private Person person;
- }
當(dāng)然,我也翻閱了一些資料,嘗試尋求 jackson 更優(yōu)雅的解決方式,例如這兩個注解:
- @JsonManagedReference
- @JsonBackReference
但在我看來,似乎他們并沒有什么大用場。
當(dāng)然,你如果不嫌棄經(jīng)常出安全漏洞的 fastjson,也可以選擇使用 FastJsonHttpMessageConverter 替換掉 jackson 的默認(rèn)實現(xiàn),像下面這樣:
- @Bean
- public HttpMessageConverters fastJsonHttpMessageConverters() {
- //1、定義一個convert轉(zhuǎn)換消息的對象
- FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
- //2、添加fastjson的配置信息
- FastJsonConfig fastJsonConfig = new FastJsonConfig();
- SerializerFeature[] serializerFeatures = new SerializerFeature[]{
- // 輸出key是包含雙引號
- // SerializerFeature.QuoteFieldNames,
- // 是否輸出為null的字段,若為null 則顯示該字段
- // SerializerFeature.WriteMapNullValue,
- // 數(shù)值字段如果為null,則輸出為0
- SerializerFeature.WriteNullNumberAsZero,
- // List字段如果為null,輸出為[],而非null
- SerializerFeature.WriteNullListAsEmpty,
- // 字符類型字段如果為null,輸出為"",而非null
- SerializerFeature.WriteNullStringAsEmpty,
- // Boolean字段如果為null,輸出為false,而非null
- SerializerFeature.WriteNullBooleanAsFalse,
- // Date的日期轉(zhuǎn)換器
- SerializerFeature.WriteDateUseDateFormat,
- // 循環(huán)引用
- //SerializerFeature.DisableCircularReferenceDetect,
- };
- fastJsonConfig.setSerializerFeatures(serializerFeatures);
- fastJsonConfig.setCharset(Charset.forName("UTF-8"));
- //3、在convert中添加配置信息
- fastConverter.setFastJsonConfig(fastJsonConfig);
- //4、將convert添加到converters中
- HttpMessageConverter<?> converter = fastConverter;
- return new HttpMessageConverters(converter);
- }
你可以自定義一些 json 轉(zhuǎn)換時的 feature,當(dāng)然我今天主要關(guān)注 SerializerFeature.DisableCircularReferenceDetect 這一屬性,只要不顯示開啟該特性,fastjson 默認(rèn)就能處理循環(huán)引用的問題。
如上配置后,讓我們看看效果:
- {"idCard":{"id":"xxx19950102xxx","person":{"$ref":".."}},"name":"kirito"}
已經(jīng)正常返回了,fastjson 使用了"$ref":".." 這樣的標(biāo)識,解決了循環(huán)引用的問題,如果繼續(xù)使用 fastjson 反序列化,依舊可以解析成同一對象,其實我在之前的文章中已經(jīng)介紹過這一特性了《gson 替換 fastjson 引發(fā)的線上問題分析》。
使用 FastJsonHttpMessageConverter 可以徹底規(guī)避掉循環(huán)引用的問題,這對于返回類型不固定的場景十分有幫助,而 @JsonIgnore 只能作用于那些固定結(jié)構(gòu)的循環(huán)引用對象上。
問題思考
值得一提的是,為什么一般標(biāo)準(zhǔn)的 JSON 類庫并沒有如此關(guān)注循環(huán)引用的問題呢?fastjson 看起來反而是個特例,我覺得主要還是 JSON 這種序列化的格式就是為了通用而存在的,$ref 這樣的契約信息,并沒有被 JSON 的規(guī)范去定義,fastjson 可以確保 $ref 在序列化、反序列化時能夠正常解析,但如果是跨框架、跨系統(tǒng)、跨語言等場景,這一切都是個未知數(shù)了。說到底,這還是 Java 語言的循環(huán)引用和 JSON 通用規(guī)范不包含這一概念之間的 gap(可能 JSON 規(guī)范描述了這一特性,但我沒有找到,如有問題,煩請指正)。
我到底應(yīng)該選擇 @JsonIgnore 還是使用 FastJsonHttpMessageConverter 呢?經(jīng)歷了上面的思考,我覺得各位看官應(yīng)該能夠根據(jù)自己的場景選擇合適的方案了。
總結(jié)下,如果選擇 FastJsonHttpMessageConverter ,改動較大,如果有較多的存量接口,建議做好回歸,以確認(rèn)解決循環(huán)引用問題的同時,別引入了其他不兼容的改動。并且,需要基于你的使用場景評估方案,如果出現(xiàn)了循環(huán)引用,fastjson 會使用 $ref 來記錄引用信息,請確認(rèn)你的前端或者接口方能夠識別該信息,因為這可能并不是標(biāo)準(zhǔn)的 JSON 規(guī)范。你也可以選擇 @JsonIgnore 來實現(xiàn)最小改動,但也同時需要注意,如果根據(jù)序列化的結(jié)果再次反序列化,引用信息可不會自動恢復(fù)。