您好,登錄后才能下訂單哦!
如何進行gson替換fastjson引發的線上問題分析,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
Json 序列化框架存在的安全漏洞一直以來都是程序員們掛在嘴邊調侃的一個話題,尤其是這兩年 fastjson 由于被針對性研究,更是頻頻地的報出漏洞,出個漏洞不要緊,可安全團隊總是用郵件催著線上應用要進行依賴升級,這可就要命了,我相信很多小伙伴也是不勝其苦,考慮了使用其他序列化框架替換 fastjson。這不,最近我們就有一個項目將 fastjson 替換為了 gson,引發了一個線上的問題。分享下這次的經歷,以免大家踩到同樣的坑,在此警示大家,規范千萬條,安全第一條,升級不規范,線上兩行淚。
線上一個非常簡單的邏輯,將對象序列化成 fastjson,再使用 HTTP 請求將字符串發送出去。原本工作的好好的,在將 fastjson 替換為 gson 之后,竟然引發了線上的 OOM。經過內存 dump 分析,發現竟然發送了一個 400 M+ 的報文,由于 HTTP 工具沒有做發送大小的校驗,強行進行了傳輸,直接導致了線上服務整體不可用。
為什么同樣是 JSON 序列化,fastjson 沒出過問題,而換成 gson 之后立馬就暴露了呢?通過分析內存 dump 的數據,發現很多字段的值都是重復的,再結合我們業務數據的特點,一下子定位到了問題 -- gson 序列化重復對象存在嚴重的缺陷。
直接用一個簡單的例子,來說明當時的問題。模擬線上的數據特性,使用 List<Foo>
添加進同一個引用對象
Foo foo = new Foo();
Bar bar = new Bar();
List<Foo> foos = new ArrayList<>();
for(int i=0;i<3;i++){
foos.add(foo);
}
bar.setFoos(foos);
Gson gson = new Gson();
String gsonStr = gson.toJson(bar);
System.out.println(gsonStr);
String fastjsonStr = JSON.toJSONString(bar);
System.out.println(fastjsonStr);
觀察打印結果:
gson:
{"foos":[{"a":"aaaaa"},{"a":"aaaaa"},{"a":"aaaaa"}]}
fastjson:
{"foos":[{"a":"aaaaa"},{"$ref":"$.foos[0]"},{"$ref":"$.foos[0]"}]}
可以發現 gson 處理重復對象,是對每個對象都進行了序列化,而 fastjson 處理重復對象,是將除第一個對象外的其他對象使用引用符號 $ref
進行了標記。
當單個重復對象的數量非常多,以及單個對象的提交較大時,兩種不同的序列化策略會導致一個質變,我們不妨來針對特殊的場景進行下對比。
序列化對象:包含大量的屬性。以模擬線上的業務數據。
重復次數:200。即 List 中包含 200 個同一引用的對象,以模擬線上復雜的對象結構,擴大差異性。
序列化方式:gson、fastjson、Java、Hessian2。額外引入了 Java 和 Hessian2 的對照組,方便我們了解各個序列化框架在這個特殊場景下的表現。
主要觀察各個序列化方式壓縮后的字節大小,因為這關系到網絡傳輸時的大小;次要觀察反序列后 List 中還是不是同一個對象
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Foo foo = new Foo();
Bar bar = new Bar();
List<Foo> foos = new ArrayList<>();
for(int i=0;i<200;i++){
foos.add(foo);
}
bar.setFoos(foos);
// gson
Gson gson = new Gson();
String gsonStr = gson.toJson(bar);
System.out.println(gsonStr.length());
Bar gsonBar = gson.fromJson(fastjsonStr, Bar.class);
System.out.println(gsonBar.getFoos().get(0) == gsonBar.getFoos().get(1));
// fastjson
String fastjsonStr = JSON.toJSONString(bar);
System.out.println(fastjsonStr.length());
Bar fastjsonBar = JSON.parseObject(fastjsonStr, Bar.class);
System.out.println(fastjsonBar.getFoos().get(0) == fastjsonBar.getFoos().get(1));
// java
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(bar);
oos.close();
System.out.println(byteArrayOutputStream.toByteArray().length);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
Bar javaBar = (Bar) ois.readObject();
ois.close();
System.out.println(javaBar.getFoos().get(0) == javaBar.getFoos().get(1));
// hessian2
ByteArrayOutputStream hessian2Baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(hessian2Baos);
hessian2Output.writeObject(bar);
hessian2Output.close();
System.out.println(hessian2Baos.toByteArray().length);
ByteArrayInputStream hessian2Bais = new ByteArrayInputStream(hessian2Baos.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(hessian2Bais);
Bar hessian2Bar = (Bar) hessian2Input.readObject();
hessian2Input.close();
System.out.println(hessian2Bar.getFoos().get(0) == hessian2Bar.getFoos().get(1));
}
}
輸出結果:
gson:
62810
false
fastjson:
4503
true
Java:
1540
true
Hessian2:
686
true
結論分析:由于單個對象序列化后的體積較大,采用引用表示的方式可以很好的縮小體積,可以發現 gson 并沒有采取這種序列化優化策略,導致體積膨脹。甚至一貫不被看好的 Java 序列化都比其優秀的多,而 Hessian2 更是夸張,直接比 gson 優化了 2個數量級。并且反序列化后,gson 并不能將原本是同一引用的對象還原回去,而其他的序列化框架均可以實現這一點。
除了關注序列化之后數據量的大小,各個序列化的吞吐量也是我們關心的一個點。使用基準測試可以精準地測試出各個序列化方式的吞吐量。
@BenchmarkMode({Mode.Throughput})
@State(Scope.Benchmark)
public class MicroBenchmark {
private Bar bar;
@Setup
public void prepare() {
Foo foo = new Foo();
Bar bar = new Bar();
List<Foo> foos = new ArrayList<>();
for(int i=0;i<200;i++){
foos.add(foo);
}
bar.setFoos(foos);
}
Gson gson = new Gson();
@Benchmark
public void gson(){
String gsonStr = gson.toJson(bar);
gson.fromJson(gsonStr, Bar.class);
}
@Benchmark
public void fastjson(){
String fastjsonStr = JSON.toJSONString(bar);
JSON.parseObject(fastjsonStr, Bar.class);
}
@Benchmark
public void java() throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(bar);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
Bar javaBar = (Bar) ois.readObject();
ois.close();
}
@Benchmark
public void hessian2() throws Exception {
ByteArrayOutputStream hessian2Baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(hessian2Baos);
hessian2Output.writeObject(bar);
hessian2Output.close();
ByteArrayInputStream hessian2Bais = new ByteArrayInputStream(hessian2Baos.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(hessian2Bais);
Bar hessian2Bar = (Bar) hessian2Input.readObject();
hessian2Input.close();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MicroBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
吞吐量報告:
Benchmark Mode Cnt Score Error Units
MicroBenchmark.fastjson thrpt 25 6724809.416 ± 1542197.448 ops/s
MicroBenchmark.gson thrpt 25 1508825.440 ± 194148.657 ops/s
MicroBenchmark.hessian2 thrpt 25 758643.567 ± 239754.709 ops/s
MicroBenchmark.java thrpt 25 734624.615 ± 66892.728 ops/s
是不是有點出乎意料,fastjson 竟然獨領風騷,文本類序列化的吞吐量相比二進制序列化的吞吐量要高出一個數量級,分別是每秒百萬級和每秒十萬級的吞吐量。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。