淺析?SpringMVC?中返回對(duì)象的循環(huán)引用問(wèn)題
時(shí)間:2021-08-19 16:27:59
手機(jī)看文章
掃描二維碼
隨時(shí)隨地手機(jī)看文章
[導(dǎo)讀]問(wèn)題發(fā)現(xiàn)今天這個(gè)話題還是比較輕松的,可能很多朋友也都遇到過(guò)這個(gè)問(wèn)題。@RestController、@ResponseBody等注解是我們?cè)趯慦eb應(yīng)用時(shí)打交道最多的注解了,我們經(jīng)常有這樣的需求:返回一個(gè)對(duì)象給前端,SpringMVC幫助我們序列化成JSON對(duì)象。而今天我要分享的...
問(wèn)題發(fā)現(xiàn)今天這個(gè)話題還是比較輕松的,可能很多朋友也都遇到過(guò)這個(gè)問(wèn)題。@RestController、@ResponseBody 等注解是我們?cè)趯?Web 應(yīng)用時(shí)打交道最多的注解了,我們經(jīng)常有這樣的需求:返回一個(gè)對(duì)象給前端,SpringMVC 幫助我們序列化成 JSON 對(duì)象。而今天我要分享的話題也不是什么高深的內(nèi)容,那就是返回對(duì)象中存在循環(huán)引用時(shí)問(wèn)題的探討。該問(wèn)題非常簡(jiǎn)單容易復(fù)現(xiàn),直接上代碼。準(zhǔn)備兩個(gè)存在循環(huán)引用的對(duì)象:
@Data
public?class?Person?{
????private?String?name;
????private?IdCard?idCard;
}
@Data
public?class?IdCard?{
????private?String?id;
????private?Person?person;
}
在 SpringMVC 的 controller 中直接返回存在循環(huán)引用的對(duì)象:@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),直接報(bào)了一個(gè) StackOverFlowError:
問(wèn)題剖析
不難理解這中間發(fā)生了什么,從堆棧和常識(shí)中都應(yīng)當(dāng)了解到一個(gè)事實(shí),SpringMVC 默認(rèn)使用了 jackson 作為 HttpMessageConverter,這樣當(dāng)我們返回對(duì)象時(shí),會(huì)經(jīng)過(guò) jackson 的 serializer 序列化成 json 串,而另一個(gè)事實(shí)便是 jackson 是無(wú)法解析 java 中的循環(huán)引用的,套娃式的解析,最終導(dǎo)致了 StackOverFlowError。有人會(huì)說(shuō),為什么你會(huì)有循環(huán)引用呢?天知道業(yè)務(wù)場(chǎng)景有多奇葩,既然 Java 沒(méi)有限制循環(huán)引用的存在,那就肯定會(huì)有某一合理的場(chǎng)景存在該可能性,如果你在線上的一個(gè)接口一直平穩(wěn)運(yùn)行著,知道有一天,碰到了一個(gè)包含循環(huán)引用的對(duì)象,你看著打印出來(lái)的 StackOverFlowError 的堆棧,開(kāi)始懷疑人生,是哪個(gè)小(大)可(S)愛(ài)(B)干的這種事!我們先假設(shè)循環(huán)引用存在的合理性,如何解決該問(wèn)題呢?最簡(jiǎn)單的解法:?jiǎn)蜗蚓S護(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)雅的解決方式,例如這兩個(gè)注解:@JsonManagedReference
@JsonBackReference
但在我看來(lái),似乎他們并沒(méi)有什么大用場(chǎng)。當(dāng)然,你如果不嫌棄經(jīng)常出安全漏洞的 fastjson,也可以選擇使用 FastJsonHttpMessageConverter 替換掉 jackson 的默認(rèn)實(shí)現(xiàn),像下面這樣:@Bean
public?HttpMessageConverters?fastJsonHttpMessageConverters()?{
????//1、定義一個(gè)convert轉(zhuǎn)換消息的對(duì)象
????FastJsonHttpMessageConverter?fastConverter?=?new?FastJsonHttpMessageConverter();
????//2、添加fastjson的配置信息
????FastJsonConfig?fastJsonConfig?=?new?FastJsonConfig();
????SerializerFeature[]?serializerFeatures?=?new?SerializerFeature[]{
????????//????輸出key是包含雙引號(hào)
????????//????????????????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)換時(shí)的 feature,當(dāng)然我今天主要關(guān)注 SerializerFeature.DisableCircularReferenceDetect 這一屬性,只要不顯示開(kāi)啟該特性,fastjson 默認(rèn)就能處理循環(huán)引用的問(wèn)題。如上配置后,讓我們看看效果:{"idCard":{"id":"xxx19950102xxx","person":{"$ref":".."}},"name":"kirito"}
已經(jīng)正常返回了,fastjson 使用了"$ref":".." 這樣的標(biāo)識(shí),解決了循環(huán)引用的問(wèn)題,如果繼續(xù)使用 fastjson 反序列化,依舊可以解析成同一對(duì)象,其實(shí)我在之前的文章中已經(jīng)介紹過(guò)這一特性了《gson 替換 fastjson 引發(fā)的線上問(wèn)題分析》。使用 FastJsonHttpMessageConverter 可以徹底規(guī)避掉循環(huán)引用的問(wèn)題,這對(duì)于返回類型不固定的場(chǎng)景十分有幫助,而 @JsonIgnore 只能作用于那些固定結(jié)構(gòu)的循環(huán)引用對(duì)象上。問(wèn)題思考
值得一提的是,為什么一般標(biāo)準(zhǔn)的 JSON 類庫(kù)并沒(méi)有如此關(guān)注循環(huán)引用的問(wèn)題呢?fastjson 看起來(lái)反而是個(gè)特例,我覺(jué)得主要還是 JSON 這種序列化的格式就是為了通用而存在的,$ref 這樣的契約信息,并沒(méi)有被 JSON 的規(guī)范去定義,fastjson 可以確保 $ref 在序列化、反序列化時(shí)能夠正常解析,但如果是跨框架、跨系統(tǒng)、跨語(yǔ)言等場(chǎng)景,這一切都是個(gè)未知數(shù)了。說(shuō)到底,這還是 Java 語(yǔ)言的循環(huán)引用和 JSON 通用規(guī)范不包含這一概念之間的 gap(可能 JSON 規(guī)范描述了這一特性,但我沒(méi)有找到,如有問(wèn)題,煩請(qǐng)指正)。我到底應(yīng)該選擇 @JsonIgnore 還是使用 FastJsonHttpMessageConverter ?呢?經(jīng)歷了上面的思考,我覺(jué)得各位看官應(yīng)該能夠根據(jù)自己的場(chǎng)景選擇合適的方案了。總結(jié)下,如果選擇 FastJsonHttpMessageConverter ,改動(dòng)較大,如果有較多的存量接口,建議做好回歸,以確認(rèn)解決循環(huán)引用問(wèn)題的同時(shí),別引入了其他不兼容的改動(dòng)。并且,需要基于你的使用場(chǎng)景評(píng)估方案,如果出現(xiàn)了循環(huán)引用,fastjson 會(huì)使用 $ref 來(lái)記錄引用信息,請(qǐng)確認(rèn)你的前端或者接口方能夠識(shí)別該信息,因?yàn)檫@可能并不是標(biāo)準(zhǔn)的 JSON 規(guī)范。你也可以選擇 @JsonIgnore 來(lái)實(shí)現(xiàn)最小改動(dòng),但也同時(shí)需要注意,如果根據(jù)序列化的結(jié)果再次反序列化,引用信息可不會(huì)自動(dòng)恢復(fù)。




