FastJson稍微使用不当就会导致StackOverflow
GitHub 9.4k Star 的Java工程師成神之路 ,不來了解一下嗎?
GitHub 9.4k Star 的Java工程師成神之路 ,真的不來了解一下嗎?
GitHub 9.4k Star 的Java工程師成神之路 ,真的確定不來了解一下嗎?
對于廣大的開發(fā)人員來說,FastJson大家一定都不陌生。
FastJson(https://github.com/alibaba/fastjson)是阿里巴巴的開源JSON解析庫,它可以解析JSON格式的字符串,支持將Java Bean序列化為JSON字符串,也可以從JSON字符串反序列化到JavaBean。
它具有速度快、使用廣泛、測試完備以及使用簡單等特點(diǎn)。但是,雖然有這么多優(yōu)點(diǎn),但是不代表著就可以隨便使用,因?yàn)槿绻褂玫姆绞讲徽_的話,就可能導(dǎo)致StackOverflowError。而StackOverflowError對于程序來說是無疑是一種災(zāi)難。
筆者在一次使用FastJson的過程中就遇到了這種情況,后來經(jīng)過深入源碼分析,了解這背后的原理。本文就來從情景再現(xiàn)看是抽絲剝繭,帶大家看看坑在哪以及如何避坑。
緣由
FastJson可以幫助開發(fā)在Java Bean和JSON字符串之間互相轉(zhuǎn)換,所以是序列化經(jīng)常使用的一種方式。
有很多時(shí)候,我們需要在數(shù)據(jù)庫的某張表中保存一些冗余字段,而這些字段一般會通過JSON字符串的形式保存。比如我們需要在訂單表中冗余一些買家的基本信息,如JSON內(nèi)容:
{"buyerName":"Hollis","buyerWechat":"hollischuang","buyerAgender":"male" }因?yàn)檫@些字段被冗余下來,必定要有地方需要讀取這些字段的值。所以,為了方便使用,一般也對定義一個(gè)對應(yīng)的對象。
這里推薦一個(gè)IDEA插件——JsonFormat,可以一鍵通過JSON字符串生成一個(gè)JavaBean。我們得到以下Bean:
public class BuyerInfo {/*** buyerAgender : male* buyerName : Hollis* buyerWechat : hollischuang@qq.com*/private String buyerAgender;private String buyerName;private String buyerWechat;public void setBuyerAgender(String buyerAgender) { this.buyerAgender = buyerAgender;}public void setBuyerName(String buyerName) { this.buyerName = buyerName;}public void setBuyerWechat(String buyerWechat) { this.buyerWechat = buyerWechat;}public String getBuyerAgender() { return buyerAgender;}public String getBuyerName() { return buyerName;}public String getBuyerWechat() { return buyerWechat;} }然后在代碼中,就可以使用FastJson把JSON字符串和Java Bean進(jìn)行互相轉(zhuǎn)換了。如以下代碼:
Order order = orderDao.getOrder();// 把JSON串轉(zhuǎn)成Java Bean BuyerInfo buyerInfo = JSON.parseObject(order.getAttribute(),BuyerInfo.class);buyerInfo.setBuyerName("Hollis");// 把Java Bean轉(zhuǎn)成JSON串 order.setAttribute(JSON.toJSONString(buyerInfo)); orderDao.update(order);有的時(shí)候,如果有多個(gè)地方都需要這樣互相轉(zhuǎn)換,我們會嘗試在BuyerInfo中封裝一個(gè)方法,專門將對象轉(zhuǎn)換成JSON字符串,如:
public class BuyerInfo {public String getJsonString(){return JSON.toJSONString(this);} }但是,如果我們定義了這樣的方法后,我們再嘗試將BuyerInfo轉(zhuǎn)換成JSON字符串的時(shí)候就會有問題,如以下測試代碼:
public static void main(String[] args) {BuyerInfo buyerInfo = new BuyerInfo();buyerInfo.setBuyerName("Hollis");JSON.toJSONString(buyerInfo); }運(yùn)行結(jié)果:
可以看到,運(yùn)行以上測試代碼后,代碼執(zhí)行時(shí),拋出了StackOverflow。
從以上截圖中異常的堆棧我們可以看到,主要是在執(zhí)行到BuyerInfo的getJsonString方法后導(dǎo)致的。
那么,為什么會發(fā)生這樣的問題呢?這就和FastJson的實(shí)現(xiàn)原理有關(guān)了。
FastJson的實(shí)現(xiàn)原理
關(guān)于序列化和反序列化的基礎(chǔ)知識大家可以參考Java對象的序列化與反序列化,這里不再贅述。
FastJson的序列化過程,就是把一個(gè)內(nèi)存中的Java Bean轉(zhuǎn)換成JSON字符串,得到字符串之后就可以通過數(shù)據(jù)庫等方式進(jìn)行持久化了。
那么,FastJson是如何把一個(gè)Java Bean轉(zhuǎn)換成字符串的呢,一個(gè)Java Bean中有很多屬性和方法,哪些屬性要保留,哪些要剔除呢,到底遵循什么樣的原則呢?
其實(shí),對于JSON框架來說,想要把一個(gè)Java對象轉(zhuǎn)換成字符串,可以有兩種選擇:
- 1、基于屬性。
- 2、基于setter/getter
關(guān)于Java Bean中的getter/setter方法的定義其實(shí)是有明確的規(guī)定的,參考JavaBeans? Specification
而我們所常用的JSON序列化框架中,FastJson和jackson在把對象序列化成json字符串的時(shí)候,是通過遍歷出該類中的所有g(shù)etter方法進(jìn)行的。Gson并不是這么做的,他是通過反射遍歷該類中的所有屬性,并把其值序列化成json。
不同的框架進(jìn)行不同的選擇是有著不同的思考的,這個(gè)大家如果感興趣,后續(xù)文字可以專門介紹下。
那么,我們接下來深入一下源碼,驗(yàn)證下到底是不是這么回事。
分析問題的時(shí)候,最好的辦法就是沿著異常的堆棧信息,一點(diǎn)點(diǎn)看下去。我們再來回頭看看之前異常的堆棧:
我們簡化下,可以得到以下調(diào)用鏈:
BuyerInfo.getJsonString -> JSON.toJSONString-> JSONSerializer.write-> ASMSerializer_1_BuyerInfo.write-> BuyerInfo.getJsonString是因?yàn)樵贔astJson將Java對象轉(zhuǎn)換成字符串的時(shí)候,出現(xiàn)了死循環(huán),所以導(dǎo)致了StackOverflowError。
調(diào)用鏈中的ASMSerializer_1_BuyerInfo,其實(shí)是FastJson利用ASM為BuyerInfo生成的一個(gè)Serializer,而這個(gè)Serializer本質(zhì)上還是FastJson中內(nèi)置的JavaBeanSerizlier。
讀者可以自己試驗(yàn)一下,比如通過如下方式進(jìn)行degbug,就可以發(fā)現(xiàn)ASMSerializer_1_BuyerInfo其實(shí)就是JavaBeanSerizlier。 
之所以使用ASM技術(shù),主要是FastJson想通過動態(tài)生成類來避免重復(fù)執(zhí)行時(shí)的反射開銷。但是,在FastJson中,兩種序列化實(shí)現(xiàn)是并存的,并不是所有情況都需要通過ASM生成一個(gè)動態(tài)類。讀者可以嘗試將BuyerInfo作為一個(gè)內(nèi)部類,重新運(yùn)行以上Demo,再看異常堆棧,就會發(fā)現(xiàn)JavaBeanSerizlier的身影。
那么,既然是因?yàn)槌霈F(xiàn)了循環(huán)調(diào)用導(dǎo)致了StackOverflowError,我們接下來就將重點(diǎn)放在為什么會出現(xiàn)循環(huán)調(diào)用上。
JavaBeanSerizlier序列化原理
我們已經(jīng)知道,在FastJson序列化的過程中,會使用JavaBeanSerizlier進(jìn)行,那么就來看下 JavaBeanSerizlier到底做了什么,他是如何幫助FastJson進(jìn)行序列化的。
FastJson在序列化的過程中,會調(diào)用JavaBeanSerizlier的write方法進(jìn)行,我們看一下這個(gè)方法的內(nèi)容:
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {SerializeWriter out = serializer.out;// 省略部分代碼final FieldSerializer[] getters = this.getters;//獲取bean的所有g(shù)etter方法// 省略部分代碼for (int i = 0; i < getters.length; i) {//遍歷getter方法FieldSerializer fieldSerializer = getters[i];// 省略部分代碼Object propertyValue;// 省略部分代碼try {//調(diào)用getter方法,獲取字段值propertyValue = fieldSerializer.getPropertyValue(object);} catch (InvocationTargetException ex) {// 省略部分代碼}// 省略部分代碼} }以上代碼,我們省略了大部分代碼之后,可以看到邏輯相對簡單:就是先獲取要序列化的對象的所有g(shù)etter方法,然后遍歷方法進(jìn)行執(zhí)行,視圖通過getter方法獲得對應(yīng)的屬性的值。
但是,當(dāng)調(diào)用到我們定義的getJsonString方法的時(shí)候,進(jìn)而會調(diào)用到JSON.toJSONString(this),就會再次調(diào)用到JavaBeanSerizlier的write。如此往復(fù),形成死循環(huán),進(jìn)而發(fā)生StackOverflowError。
所以,如果你定義了一個(gè)Java對象,定一個(gè)了一個(gè)getXXX方法,并且在該方法中調(diào)用了JSON.toJSONString方法,那么就會發(fā)生StackOverflowError!
如何避免StackOverflowError
通過查看FastJson的源碼,我們已經(jīng)基本定位到問題了,那么如何避免這個(gè)問題呢?
還是從源碼入手,既然JavaBeanSerizlier的write方法會嘗試獲取對象的所有g(shù)etter方法,那么我們就來看下他到底是怎么獲取getter方法的,到底哪些方法會被他識別為"getter",然后我們再對癥下藥。
在JavaBeanSerizlier的write方法中,getters的獲取方式如下:
final FieldSerializer[] getters;if (out.sortField) {getters = this.sortedGetters; } else {getters = this.getters; }可見,無論是this.sortedGetters還是this.getters,都是JavaBeanSerizlier中的屬性,那么就繼續(xù)往上找,看看JavaBeanSerizlier是如何被初始化的。
通過調(diào)用棧追根溯源,我們可以發(fā)現(xiàn),JavaBeanSerizlier是在SerializeConfig的成員變量serializers中獲取到的,那么繼續(xù)深入,就要看SerializeConfig是如何被初始化的,即BuyerInfo對應(yīng)的JavaBeanSerizlier是如何被塞進(jìn)serializers的。
通過調(diào)用關(guān)系,我們發(fā)現(xiàn),SerializeConfig.serializers是通過SerializeConfig.putInternal方法塞值的:
而getObjectWriter中有關(guān)于putInternal的調(diào)用:
putInternal(clazz, createJavaBeanSerializer(clazz));這里面就到了我們前面提到的JavaBeanSerializer,我們知道createJavaBeanSerializer是如何創(chuàng)建JavaBeanSerializer的,并且如何設(shè)置其中的setters的就可以了。
private final ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, null, propertyNamingStrategy);if (beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz)) {return MiscCodec.instance;}return createJavaBeanSerializer(beanInfo); }重點(diǎn)來了,TypeUtils.buildBeanInfo就是重點(diǎn),這里面就到了我們要找的內(nèi)容。
buildBeanInfo調(diào)用了 computeGetters,深入這個(gè)方法,看一下setters是如何識別出來的。部分代碼如下:
for (Method method : clazz.getMethods()) {if (methodName.startsWith("get")) {if (methodName.length() < 4) {continue;}if (methodName.equals("getClass")) {continue;}....} }這個(gè)方法很長很長,以上只是截取了其中的一部分,以上只是做了個(gè)簡單的判斷,判斷方法是不是以’get’開頭,然后長度是不是小于3,在判斷方法名是不是getClass,等等一系列判斷。。。
下面我簡單畫了一張圖,列出了其中的核心判斷邏輯:
那么,通過上圖,我們可以看到computeGetters方法在過濾getter方法的時(shí)候,是有一定的邏輯的,只要我們想辦法利用這些邏輯,就可以避免發(fā)生StackOverflowError。
這里要提一句,下面將要介紹的幾種方法,都是想辦法使目標(biāo)方法不參與序列化的,所以要特別注意下。但是話又說回來,誰會讓一個(gè)JavaBean的toJSONString進(jìn)行序列化呢?
1、修改方法名
首先我們可以通過修改方法名的方式解決這個(gè)問題,我們把getJsonString方法的名字改一下,只要不以get開頭就可以了。
public class Main {public static void main(String[] args) {BuyerInfo buyerInfo = new BuyerInfo();buyerInfo.setBuyerName("Hollis");JSON.toJSONString(buyerInfo);} }class BuyerInfo {private String buyerAgender;private String buyerName;private String buyerWechat;//省略setter/getterpublic String toJsonString(){return JSON.toJSONString(this);} }2、使用JSONField注解
除了修改方法名以外,FastJson還提供了兩個(gè)注解可以讓我們使用,首先介紹JSONField注解,這個(gè)注解可以作用在方法上,如果其參數(shù)serialize設(shè)置成false,那么這個(gè)方法就不會被識別為getter方法,就不會參加序列化。
public class Main {public static void main(String[] args) {BuyerInfo buyerInfo = new BuyerInfo();buyerInfo.setBuyerName("Hollis");JSON.toJSONString(buyerInfo);} }class BuyerInfo {private String buyerAgender;private String buyerName;private String buyerWechat;//省略setter/getter@JSONField(serialize = false)public String getJsonString(){return JSON.toJSONString(this);} }3、使用JSONType注解
FastJson還提供了另外一個(gè)注解——JSONType,這個(gè)注解用于修飾類,可以指定ignores和includes。如下面的例子,如果使用@JSONType(ignores = “jsonString”)定義BuyerInfo,則也可避免StackOverflowError。
public class Main {public static void main(String[] args) {BuyerInfo buyerInfo = new BuyerInfo();buyerInfo.setBuyerName("Hollis");JSON.toJSONString(buyerInfo);} }@JSONType(ignores = "jsonString") class BuyerInfo {private String buyerAgender;private String buyerName;private String buyerWechat;//省略setter/getter public String getJsonString(){return JSON.toJSONString(this);} }總結(jié)
FastJson是使用非常廣泛的序列化框架,可以在JSON字符串和Java Bean之間進(jìn)行互相轉(zhuǎn)換。
但是在使用時(shí)要尤其注意,不要在Java Bean的getXXX方法中調(diào)用JSON.toJSONString方法,否則會導(dǎo)致StackOverflowError。
原因是因?yàn)镕astJson在序列化的時(shí)候,會根據(jù)一系列規(guī)則獲取一個(gè)對象中的所有g(shù)etter方法,然后依次執(zhí)行。
如果一定要定義一個(gè)方法,調(diào)用JSON.toJSONString的話,想要避免這個(gè)問題,可以采用以下方法:
- 1、方法名不以get開頭
- 2、使用@JSONField(serialize = false)修飾目標(biāo)方法
- 3、使用@JSONType修飾該Bean,并ignore掉方法對應(yīng)的屬性名(getXxx -> xxx)
最后,作者之所以寫這篇文章,是因?yàn)樵诠ぷ髦姓娴膶?shí)實(shí)在在的碰到了這個(gè)問題。
發(fā)生問題的時(shí)候,我立刻想到改個(gè)方法名,把getJsonString改成了toJsonString解決了這個(gè)問題。因?yàn)槲抑翱吹竭^關(guān)于FastJson的簡單原理。
后來想著,既然FastJson設(shè)計(jì)成通過getter來進(jìn)行序列化,那么他一定提供了一個(gè)口子,讓開發(fā)者可以指定某些以get開頭的方法不參與序列化。
第一時(shí)間想到一般這種口子都是通過注解來實(shí)現(xiàn)的,于是打開FastJson的源代碼,找到了對應(yīng)的注解。
然后,趁著周末的時(shí)間,好好的翻了一下FastJson的源代碼,徹底弄清楚了其底層的真正原理。
以上就是我 發(fā)現(xiàn)問題——>分析問題——>解決問題——>問題的升華 的全過程,希望對你有幫助。
通過這件事,筆者悟出了一個(gè)道理:
看過了太多的開發(fā)規(guī)范,卻依然還是會寫B(tài)UG!
希望通過這樣一篇小文章,可以讓你對這個(gè)問題有個(gè)基本的印象,萬一某一天遇到類似的問題,你可以馬上想到Hollis好像寫過這樣一篇文章。足矣!
總結(jié)
以上是生活随笔為你收集整理的FastJson稍微使用不当就会导致StackOverflow的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: hdu 3065 病毒侵袭持续中(AC自
- 下一篇: hdu 3068 最长回文(manach