前言
本篇文章是我這一個多月來幫助組內廢棄fastjson框架的總結,咱們將大部分Java倉庫從fastjson遷移至了Gson。java
這麼作的主要的緣由是公司受夠了fastjson頻繁的安全漏洞問題,每一次出現漏洞都要推一次全公司的fastjson強制版本升級,很令公司頭疼。git
文章的前半部分,我會簡單分析各類json解析框架的優劣,並給出企業級項目遷移json框架的幾種解決方案。github
在文章的後半部分,我會結合這一個月的經驗,總結下Gson的使用問題,以及fastjson遷移到Gson踩過的深坑。web
文章目錄:spring
-
爲什麼要放棄fastjson? -
fastjson替代方案 -
三種json框架的特色 -
性能對比 -
最終選擇方案 -
替換依賴時的注意事項 -
謹慎,謹慎,再謹慎 -
作好開發團隊和測試團隊的溝通 -
作好迴歸/接口測試 -
考慮遷移先後的性能差別 -
使用Gson替換fastjson -
Json反序列化 -
範型處理 -
List/Map寫入 -
駝峯與下劃線轉換 -
遷移常見問題踩坑 -
Date序列化方式不一樣 -
SpringBoot異常 -
Swagger異常 -
@Mapping JsonObject做爲入參異常
注意:是否使用fastjson是近年來一個爭議性很大的話題,本文無心討論框架選型的對錯,只關注遷移這件事中遇到的問題進行反思和思考。你們若是有想發表的見解,能夠在評論區 理 性 討論。json
本文閱讀大概須要: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<User> userListResultFastjson = JSONArray.parseArray(JSON.toJSONString(userList), User.class);
List<User> userListResultFastjson2 = JSON.parseObject(JSON.toJSONString(userList), new TypeReference<List<User>>(){});
System.out.println(userListResultFastjson);
System.out.println("userListResultFastjson2" + userListResultFastjson2);
// 輸出:
// userListResultFastjson[User [Hash = 483422889, id=1, userName=馬雲], null]
// userListResultFastjson2[User [Hash = 488970385, id=1, userName=馬雲], null]
// Gson
List<User> userListResultTrue = gson.fromJson(gson.toJson(userList), new TypeToken<List<User>>(){}.getType());
System.out.println("userListResultGson" + userListResultGson);
// 輸出:
// userListResultGson[User [Hash = 1435804085, id=1, userName=馬雲], null]
能夠看出,Gson也能支持泛型。
List/Map寫入
這一點fastjson和Gson有區別,Gson不支持直接將List寫入value,而fastjson支持。
因此Gson只能將List解析後,寫入value中,詳見以下代碼:
// 實體類
User user = new User();
user.setId(1L);
user.setUserName("馬雲");
// fastjson
JSONObject jsonObject1 = new JSONObject();
jsonObject1.put("user", user);
jsonObject1.put("userList", userList);
System.out.println(jsonObject1);
// 輸出:
// {"userList":[{"id":1,"userName":"馬雲"},null],"user":{"id":1,"userName":"馬雲"}}
// Gson
JsonObject jsonObject = new JsonObject();
jsonObject.add("user", gson.toJsonTree(user));
System.out.println(jsonObject);
// 輸出:
// {"user":{"id":1,"userName":"馬雲"},"userList":[{"id":1,"userName":"馬雲"},null]}
如此一來,Gson看起來就沒有fastjson方便,由於放入List是以gson.toJsonTree(user)
的形式放入的。這樣就不能先入對象,在後面修改該對象了。(有些同窗比較習慣先放入對象,再修改對象,這樣的代碼就得改動)
駝峯與下劃線轉換
駝峯轉換下劃線依靠的是修改Gson的序列化模式,修改成LOWER_CASE_WITH_UNDERSCORES
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
Gson gsonUnderScore = gsonBuilder.create();
System.out.println(gsonUnderScore.toJson(user));
// 輸出:
// {"id":1,"user_name":"馬雲"}
常見問題排雷
下面整理了咱們在公司項目遷移Gson過程當中,踩過的坑,這些坑如今寫起來感受沒什麼技術含量。可是這纔是我寫這篇文章的初衷,幫助你們把這些很難發現的坑避開。
這些問題有的是在測試進行迴歸測試的時候發現的,有的是在自測的時候發現的,有的是在上線後發現的,好比Swagger掛了這種不會去測到的問題。
Date序列化方式不一樣
不知道你們想過一個問題沒有,若是你的項目裏有緩存系統,使用fastjson寫入的緩存,在你切換Gson後,須要用Gson解析出來。因此就必定要保證兩個框架解析邏輯是相同的,可是,顯然這個願望是美好的。
在測試過程當中,發現了Date類型,在兩個框架裏解析是不一樣的方式。
-
fastjson:Date直接解析爲Unix -
Gson:直接序列化爲標準格式Date

