廢棄fastjson!大型專案遷移Gson保姆級攻略
阿新 • • 發佈:2021-01-13
# 前言
大家好,又雙叒叕見面了,我是天天放大家鴿子的蠻三刀。
在被大家取關之前,我立下一個“遠大的理想”,一定要在這周更新文章。現在看來,flag有用了。。。
本篇文章是我這一個多月來幫助組內廢棄fastjson框架的總結,我們將大部分Java倉庫從fastjson遷移至了Gson。
這麼做的主要的原因是公司受夠了fastjson頻繁的安全漏洞問題,每一次出現漏洞都要推一次全公司的fastjson強制版本升級,很令公司頭疼。
**文章的前半部分,我會簡單分析各種json解析框架的優劣,並給出企業級專案遷移json框架的幾種解決方案。**
**在文章的後半部分,我會結合這一個月的經驗,總結下Gson的使用問題,以及fastjson遷移到Gson踩過的深坑。**
**文章目錄:**
- 為何要放棄fastjson?
- fastjson替代方案
- 三種json框架的特點
- 效能對比
- 最終選擇方案
- 替換依賴時的注意事項
- 謹慎,謹慎,再謹慎
- 做好開發團隊和測試團隊的溝通
- 做好迴歸/介面測試
- 考慮遷移前後的效能差異
- 使用Gson替換fastjson
- Json反序列化
- 範型處理
- List/Map寫入
- 駝峰與下劃線轉換
- 遷移常見問題踩坑
- Date序列化方式不同
- SpringBoot異常
- Swagger異常
- @Mapping JsonObject作為入參異常
**注意:是否使用fastjson是近年來一個爭議性很大的話題,本文無意討論框架選型的對錯,只關注遷移這件事中遇到的問題進行反思和思考。大家如果有想發表的看法,可以在評論區 理 性 討論。**
> 本文閱讀大概需要:5分鐘
>
> 碼字不易,歡迎關注我的個人公眾號:後端技術漫談
# 為何要放棄fastjson?
究其原因,**是fastjson漏洞頻發**,導致了公司內部需要頻繁的督促各業務線升級fastjson版本,來防止安全問題。
fastjson在2020年頻繁暴露安全漏洞,此漏洞可以繞過autoType開關來實現反序列化遠端程式碼執行並獲取伺服器訪問許可權。
從2019年7月份釋出的v1.2.59一直到2020年6月份釋出的 v1.2.71 ,每個版本的升級中都有關於AutoType的升級,涉及13個正式版本。
fastjson中與AutoType相關的版本歷史:
```
1.2.59釋出,增強AutoType開啟時的安全性 fastjson
1.2.60釋出,增加了AutoType黑名單,修復拒絕服務安全問題 fastjson
1.2.61釋出,增加AutoType安全黑名單 fastjson
1.2.62釋出,增加AutoType黑名單、增強日期反序列化和JSONPath fastjson
1.2.66釋出,Bug修復安全加固,並且做安全加固,補充了AutoType黑名單 fastjson
1.2.67釋出,Bug修復安全加固,補充了AutoType黑名單 fastjson
1.2.68釋出,支援GEOJSON,補充了AutoType黑名單
1.2.69釋出,修復新發現高危AutoType開關繞過安全漏洞,補充了AutoType黑名單
1.2.70釋出,提升相容性,補充了AutoType黑名單
1.2.71釋出,補充安全黑名單,無新增利用,預防性補充
```
相比之下,其他的json框架,如Gson和Jackson,漏洞數量少很多,**高危漏洞也比較少**,這是公司想要替換框架的主要原因。
# fastjson替代方案
**本文主要討論Gson替換fastjson框架的實戰問題,所以在這裡不展開詳細討論各種json框架的優劣,只給出結論。**
經過評估,主要有Jackson和Gson兩種json框架放入考慮範圍內,與fastjson進行對比。
## 三種json框架的特點
### FastJson
> 速度快
>
> fastjson相對其他JSON庫的特點是快,從2011年fastjson釋出1.1.x版本之後,其效能從未被其他Java實現的JSON庫超越。
>
> 使用廣泛
>
> fastjson在阿里巴巴大規模使用,在數萬臺伺服器上部署,fastjson在業界被廣泛接受。在2012年被開源中國評選為最受歡迎的國產開源軟體之一。
>
> 測試完備
>
> fastjson有非常多的testcase,在1.2.11版本中,testcase超過3321個。每次釋出都會進行迴歸測試,保證質量穩定。
>
> 使用簡單
>
> fastjson的API十分簡潔。
### Jackson
> 容易使用 - jackson API提供了一個高層次外觀,以簡化常用的用例。
>
> 無需建立對映 - API提供了預設的對映大部分物件序列化。
>
> 效能高 - 快速,低記憶體佔用,適合大型物件圖表或系統。
>
> 乾淨的JSON - jackson建立一個乾淨和緊湊的JSON結果,這是讓人很容易閱讀。
>
> 不依賴 - 庫不需要任何其他的庫,除了JDK。
### Gson
> 提供一種機制,使得將Java物件轉換為JSON或相反如使用toString()以及構造器(工廠方法)一樣簡單。
>
> 允許預先存在的不可變的物件轉換為JSON或與之相反。
>
> 允許自定義物件的表現形式
>
> 支援任意複雜的物件
>
> 輸出輕量易讀的JSON
## 效能對比
同事撰寫的效能對比原始碼:
https://github.com/zysrxx/json-comparison
本文不詳細討論效能的差異,畢竟這其中涉及了很多各個框架的實現思路和優化,所以只給出結論:
> 1.序列化單物件效能Fastjson > Jackson > Gson,其中Fastjson和Jackson效能差距很小,Gson效能較差
>
> 2.序列化大物件效能Jackson> Fastjson > Gson ,序列化大Json物件時Jackson> Gson > Fastjson,Jackson序列化大資料時效能優勢明顯
>
> 3.反序列化單物件效能 Fastjson > Jackson > Gson , 效能差距較小
>
> 4.反序列化大物件效能 Fastjson > Jackson > Gson , 效能差距較很小
## 最終選擇方案
- Jackson適用於高效能場景,Gson適用於高安全性場景
- 對於新專案倉庫,不再使用fastjson。對於存量系統,考慮到Json更換成本,由以下幾種方案可選:
- 專案未使用autoType功能,建議直接切換為非fastjson,如果切換成本較大,可以考慮繼續使用fastjson,關閉safemode。
- 業務使用了autoType功能,建議推進廢棄fastjson。
# 替換依賴注意事項
企業專案或者說大型專案的特點:
- 程式碼結構複雜,團隊多人維護。
- 承擔重要線上業務,一旦出現嚴重bug會導致重大事故。
- 如果是老專案,可能缺少文件,不能隨意修改,牽一髮而動全身。
- 專案有很多開發分支,不斷在迭代上線。
所以對於大型專案,想要做到將底層的fastjson遷移到gson是一件複雜且痛苦的事情,其實對於其他依賴的替換,也都一樣。
**我總結了如下幾個在替換專案依賴過程中要特別重視的問題。**
## 謹慎,謹慎,再謹慎
再怎麼謹慎都不為過,如果你要更改的專案是非常重要的業務,那麼一旦犯下錯誤,代價是非常大的。並且,對於業務方和產品團隊來說,沒有新的功能上線,但是系統卻炸了,是一件“無法忍受”的事情。儘管你可能覺得很委屈,因為只有你或者你的團隊知道,雖然業務看上去沒變化,但是程式碼底層已經發生了翻天覆地的變化。
所以,謹慎點!
## 做好開發團隊和測試團隊的溝通
在依賴替換的過程中,需要做好專案的規劃,比如分模組替換,嚴格細分排期。
把前期規劃做好,開發和測試才能有條不紊的進行工作。
開發之間,需要提前溝通好開發注意事項,比如依賴版本問題,防止由多個開發同時修改程式碼,最後發現使用的版本不同,介面用法都不同這種很尷尬,並且要花額外時間處理的事情。
而對於測試,更要事先溝通好。一般來說,測試不會太在意這種對於業務沒有變化的技術專案,因為既不是優化速度,也不是新功能。但其實遷移涉及到了底層,很容易就出現BUG。要讓測試團隊瞭解更換專案依賴,是需要大量的測試時間投入的,成本不亞於新功能,讓他們儘量重視起來。
## 做好迴歸/介面測試
上面說到測試團隊需要投入大量工時,這些工時主要都用在專案功能的整體迴歸上,也就是迴歸測試。
當然,不只是業務迴歸測試,如果有條件的話,要做介面迴歸測試。
**如果公司有介面管理平臺,那麼可以極大提高這種專案測試的效率。**
打個比方,在一個模組修改完成後,在測試環境(或者沙箱環境),部署一個線上版本,部署一個修改後的版本,直接將介面返回資料進行對比。一般來說是Json對比,網上也有很多的Json對比工具:
https://www.sojson.com/
## 考慮遷移前後的效能差異
正如上面描述的Gson和Fastjson效能對比,替換框架需要注意框架之間的效能差異,尤其是對於流量業務,也就是高併發專案,響應時間如果發生很大的變化會引起上下游的注意,導致一些額外的後果。
# 使用Gson替換Fastjson
**這裡總結了兩種json框架常用的方法,貼出詳細的程式碼示例,幫助大家快速的上手Gson,無縫切換!**
## Json反序列化
```
String jsonCase = "[{\"id\":10001,\"date\":1609316794600,\"name\":\"小明\"},{\"id\":10002,\"date\":1609316794600,\"name\":\"小李\"}]";
// fastjson
JSONArray jsonArray = JSON.parseArray(jsonCase);
System.out.println(jsonArray);
System.out.println(jsonArray.getJSONObject(0).getString("name"));
System.out.println(jsonArray.getJSONObject(1).getString("name"));
// 輸出:
// [{"date":1609316794600,"name":"小明","id":10001},{"date":1609316794600,"name":"小李","id":10002}]
// 小明
// 小李
// Gson
JsonArray jsonArrayGson = gson.fromJson(jsonCase, JsonArray.class);
System.out.println(jsonArrayGson);
System.out.println(jsonArrayGson.get(0).getAsJsonObject().get("name").getAsString());
System.out.println(jsonArrayGson.get(1).getAsJsonObject().get("name").getAsString());
// 輸出:
// [{"id":10001,"date":1609316794600,"name":"小明"},{"id":10002,"date":1609316794600,"name":"小李"}]
// 小明
// 小李
```
看得出,兩者區別主要在get各種型別上,Gson呼叫方法有所改變,但是變化不大。
那麼,來看下空物件反序列化會不會出現異常:
```
String jsonObjectEmptyCase = "{}";
// fastjson
JSONObject jsonObjectEmpty = JSON.parseObject(jsonObjectEmptyCase);
System.out.println(jsonObjectEmpty);
System.out.println(jsonObjectEmpty.size());
// 輸出:
// {}
// 0
// Gson
JsonObject jsonObjectGsonEmpty = gson.fromJson(jsonObjectEmptyCase, JsonObject.class);
System.out.println(jsonObjectGsonEmpty);
System.out.println(jsonObjectGsonEmpty.size());
// 輸出:
// {}
// 0
```
沒有異常,開心。
看看空陣列呢,畢竟[]感覺比{}更加容易出錯。
```
String jsonArrayEmptyCase = "[]";
// fastjson
JSONArray jsonArrayEmpty = JSON.parseArray(jsonArrayEmptyCase);
System.out.println(jsonArrayEmpty);
System.out.println(jsonArrayEmpty.size());
// 輸出:
// []
// 0
// Gson
JsonArray jsonArrayGsonEmpty = gson.fromJson(jsonArrayEmptyCase, JsonArray.class);
System.out.println(jsonArrayGsonEmpty);
System.out.println(jsonArrayGsonEmpty.size());
// 輸出:
// []
// 0
```
兩個框架也都沒有問題,完美解析。
## 範型處理
解析泛型是一個非常常用的功能,我們專案中大部分fastjson程式碼就是在解析json和Java Bean。
```
// 實體類
User user = new User();
user.setId(1L);
user.setUserName("馬雲");
// fastjson
List