FastJson不成想還有個版本2啊:序列化大字串報錯

2023-08-24 06:00:45

背景

發現陷入了一個怪圈,寫文章的話,感覺只有大bug或比較值得寫的內容才會寫,每次一寫就是幾千字,爭取寫得透徹一些,但這樣,我也挺費時間,讀者也未必有這麼多時間看。

我想著,日常遇到的小bug、平時工作中的一些小的心得體會,都還是可以寫寫,這樣也才是最貼近咱們作為一線開發生活的,也不必非得是個完整且深入的主題,因此,準備搞一個專門的標籤:點滴記錄Coding之路來記錄這些。

ok,咱們開始,最近,手下開發小哥去幫忙做一個其他組的專案,但遇到一些解決不了的問題就會找我幫忙看。最近來問我了一個問題,說是他有個介面,呼叫會報記憶體溢位,在本機就能復現,不知道咋回事。

上下文

介面程式碼如下:

在一個for迴圈裡面,會去執行sql,查詢資料庫記錄,存到dataList這個列表中,然後序列化為json,這裡呢,他們使用的是fastjson。

他呼叫介面給我演示了下,上面程式碼不是個迴圈嘛,跑著跑著就報錯了,報錯的棧大概如下(這個棧來自網上,問題類似):

Exception in thread "pool-4-thread-1" java.lang.OutOfMemoryError
	at com.alibaba.fastjson2.JSONWriterUTF16.writeNameRaw(JSONWriterUTF16.java:561)
	at com.alibaba.fastjson2.writer.FieldWriterImpl.writeFieldName(FieldWriterImpl.java:143)
	at com.alibaba.fastjson2.writer.ObjectWriter_3.write(Unknown Source)
	at com.alibaba.fastjson2.writer.ObjectWriterImplList.write(ObjectWriterImplList.java:278)
	at com.alibaba.fastjson2.JSON.toJSONString(JSON.java:1757)
	.....
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

排查

剛看到這個,也沒啥思路,一開始還以為是記憶體、gc之類的問題,看了會後,決定在報錯的地方打斷點看下,到底為啥報這個。

我這邊debug了兩圈後,發現都是走到如下位置的時候報錯:

這個函數,大概就是,在初步序列化物件為字串後,要計算字串的長度,然後看看這個長度能不能寫入到底層JsonWriter的字元陣列中(會比較字串的長度和JsonWriter中陣列的長度),如果JsonWriter中陣列長度過小,這裡就要觸發擴容。

而擴容前,如果發現要擴容的大小大於maxArraySize(一個設定項),就會拋這個記憶體溢位的溢位,並不是真的發生了記憶體溢位。

當時debug的時候,看到maxArraySize大概是60w多,大概就是60多m大小。當時就很納悶,是不是查出來的資料太大了,不然即使擴容啥的,也不可能大於60M,後面果然看到資料竟然達到了幾十M大小,由於這個系統我也沒參與,這塊業務合不合理就不管了,解決問題就行。

然後我就看了下,maxArraySize賦值的地方,看看這個能不能改大點,改大了就沒事了。

protected final int maxArraySize;

    protected JSONWriter(Context context, Charset charset) {
        this.context = context;
        this.charset = charset;
        this.utf8 = charset == StandardCharsets.UTF_8;
        this.utf16 = charset == StandardCharsets.UTF_16;

        quote = (context.features & Feature.UseSingleQuotes.mask) == 0 ? '"' : '\'';

        // 64M or 1G
        maxArraySize = (context.features & LargeObject.mask) != 0 ? 1073741824 : 67108864;
    }

這邊果然看到,有個註釋,64M OR 1G,果然,是個設定項,看起來,這個設定項是受LargeObject這個控制的。

一開始,我以為這個是com.alibaba.fastjson.serializer.SerializerFeature裡的列舉項,結果並不是,沒發現是JsonWriter的設定項:

com.alibaba.fastjson2.JSONWriter.Feature

知道是設定項了,問題是怎麼設定呢?仔細看了各個方法,都不能傳這種JsonWriter的列舉啊

後邊,看了半天,發現這個方法可以傳JsonWriter的feature:

問題是,這個defaultFeatures是int,32位元整數,每個bit代表一個特性,也就是說,我得自己計算將LargeObject這個bit置為1後,整個int的值。

大家看這個feature的值:

// 十進位制為:8589934592, 二進位制為:001000000000000000000000000000000000
LargeObject(1L << 33),

我就根據這個,自己把這個bit設為1,然後算了個值出來,結果,跟我說,超過了int的範圍,導致我沒法傳參進去。

解決

我都服了,然後開始在網上看看有沒有類似的問題,結果只找到了一篇文章。

https://blog.csdn.net/m0_68736501/article/details/132078314

解決辦法是說,升級jar包版本到2.0.16,裡面有個方法,可以傳JsonWriter的Feature列舉值進去:

JSON.toJSONString(t, JSONWriter.Feature.WriteClassName, JSONWriter.Feature.LargeObject).getBytes(DEFAULT_CHARSET);

結果我看了我們版本,都2.0.19了,版本比他還高,結果沒看到這個方法。服了,難道高版本還把這個方法刪了?

然後小夥子看我忙,就說他回去再研究研究,我說行,我也網上查下。

後邊也找到篇文章,讓他試試:https://www.exyb.cn/news/show-5352725.html,他沒說有沒有效果,但是過了一陣,他跟我說,知道問題了。

行吧,我給大家梳理下結論,我們的pom引入的依賴是:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.19</version>
</dependency>

這個內部其實還依賴了另外的jar:

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2-extension</artifactId>
</dependency>

而上面的這個,又依賴了:

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
</dependency>

差不多,就是下圖這樣的關係:

然後,導致我們專案中,其實有兩個JSON類:

com.alibaba.fastjson2.JSON;  位於fastjson2-2.0.19.jar
com.alibaba.fastjson.JSON;  位於fastjson-2.0.19.jar

而之前我們匯入的是下面那個,也就是傳統的com.alibaba.fastjson.JSON,裡面就是沒法傳JsonWriter的Feature列舉的,只有上面那個才有:

com.alibaba.fastjson2.JSON#toJSONString(java.lang.Object, com.alibaba.fastjson2.JSONWriter.Feature...)
    /**
     * Serialize Java Object to JSON {@link String} with specified {@link JSONReader.Feature}s enabled
     *
     * @param object Java Object to be serialized into JSON {@link String}
     * @param features features to be enabled in serialization
     */
    static String toJSONString(Object object, JSONWriter.Feature... features) {

所以,剩下的事情,簡單了,修改import的類為com.alibaba.fastjson2.JSON即可,然後序列化時傳入feature:

String previewDataJson = JSON.toJSONString(dataList,LargeObject);

問題解決。

結論

新專案建議還是用jackson算了,當然了,這個專案也不是我主導,而且都開發快完成了,就這樣吧,一般大問題也沒有,有就再改吧。