致使了Gson在反序列化這個json的時候,直接報錯,沒法轉換爲Date。
解決方案:
新建一個專門用於解析Date類型的類:
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.Date;
public class MyDateTypeAdapter extends TypeAdapter<Date> {
@Override
public void write(JsonWriter out, Date value) throws IOException {
if (value == null) {
out.nullValue();
} else {
out.value(value.getTime());
}
}
@Override
public Date read(JsonReader in) throws IOException {
if (in != null) {
return new Date(in.nextLong());
} else {
return null;
}
}
}
接着,在建立Gson時,把他放入做爲Date的專用處理類:
Gson gson = new GsonBuilder().registerTypeAdapter(Date.class,new MyDateTypeAdapter()).create();
這樣就可讓Gson將Date處理爲Unix。
固然,這只是爲了兼容老的緩存,若是你以爲你的倉庫沒有這方面的顧慮,能夠忽略這個問題。
SpringBoot異常
切換到Gson後,使用SpringBoot搭建的Web項目的接口直接請求不了了。報錯相似:
org.springframework.http.converter.HttpMessageNotWritableException
由於SpringBoot默認的Mapper是Jackson解析,咱們切換爲了Gson做爲返回對象後,Jackson解析不了了。
解決方案:
application.properties裏面添加:
#Preferred JSON mapper to use for HTTP message conversion
spring.mvc.converters.preferred-json-mapper=gson
Swagger異常
這個問題和上面的SpringBoot異常相似,是由於在SpringBoot中引入了Gson,致使 swagger 沒法解析 json。

採用相似下文的解決方案(添加Gson適配器):
http://yuyublog.top/2018/09/03/springboot%E5%BC%95%E5%85%A5swagger/
-
GsonSwaggerConfig.java
@Configuration
public class GsonSwaggerConfig {
//設置swagger支持gson
@Bean
public IGsonHttpMessageConverter IGsonHttpMessageConverter() {
return new IGsonHttpMessageConverter();
}
}
-
IGsonHttpMessageConverter.java
public class IGsonHttpMessageConverter extends GsonHttpMessageConverter {
public IGsonHttpMessageConverter() {
//自定義Gson適配器
super.setGson(new GsonBuilder()
.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter())
.serializeNulls()//空值也參與序列化
.create());
}
}
-
SpringfoxJsonToGsonAdapter.java
public class SpringfoxJsonToGsonAdapter implements JsonSerializer<Json> {
@Override
public JsonElement serialize(Json json, Type type, JsonSerializationContext jsonSerializationContext) {
return new JsonParser().parse(json.value());
}
}
@Mapping JsonObject做爲入參異常
有時候,咱們會在入參使用相似:
public ResponseResult<String> submitAudit(@RequestBody JsonObject jsonObject) {}
若是使用這種代碼,其實就是使用Gson來解析json字符串。可是這種寫法的風險是很高的,日常請你們儘可能避免使用JsonObject直接接受參數。
在Gson中,JsonObject如果有數字字段,會統一序列化爲double,也就是會把count = 0
這種序列化成count = 0.0
。
爲什麼會有這種狀況?簡單的來講就是Gson在將json解析爲Object類型時,會默認將數字類型使用double轉換。
若是Json對應的是Object類型,最終會解析爲Map<String, Object>類型;其中Object類型跟Json中具體的值有關,好比雙引號的""值翻譯爲STRING。咱們能夠看下數值類型(NUMBER)所有轉換爲了Double類型,因此就有了咱們以前的問題,整型數據被翻譯爲了Double類型,好比30變爲了30.0。
能夠看下Gson的ObjectTypeAdaptor類,它繼承了Gson的TypeAdaptor抽象類:

具體的源碼分析和原理闡述,你們能夠看這篇拓展閱讀:
https://www.jianshu.com/p/eafce9689e7d
解決方案:
第一個方案:把入參用實體類接收,不要使用JsonObject
第二個方案:與上面的解決Date類型問題相似,本身定義一個Adaptor,來接受數字,而且處理。這種想法我以爲可行可是難度較大,可能會影響到別的類型的解析,須要在設計適配器的時候格外注意。
總結
這篇文章主要是爲了那些須要將項目遷移到Gson框架的同窗們準備的。
通常來講,我的小項目,是不須要費這麼大精力去作遷移,因此這篇文章可能目標人羣比較狹窄。
但文章中也提到了很多通用問題的解決思路,好比怎麼評估遷移框架的必要性。其中須要考慮到框架兼容性,二者性能差別,遷移耗費的工時等不少問題。
但願文章對你有所幫助。
參考
《如何從Fastjson遷移到Gson》
https://juejin.im/post/6844904089281626120
《FastJson遷移至Jackson》此文做者本身封裝了工具類來完成遷移
https://mxcall.github.io/posts/%E5%B7%A5%E4%BD%9C/%E7%A8%8B%E5%BA%8F%E5%91%98/javaSE/FastJson%E8%BF%81%E7%A7%BB%E8%87%B3Jackson/
《你真的會用Gson嗎?Gson使用指南》
https://www.jianshu.com/p/e740196225a4
json性能對比
https://github.com/zysrxx/json-comparison/tree/master/src/main/java/json/comparison
fastjson官方文檔
https://github.com/alibaba/fastjson/wiki
易百教程
https://www.yiibai.com/jackson
本文分享自微信公衆號 - Java中文社羣(javacn666)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。