1. 程式人生 > 程式設計 >FastJson稍微使用不當就會導致StackOverflow

FastJson稍微使用不當就會導致StackOverflow

GitHub 9.4k Star 的Java工程師成神之路 ,不來瞭解一下嗎?

GitHub 9.4k Star 的Java工程師成神之路 ,真的不來瞭解一下嗎?

GitHub 9.4k Star 的Java工程師成神之路 ,真的確定不來瞭解一下嗎?

對於廣大的開發人員來說,FastJson大家一定都不陌生。

FastJson(github.com/alibaba/fas… )是阿里巴巴的開源JSON解析庫,它可以解析JSON格式的字串,支援將Java Bean序列化為JSON字串,也可以從JSON字串反序列化到JavaBean。

它具有速度快、使用廣泛、測試完備以及使用簡單等特點。但是,雖然有這麼多優點,但是不代表著就可以隨便使用,因為如果使用的方式不正確的話,就可能導致StackOverflowError。而StackOverflowError對於程式來說是無疑是一種災難。

筆者在一次使用FastJson的過程中就遇到了這種情況,後來經過深入原始碼分析,瞭解這背後的原理。本文就來從情景再現看是抽絲剝繭,帶大家看看坑在哪以及如何避坑。

緣由

FastJson可以幫助開發在Java Bean和JSON字串之間互相轉換,所以是序列化經常使用的一種方式。

有很多時候,我們需要在資料庫的某張表中儲存一些冗餘欄位,而這些欄位一般會通過JSON字串的形式儲存。比如我們需要在訂單表中冗餘一些買家的基本資訊,如JSON內容:

{
    "buyerName":"Hollis","buyerWechat":"hollischuang","buyerAgender":"male"
}
複製程式碼

因為這些欄位被冗餘下來,必定要有地方需要讀取這些欄位的值。所以,為了方便使用,一般也對定義一個對應的物件。

這裡推薦一個IDEA外掛——JsonFormat,可以一鍵通過JSON字串生成一個JavaBean。我們得到以下Bean:

public class BuyerInfo {

    /**
     * buyerAgender : male
     * buyerName : Hollis
     * buyerWechat : [email protected]
     */
    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的實現原理有關了。

FastJson的實現原理

關於序列化和反序列化的基礎知識大家可以參考Java物件的序列化與反序列化,這裡不再贅述。

FastJson的序列化過程,就是把一個記憶體中的Java Bean轉換成JSON字串,得到字串之後就可以通過資料庫等方式進行持久化了。

那麼,FastJson是如何把一個Java Bean轉換成字串的呢,一個Java Bean中有很多屬性和方法,哪些屬性要保留,哪些要剔除呢,到底遵循什麼樣的原則呢?

其實,對於JSON框架來說,想要把一個Java物件轉換成字串,可以有兩種選擇:

  • 1、基於屬性。
  • 2、基於setter/getter

關於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,我們接下來就將重點放在為什麼會出現迴圈呼叫上。

JavaBeanSerizlier序列化原理

我們已經知道,在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!

如何避免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進行序列化呢?

1、修改方法名

首先我們可以通過修改方法名的方式解決這個問題,我們把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);
    }
}
複製程式碼

2、使用JSONField註解

除了修改方法名以外,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);
    }
}
複製程式碼

3、使用JSONType註解

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的話,想要避免這個問題,可以採用以下方法:

  • 1、方法名不以get開頭
  • 2、使用@JSONField(serialize = false)修飾目標方法
  • 3、使用@JSONType修飾該Bean,並ignore掉方法對應的屬性名(getXxx -> xxx)

最後,作者之所以寫這篇文章,是因為在工作中真的實實在在的碰到了這個問題。

發生問題的時候,我立刻想到改個方法名,把getJsonString改成了toJsonString解決了這個問題。因為我之前看到過關於FastJson的簡單原理。

後來想著,既然FastJson設計成通過getter來進行序列化,那麼他一定提供了一個口子,讓開發者可以指定某些以get開頭的方法不參與序列化。

第一時間想到一般這種口子都是通過註解來實現的,於是開啟FastJson的原始碼,找到了對應的註解。

然後,趁著週末的時間,好好的翻了一下FastJson的原始碼,徹底弄清楚了其底層的真正原理。

以上就是我 發現問題——>分析問題——>解決問題——>問題的昇華 的全過程,希望對你有幫助。

通過這件事,筆者悟出了一個道理:

看過了太多的開發規範,卻依然還是會寫BUG!

希望通過這樣一篇小文章,可以讓你對這個問題有個基本的印象,萬一某一天遇到類似的問題,你可以馬上想到Hollis好像寫過這樣一篇文章。足矣!