GitHub 9.4k Star 的Java工程師成神之路 ,不來了解一下嗎?java
GitHub 9.4k Star 的Java工程師成神之路 ,真的不來了解一下嗎?git
GitHub 9.4k Star 的Java工程師成神之路 ,真的肯定不來了解一下嗎?github
對於廣大的開發人員來講,FastJson你們必定都不陌生。數據庫
FastJson(github.com/alibaba/fas… )是阿里巴巴的開源JSON解析庫,它能夠解析JSON格式的字符串,支持將Java Bean序列化爲JSON字符串,也能夠從JSON字符串反序列化到JavaBean。json
它具備速度快、使用普遍、測試完備以及使用簡單等特色。可是,雖然有這麼多優勢,可是不表明着就能夠隨便使用,由於若是使用的方式不正確的話,就可能致使StackOverflowError。而StackOverflowError對於程序來講是無疑是一種災難。oracle
筆者在一次使用FastJson的過程當中就遇到了這種狀況,後來通過深刻源碼分析,瞭解這背後的原理。本文就來從情景再現看是抽絲剝繭,帶你們看看坑在哪以及如何避坑。框架
FastJson能夠幫助開發在Java Bean和JSON字符串之間互相轉換,因此是序列化常用的一種方式。源碼分析
有不少時候,咱們須要在數據庫的某張表中保存一些冗餘字段,而這些字段通常會經過JSON字符串的形式保存。好比咱們須要在訂單表中冗餘一些買家的基本信息,如JSON內容:測試
{
"buyerName":"Hollis",
"buyerWechat":"hollischuang",
"buyerAgender":"male"
}
複製代碼
由於這些字段被冗餘下來,一定要有地方須要讀取這些字段的值。因此,爲了方便使用,通常也對定義一個對應的對象。ui
這裏推薦一個IDEA插件——JsonFormat,能夠一鍵經過JSON字符串生成一個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進行互相轉換了。如如下代碼:
Order order = orderDao.getOrder();
// 把JSON串轉成Java Bean
BuyerInfo buyerInfo = JSON.parseObject(order.getAttribute(),BuyerInfo.class);
buyerInfo.setBuyerName("Hollis");
// 把Java Bean轉成JSON串
order.setAttribute(JSON.toJSONString(buyerInfo));
orderDao.update(order);
複製代碼
有的時候,若是有多個地方都須要這樣互相轉換,咱們會嘗試在BuyerInfo中封裝一個方法,專門將對象轉換成JSON字符串,如:
public class BuyerInfo {
public String getJsonString(){
return JSON.toJSONString(this);
}
}
複製代碼
可是,若是咱們定義了這樣的方法後,咱們再嘗試將BuyerInfo轉換成JSON字符串的時候就會有問題,如如下測試代碼:
public static void main(String[] args) {
BuyerInfo buyerInfo = new BuyerInfo();
buyerInfo.setBuyerName("Hollis");
JSON.toJSONString(buyerInfo);
}
複製代碼
運行結果:
能夠看到,運行以上測試代碼後,代碼執行時,拋出了StackOverflow。
從以上截圖中異常的堆棧咱們能夠看到,主要是在執行到BuyerInfo的getJsonString方法後致使的。
那麼,爲何會發生這樣的問題呢?這就和FastJson的實現原理有關了。
關於序列化和反序列化的基礎知識你們能夠參考Java對象的序列化與反序列化,這裏再也不贅述。
FastJson的序列化過程,就是把一個內存中的Java Bean轉換成JSON字符串,獲得字符串以後就能夠經過數據庫等方式進行持久化了。
那麼,FastJson是如何把一個Java Bean轉換成字符串的呢,一個Java Bean中有不少屬性和方法,哪些屬性要保留,哪些要剔除呢,到底遵循什麼樣的原則呢?
其實,對於JSON框架來講,想要把一個Java對象轉換成字符串,能夠有兩種選擇:
關於Java Bean中的getter/setter方法的定義實際上是有明確的規定的,參考JavaBeans(TM) Specification
而咱們所經常使用的JSON序列化框架中,FastJson和jackson在把對象序列化成json字符串的時候,是經過遍歷出該類中的全部getter方法進行的。Gson並非這麼作的,他是經過反射遍歷該類中的全部屬性,並把其值序列化成json。
不一樣的框架進行不一樣的選擇是有着不一樣的思考的,這個你們若是感興趣,後續文字能夠專門介紹下。
那麼,咱們接下來深刻一下源碼,驗證下究竟是不是這麼回事。
分析問題的時候,最好的辦法就是沿着異常的堆棧信息,一點點看下去。咱們再來回頭看看以前異常的堆棧:
咱們簡化下,能夠獲得如下調用鏈:
BuyerInfo.getJsonString
-> JSON.toJSONString
-> JSONSerializer.write
-> ASMSerializer_1_BuyerInfo.write
-> BuyerInfo.getJsonString
複製代碼
是由於在FastJson將Java對象轉換成字符串的時候,出現了死循環,因此致使了StackOverflowError。
調用鏈中的ASMSerializer_1_BuyerInfo,實際上是FastJson利用ASM爲BuyerInfo生成的一個Serializer,而這個Serializer本質上仍是FastJson中內置的JavaBeanSerizlier。
讀者能夠本身試驗一下,好比經過以下方式進行degbug,就能夠發現ASMSerializer_1_BuyerInfo其實就是JavaBeanSerizlier。
之因此使用ASM技術,主要是FastJson想經過動態生成類來避免重複執行時的反射開銷。可是,在FastJson中,兩種序列化實現是並存的,並非全部狀況都須要經過ASM生成一個動態類。讀者能夠嘗試將BuyerInfo做爲一個內部類,從新運行以上Demo,再看異常堆棧,就會發現JavaBeanSerizlier的身影。
那麼,既然是由於出現了循環調用致使了StackOverflowError,咱們接下來就將重點放在爲何會出現循環調用上。
咱們已經知道,在FastJson序列化的過程當中,會使用JavaBeanSerizlier進行,那麼就來看下 JavaBeanSerizlier到底作了什麼,他是如何幫助FastJson進行序列化的。
FastJson在序列化的過程當中,會調用JavaBeanSerizlier的write方法進行,咱們看一下這個方法的內容:
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的全部getter方法
// 省略部分代碼
for (int i = 0; i < getters.length; ++i) {//遍歷getter方法
FieldSerializer fieldSerializer = getters[i];
// 省略部分代碼
Object propertyValue;
// 省略部分代碼
try {
//調用getter方法,獲取字段值
propertyValue = fieldSerializer.getPropertyValue(object);
} catch (InvocationTargetException ex) {
// 省略部分代碼
}
// 省略部分代碼
}
}
複製代碼
以上代碼,咱們省略了大部分代碼以後,能夠看到邏輯相對簡單:就是先獲取要序列化的對象的全部getter方法,而後遍歷方法進行執行,視圖經過getter方法得到對應的屬性的值。
可是,當調用到咱們定義的getJsonString方法的時候,進而會調用到JSON.toJSONString(this),就會再次調用到JavaBeanSerizlier的write。如此往復,造成死循環,進而發生StackOverflowError。
因此,若是你定義了一個Java對象,定一個了一個getXXX方法,而且在該方法中調用了JSON.toJSONString方法,那麼就會發生StackOverflowError!
經過查看FastJson的源碼,咱們已經基本定位到問題了,那麼如何避免這個問題呢?
仍是從源碼入手,既然JavaBeanSerizlier的write方法會嘗試獲取對象的全部getter方法,那麼咱們就來看下他究竟是怎麼獲取getter方法的,到底哪些方法會被他識別爲"getter",而後咱們再對症下藥。
在JavaBeanSerizlier的write方法中,getters的獲取方式以下:
final FieldSerializer[] getters;
if (out.sortField) {
getters = this.sortedGetters;
} else {
getters = this.getters;
}
複製代碼
可見,不管是this.sortedGetters仍是this.getters,都是JavaBeanSerizlier中的屬性,那麼就繼續往上找,看看JavaBeanSerizlier是如何被初始化的。
經過調用棧追根溯源,咱們能夠發現,JavaBeanSerizlier是在SerializeConfig的成員變量serializers中獲取到的,那麼繼續深刻,就要看SerializeConfig是如何被初始化的,即BuyerInfo對應的JavaBeanSerizlier是如何被塞進serializers的。
經過調用關係,咱們發現,SerializeConfig.serializers是經過SerializeConfig.putInternal方法塞值的:
而getObjectWriter中有關於putInternal的調用:
putInternal(clazz, createJavaBeanSerializer(clazz));
複製代碼
這裏面就到了咱們前面提到的JavaBeanSerializer,咱們知道createJavaBeanSerializer是如何建立JavaBeanSerializer的,而且如何設置其中的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);
}
複製代碼
重點來了,TypeUtils.buildBeanInfo就是重點,這裏面就到了咱們要找的內容。
buildBeanInfo調用了 computeGetters,深刻這個方法,看一下setters是如何識別出來的。部分代碼以下:
for (Method method : clazz.getMethods()) {
if (methodName.startsWith("get")) {
if (methodName.length() < 4) {
continue;
}
if (methodName.equals("getClass")) {
continue;
}
....
}
}
複製代碼
這個方法很長很長,以上只是截取了其中的一部分,以上只是作了個簡單的判斷,判斷方法是否是以'get'開頭,而後長度是否是小於3,在判斷方法名是否是getClass,等等一系列判斷。。。
下面我簡單畫了一張圖,列出了其中的核心判斷邏輯:
那麼,經過上圖,咱們能夠看到computeGetters方法在過濾getter方法的時候,是有必定的邏輯的,只要咱們想辦法利用這些邏輯,就能夠避免發生StackOverflowError。
這裏要提一句,下面將要介紹的幾種方法,都是想辦法使目標方法不參與序列化的,因此要特別注意下。可是話又說回來,誰會讓一個JavaBean的toJSONString進行序列化呢?
首先咱們能夠經過修改方法名的方式解決這個問題,咱們把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/getter
public String toJsonString(){
return JSON.toJSONString(this);
}
}
複製代碼
除了修改方法名之外,FastJson還提供了兩個註解可讓咱們使用,首先介紹JSONField註解,這個註解能夠做用在方法上,若是其參數serialize設置成false,那麼這個方法就不會被識別爲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);
}
}
複製代碼
FastJson還提供了另一個註解——JSONType,這個註解用於修飾類,能夠指定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);
}
}
複製代碼
FastJson是使用很是普遍的序列化框架,能夠在JSON字符串和Java Bean之間進行互相轉換。
可是在使用時要尤爲注意,不要在Java Bean的getXXX方法中調用JSON.toJSONString方法,不然會致使StackOverflowError。
緣由是由於FastJson在序列化的時候,會根據一系列規則獲取一個對象中的全部getter方法,而後依次執行。
若是必定要定義一個方法,調用JSON.toJSONString的話,想要避免這個問題,能夠採用如下方法:
最後,做者之因此寫這篇文章,是由於在工做中真的實實在在的碰到了這個問題。
發生問題的時候,我馬上想到改個方法名,把getJsonString改爲了toJsonString解決了這個問題。由於我以前看到過關於FastJson的簡單原理。
後來想着,既然FastJson設計成經過getter來進行序列化,那麼他必定提供了一個口子,讓開發者能夠指定某些以get開頭的方法不參與序列化。
第一時間想到通常這種口子都是經過註解來實現的,因而打開FastJson的源代碼,找到了對應的註解。
而後,趁着週末的時間,好好的翻了一下FastJson的源代碼,完全弄清楚了其底層的真正原理。
以上就是我 發現問題——>分析問題——>解決問題——>問題的昇華 的全過程,但願對你有幫助。
經過這件事,筆者悟出了一個道理:
看過了太多的開發規範,卻依然仍是會寫BUG!
但願經過這樣一篇小文章,可讓你對這個問題有個基本的印象,萬一某一天遇到相似的問題,你能夠立刻想到Hollis好像寫過這樣一篇文章。足矣!