溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何進行gson替換fastjson引發(fā)的線上問題分析

發(fā)布時間:2021-12-14 09:57:07 來源:億速云 閱讀:176 作者:柒染 欄目:大數(shù)據(jù)

如何進行gson替換fastjson引發(fā)的線上問題分析,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。

前言

Json 序列化框架存在的安全漏洞一直以來都是程序員們掛在嘴邊調(diào)侃的一個話題,尤其是這兩年 fastjson 由于被針對性研究,更是頻頻地的報出漏洞,出個漏洞不要緊,可安全團隊總是用郵件催著線上應用要進行依賴升級,這可就要命了,我相信很多小伙伴也是不勝其苦,考慮了使用其他序列化框架替換 fastjson。這不,最近我們就有一個項目將 fastjson 替換為了 gson,引發(fā)了一個線上的問題。分享下這次的經(jīng)歷,以免大家踩到同樣的坑,在此警示大家,規(guī)范千萬條,安全第一條,升級不規(guī)范,線上兩行淚。

 

問題描述

線上一個非常簡單的邏輯,將對象序列化成 fastjson,再使用 HTTP 請求將字符串發(fā)送出去。原本工作的好好的,在將 fastjson 替換為 gson 之后,竟然引發(fā)了線上的 OOM。經(jīng)過內(nèi)存 dump 分析,發(fā)現(xiàn)竟然發(fā)送了一個 400 M+ 的報文,由于 HTTP 工具沒有做發(fā)送大小的校驗,強行進行了傳輸,直接導致了線上服務整體不可用。

 

問題分析

為什么同樣是 JSON 序列化,fastjson 沒出過問題,而換成 gson 之后立馬就暴露了呢?通過分析內(nèi)存 dump 的數(shù)據(jù),發(fā)現(xiàn)很多字段的值都是重復的,再結合我們業(yè)務數(shù)據(jù)的特點,一下子定位到了問題 -- gson 序列化重復對象存在嚴重的缺陷。

直接用一個簡單的例子,來說明當時的問題。模擬線上的數(shù)據(jù)特性,使用 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]"}]}
 

可以發(fā)現(xiàn) gson 處理重復對象,是對每個對象都進行了序列化,而 fastjson 處理重復對象,是將除第一個對象外的其他對象使用引用符號 $ref 進行了標記。

當單個重復對象的數(shù)量非常多,以及單個對象的提交較大時,兩種不同的序列化策略會導致一個質(zhì)變,我們不妨來針對特殊的場景進行下對比。

 

壓縮比測試

  • 序列化對象:包含大量的屬性。以模擬線上的業(yè)務數(shù)據(jù)。

  • 重復次數(shù):200。即 List 中包含 200 個同一引用的對象,以模擬線上復雜的對象結構,擴大差異性。

  • 序列化方式:gson、fastjson、Java、Hessian2。額外引入了 Java 和 Hessian2 的對照組,方便我們了解各個序列化框架在這個特殊場景下的表現(xiàn)。

  • 主要觀察各個序列化方式壓縮后的字節(jié)大小,因為這關系到網(wǎng)絡傳輸時的大小;次要觀察反序列后 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
 

結論分析:由于單個對象序列化后的體積較大,采用引用表示的方式可以很好的縮小體積,可以發(fā)現(xiàn) gson 并沒有采取這種序列化優(yōu)化策略,導致體積膨脹。甚至一貫不被看好的 Java 序列化都比其優(yōu)秀的多,而 Hessian2 更是夸張,直接比 gson 優(yōu)化了 2個數(shù)量級。并且反序列化后,gson 并不能將原本是同一引用的對象還原回去,而其他的序列化框架均可以實現(xiàn)這一點。

 

吞吐量測試

除了關注序列化之后數(shù)據(jù)量的大小,各個序列化的吞吐量也是我們關心的一個點。使用基準測試可以精準地測試出各個序列化方式的吞吐量。

@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 竟然獨領風騷,文本類序列化的吞吐量相比二進制序列化的吞吐量要高出一個數(shù)量級,分別是每秒百萬級和每秒十萬級的吞吐量。

 

整體測試結論

  • fastjson 序列化過后帶有 $ 的引用標記也能夠被 gson 正確的反序列化,但筆者并沒有找到讓 gson 序列化時轉(zhuǎn)換成引用的配置
  • fastjson、hessian、java 均支持循環(huán)引用的解析;gson 不支持
  • fastjson 可以設置 DisableCircularReferenceDetect,關閉循環(huán)引用和重復引用的檢測
  • gson 反序列化之前的同一個引用的對象,在經(jīng)歷了序列化再反序列化回來之后,不會被認為是同一個對象,可能會導致內(nèi)存對象數(shù)量的膨脹;而 fastjson、java、hessian2 等序列化方式由于記錄的是引用標記,不存在該問題
  • 以筆者的測試 case 為例,hessian2 具有非常強大的序列化壓縮比,適合大報文序列化后供網(wǎng)絡傳輸?shù)膱鼍笆褂?/section>
  • 以筆者的測試 case 為例,fastjson 具有非常高的吞吐量,對得起它的 fast,適合需要高吞吐的場景使用
  • 序列化還需要考慮到是否支持循環(huán)引用,是否支持循環(huán)對象優(yōu)化,是否支持枚舉類型、集合、數(shù)組、子類、多態(tài)、內(nèi)部類、泛型等綜合場景,以及是否支持可視化等比較的場景,增刪字段后的兼容性等等特性。綜合來看,筆者比較推薦 hessian2 和 fastjson 兩種序列化方式
    

看完上述內(nèi)容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業(yè)資訊頻道,感謝您對億速云的支持。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI