Fastjson致命缺陷

前言

這個週末被幾個技術博主的同一篇公衆號文章 fastjson又被發現漏洞,此次危害可致使服務癱瘓! 刷屏,離以前的漏洞事件沒多久,FastJson 又出現嚴重 Bug。目前項目中很多使用了 FastJson 作對象與JSON數據的轉換,又須要更新版本從新部署,能夠說是費時費力。與此同時,也帶給我新的思考,面對大量功能強大的開源庫,咱們不能盲目地引入到項目之中,衆多開源框架中任一個不穩定因素就足以讓一個項目遭受滅頂之災。趁着週末,在家學習下一樣具有對象JSON相互轉換功能的優秀開源框架 Gson,而且打算將從此項目使用 FastJson 的地方逐漸換成使用 Gson,記錄下學習總結的內容,但願對小夥伴也有所幫助。java

本文所涉及全部代碼片斷均在下面倉庫中,感興趣的小夥伴歡迎參考學習:git

https://github.com/wrcj12138aaa/gson-actionsgithub

版本支持:面試

- JDK 8spring

- gson 2.8.5編程

- junit 5.5.1json

- Lomok 1.18.8設計模式

Gson 簡介

在正式介紹 Gson 以前,咱們能夠先從官方的wiki看下 Gson 的描述,瞭解它是什麼?數組

Gson is a Java library that can be used to convert Java Objects into their JSON representation. It can also be used to convert a JSON string to an equivalent Java object。緩存

從描述能夠看出,Gson 是用於將 Java 對象與 JSON格式字符串數據相互轉換的 Java 庫。它起初在Google 內部普遍使用在 Android 平臺 和 Java 服務端上。2008 年開源以後,成爲了谷歌又一個被普遍使用的開源框架,截止目前(2019.09.08) 在GitHub 上已有1W6 多星,相同做用的類庫還有 Spring Framework 中集成的 Jackson,以及阿里開源的 FastJson等。

在使用方面,Gson 提供簡易的API fromJson/toJson 來實現 Java 與 JSON 之間的轉換,而且能生成緊湊,可讀的 JSON 字符串輸出,還支持複雜對象轉換和豐富的自定義表示,足以知足在平常開發中咱們絕大部分的 JSON 數據處理需求。

咱們一般將對象與JSON字符串間的轉換稱之爲序列化和反序列化(Serialization/Deserialization)。將對象轉化成 JSON字符串的過程稱爲序列化,將JSON 字符串轉化成對象的過程稱爲反序列化。img

Gson 基本使用

使用 Gson 框架進行序列化與反序列操做,都離不開 com.google.gson.Gson 對象,它也是 Gson 框架的關鍵對象,提供的公共 API 具有了多種序列化和反序列方式。

img

Gson 對象的建立主要有兩種方式:

  • 使用 new 關鍵字直接建立:Gson gson = new Gson()
  • 由 GsonBuilder 對象構建:Gson gson = new GsonBuilder().create()

一般狀況下,上面兩種方式建立的 Gson 對象在進行序列化與反序列操做時行爲都是同樣的,可是第二種方式構建 Gson 對象時,容許進行額外的行爲定製,好比格式化 JSON 字符串的輸出內容,是否序列化 null 值等等。

Java 序列化

簡單對象的序列化

咱們能夠經過下面的例子來看下經過上述兩種方式序列化 Java 對象的不一樣效果:

public class ResultTest {
    @Test
    void test_serialization() {
        Gson gson = new Gson();
        Result result = new Result(200, "成功", null);
        String json = gson.toJson(result);
        System.out.println("json is " + json);

        Gson buildedGson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
        String buildedJson = buildedGson.toJson(result);
        System.out.println("buildedJson is " + buildedJson);
    }

    class Result {
        private int code;
        private String message;
        private Object data;

        public Result(int code, String message, Object data) {
            this.code = code;
            this.message = message;
            this.data = data;
        }
    }
}複製代碼

運行該測試用例,在控制檯能夠看到以下日誌輸出:

img

從結果能夠看出,默認的 Gson 對象行爲序列化對象時會將 null 值的字段忽略,而執行 com.google.gson.GsonBuilder#serializeNulls 方法後將容許 Gson 對象序列化 null 字段;而且正常序列化後的 JSON 字符串是緊湊格式,節省字符串內存,使用 com.google.gson.GsonBuilder#setPrettyPrinting 方法以後最終輸出的 JSON 字符串是更易讀的格式。固然除了這兩個方法,GsonBuilder 還提供了許多定製序列化和反序列化行爲的API,咱們將後面的內容進一步講解。

JosnObject 生成 JSON

除了上述將自定義類的對象轉換成 JSON 的方式以外,還可使用 Gson 框架提供的 JsonObject 構建普通對象,而後使用 toJson 方法生成 JSON 字符串,在原測試類中補充下方測試類,並運行查看效果以下

@Test
void test_jsonObject_serialization() {
  Gson gson = new Gson();
  JsonObject jsonObject = new JsonObject();
  jsonObject.addProperty("code", 400);
  jsonObject.addProperty("message", "參數錯誤");
  String toJson = gson.toJson(jsonObject);
  String exceptedJson = "{\"code\":400,\"message\":\"參數錯誤\"}";
  Assertions.assertEquals(exceptedJson, toJson); //true
}複製代碼

JsonObject 使用 addProperty(property,value) 方法只能用來添加 String,Number,Boolean,Character這四類數據, 由於內部是調用 com.google.gson.JsonObject#add, 並將 value 封裝成了 JsonPrimitive 對象,而後保存到了內部自定義的 LinkedTreeMap 集合變量 members 中;若是須要在 JsonObject 對象上添加其餘對象時,就須要直接使用 add(String property, JsonElement value) 方法添加一個 JsonElement 對象。這裏的 JsonElement 是一個抽象類,JsonObject 和 JsonPrimitive 都繼承了JsonElement,因此咱們最終經過新的 JsonObject 對象來做爲原 JsonObject 上的屬性對象:

Gson gson = new Gson();
JsonObject jsonObject = new JsonObject();
//...
JsonObject nestJsonObject = new JsonObject();
nestJsonObject.addProperty("username", "one");
nestJsonObject.addProperty("score", 99);
jsonObject.add("data", nestJsonObject);
String toJson2 = gson.toJson(jsonObject);
System.out.println(toJson2);
// {"code":400,"message":"參數錯誤","data":{"username":"one","score":99}}複製代碼

JSON 反序列化

簡單對象的反序列化

如今咱們再來看下 JSON 反序列化成 Java 對象用法,這裏主要使用方法是 com.google.gson.Gson#fromJson,它最基礎的用法就是 fromJson(String json, Class classOfT) ,嘗試將 JSON 字符串轉爲指定 Class 的對象,若是轉換失敗,就會拋出 JsonSyntaxException 異常。咱們能夠在原來代碼上新增一個測試用例,運行看下效果:

@Test
void test_deserialization() {
    String json = "{\"code\":400,\"message\":\"參數錯誤\"}";
    Result result = new Gson().fromJson(json, Result.class);
    Assertions.assertEquals(400, result.code); // true
    Assertions.assertEquals("參數錯誤", result.message); // true
}複製代碼

反序列化 Map

除了將JSON 字符串序列化爲自定義的Java 對象以外,咱們該能夠轉爲 Map 集合,Gson 提供了對 Map 集合的轉換,使用起來也十分簡單:

@Test
void test_map() {
    String jsonString = "{'employee.name':'one','employee.salary':10}";
    Gson gson = new Gson();
    Map map = gson.fromJson(jsonString, Map.class);
    assertEquals(2, map.size());
        assertEquals("one", map.get("employee.name"));
    assertEquals(Double.class, map.get("employee.salary").getClass());
}複製代碼

須要注意的是轉換後的 Map 對象真實類型並非咱們常常用的 HashMap,而是 Gson 自定義集合LinkedTreeMap ,它實現Map 接口了,存儲鍵值對,在新增和刪除上實現上進行了優化,而且將存儲鍵值對的順序做爲遍歷順序,也就是先存入的先被遍歷到。除此以外,JSON 字符串裏的數值型數據都會轉轉換爲 Double 類型,而 true/false 數據被會被轉換成 Boolean 類型,具體判斷依據能夠參考 com.google.gson.internal.bind.ObjectTypeAdapter#read 方法實現。

JSON 與 Array,List 轉換

JSON 轉換 Array

當咱們正對 JSON 數據進行數組轉換時,相似普通對象轉換的方式便可,toJson 方法直接使用轉爲 JSON 數據;使用fromJson 指定數組類型轉換爲對應類型的數組。

@Test
void test_array() {
  Gson gson = new Gson();
  int[] ints = {1, 2, 3, 4, 5};
  String[] strings = {"abc", "def", "ghi"};
  String s = gson.toJson(ints);// [1,2,3,4,5]
  assertEquals("[1,2,3,4,5]", s); // true

  String s1 = gson.toJson(strings);// ["abc", "def", "ghi"]
  assertEquals("[\"abc\",\"def\",\"ghi\"]", s1);
  String[] strings1 = gson.fromJson(s1, String[].class);
  assertEquals(strings.length, strings1.length); // true
  assertEquals(strings[0], strings1[0]); // true

  int[] ints2 = gson.fromJson("[1,2,3,4,5]", int[].class);
  assertEquals(1, ints2[0]); // true
  assertEquals(5, ints2[4]); // true
}複製代碼

JSON 轉換 List

要將 List 數據轉換爲 JSON數據,使用 Gson 的方式與處理 Array 數據同樣;而將JSON 數據轉爲 List 對象的操做略有不一樣,要將一個 JSON 數組數據轉則換爲一個自定義類的List 時,咱們按照原來的寫法以下:

@Test
public void givenJsonString_whenIncorrectDeserializing() {
    Gson gson = new Gson();
    String inputString = "[{\"id\":1,\"name\":\"one\"},{\"id\":2,\"name\":\"two\"}]";
    List<Person> outputList = gson.fromJson(inputString, List.class);
        outputList.get(0).getId();
}複製代碼

可是不幸的是,運行這段代碼後會拋出 ClassCastException 異常,具體描述以下:

java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.one.learn.Person
...複製代碼

從上述描述中咱們能夠知道執行 fromJson 以後,反序列化後獲得的 List 元素類型爲 LinkedTreeMap,而不是 Person,因此以 Person 對象方式訪問 id 屬性時就會拋出 ClassCastException 異常。那又該如何處理呢, 咱們須要使用 Gson 的 另一個 fromJson 方法:fromJson(String json, Type typeOfT) ,先看下使用方式

@Test
public void givenJsonString_whenCorrectDeserializing_() {
  Gson gson = new Gson();
  String inputString = "[{\"id\":1,\"name\":\"one\"},{\"id\":2,\"name\":\"two\"}]";
  Type type = new TypeToken<List<Person>>(){}.getType();
  List<Person> outputList = gson.fromJson(inputString, type);
  int id = outputList.get(0).getId();
  assertEquals(1, id); // true
  assertEquals("one", outputList.get(0).getName()); // true
}複製代碼

這個方法中的 Type 對象經過 TypeToken 對象的 getType 方法獲取到,是 TypeToken 對象所關聯的泛型類型。而這裏 TypeToken 是 Gson 爲了支持泛型而引入的類,來解決 Java 沒法提供泛型類型表示的問題,因爲 TypeToken 的構造方法是protected修飾的,沒法直接構造,使用就須要寫成new TypeToken >() {}.getType() 形式。

Gson 進階用法

接觸了 Gson 基本的使用以後,咱們接着進一步學習 Gson 的其餘用法。

泛型對象的反序列化

上節內容簡單接觸了 Gson 對泛型的支持,接下來用代碼來展現下它的強大之處,首先咱們將上文的 Result 類調整下接受泛型參數:

class Result<T> {
  private int code;
  private String message;
  private T data;

  public Result(int code, String message, T data) {
    this.code = code;
    this.message = message;
    this.data = data;
  }
}複製代碼

而後對一個有內嵌對象的 JSON字符串進行解析成 Result 對象,示例代碼以下:

@Test
void test_genric_object() {
  String json = "{\"code\":200,\"message\":\"操做成功\",\"data\":{\"username\": \"one\",\"avater\": \"image.jpg\"" +
    "}}";
  Type type = new TypeToken<Result<User>>(){}.getType();
  Result<User> result = new Gson().fromJson(json, type);
  Assertions.assertEquals(200, result.code);
  Assertions.assertEquals("one", result.data.getUsername());
  Assertions.assertEquals("image.jpg", result.data.getAvater());
}

class User {
  private String username;
  private String avater;

  public String getUsername() {
    return username;
  }

  public String getAvater() {
    return avater;
  }
}複製代碼

利用 TypeToken 對象獲取具體泛型類型 Result , 而後在 fromJson 方法中傳入就會根據對應類型的執行反序列化操做。

自定義序列化

若是咱們要對Java 對象的某些字段進行特殊處理,好比隱藏某些字段的序列化,對字段的數據格式化處理等,咱們能夠經過實現 JsonSerializer 接口,對序列化邏輯進行自定義。例如,咱們須要對 Date 類型屬性進行特定格式的處理,能夠聲明 DateSerializer 類實現以下:

class DateSerializer implements JsonSerializer<Date> {
    SimpleDateFormat dateTime = new SimpleDateFormat("yyyy-MM-dd");

    @Override
    public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
        return new JsonPrimitive(dateTime.format(src));
    }
}複製代碼

而後在構建 Gson 對象前,利用 GsonBuilder 將 DateSerializer 實例進行註冊,使用方式以下:

Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new DateSerializer()).create();
複製代碼

這樣一來,一旦遇到要序列化 Date 類型的字段時,都會經過自定義的 serialize 方法將日期以 yyyy-MM-dd 格式進行輸出,以下方的示例代碼:

@Test
void test_dateSerializer() {
  MyObject myObject = new MyObject(new Date(), "one");
  Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new DateSerializer()).create();
  String json = gson.toJson(myObject);
  String exceptedJson = "{\"date\":\"2019-09-08\",\"name\":\"one\"}";
  Assertions.assertEquals(exceptedJson, json); // true
}

class MyObject {
    private Date date;
    private String name;

    public MyObject(Date date, String name) {
        this.date = date;
        this.name = name;
    }

    public MyObject() {
    }
}複製代碼

自定義反序列化

與自定義序列化實現方式相似,想要自定義反序列化邏輯,就須要一樣要實現一個叫 JsonDeserializer 的接口,進行自定義反序列化邏輯的實現。好比如今有個 JSON 字符串內容爲 {"CODE": 400, "MESSAGE": "參數錯誤"},須要被反序列化爲前文提到的 Result 對象,因爲字段名不同,爲了實現對應的轉換,就須要自定義 ResultDeserializer 類,具體實現以下:

class ResultDeserializer implements JsonDeserializer<Result> {
    @Override
    public Result deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        JsonObject object = json.getAsJsonObject();
        Result<Object> result = new Result<>(object.getAsJsonPrimitive("CODE").getAsInt(),object.getAsJsonPrimitive("MESSAGE").getAsString(), null);
        return result;
    }
}複製代碼

接下來就是利用 GsonBuilder 註冊 ResultDeserializer 實例,生成對應的 Gson 對象,用於反序列化操做時生效:

@Test
void test_resultDeserializer() {
    //language=JSON
        String json = "{\"CODE\": 400,\"MESSAGE\": \"參數錯誤\"}";
    Gson gson = new GsonBuilder().registerTypeAdapter(Result.class, new ResultDeserializer()) .create();
    Result result = gson.fromJson(json, Result.class);
    Assertions.assertEquals(400, result.code); // true
    Assertions.assertEquals("參數錯誤", result.message); // true
}複製代碼

Gson 經常使用註解

Gson 除了提供一些 API 供開發者使用以外,還有一些具備特性的註解可使用,接下來就介紹在 Gson 中最經常使用的註解。

@Expose

這個註解只能用在字段上,做用就是註明對應的字段是否將在序列化或者反序列化時暴露出來,有兩個屬性 serializedeserialize ,默認都爲 true。當給一個字段加上 註解@Expose(serialize = true, deserialize = false),則表示了該字段盡在序列化時可見,在反序列化時會忽略賦值。須要額外注意的一點是,@Expose 註解只有在用 GsonBuilder 方式構建 Gson 時有效,而且構建前必須調用 excludeFieldsWithoutExposeAnnotation 方法,不然解析時對聲明註解的字段沒有任何效果,下面是具體的使用示例:

@Test
void test_expose() {
    MySubClass subclass = new MySubClass(42L, "the answer", "Verbose field not to serialize");
    MyClass source = new MyClass(1L, "foo", "bar", subclass);
    Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
    String s = gson.toJson(source);
    System.out.println(s);
        // {"name":"foo","subclass":{"id":42,"description":"the answer","otherVerboseInfo":"Verbose field not to serialize"}}
}

@Data
@AllArgsConstructor
class MyClass {
    private long id;
    @Expose(serialize = false, deserialize = true)
    private String name;
    private transient String other;
    @Expose
    private MySubClass subclass;
}

@Data
@AllArgsConstructor
class MySubClass {
    @Expose
    private long id;
    @Expose
    private String description;
    @Expose
    private String otherVerboseInfo;
}複製代碼

在 Gson 中 transient 關鍵字修飾的字段默認不會被序列化和反序列化,這個行爲是與 Java 原生的序列化和反序列化操做一致的。

@Since

該註解用於標記對應字段或者類型的版本,讓 Gson 能夠指定版本號進行序列化和反序列化操做。當Web服務上的 JSON 數據對應的實體類存在多個版本的字段時,這個註解就十分有用。

一樣地,該註解只針對使用 GsonBuilder 方式構建的 Gson 對象,而且使用 setVersion 方法指明版本號時有效,設置後只會解析對象中對應版本的字段,下面爲具體示例:

public class VersioningSupportTest {
    @Test
    void test() {
        VersionedClass versionedObject = new VersionedClass();
        Gson gson = new GsonBuilder().setVersion(1.0).create();
        String jsonOutput = gson.toJson(versionedObject);
        System.out.println(jsonOutput); // {"newField":"new","field":"old"}
    }
}

class VersionedClass {
    @Since(1.1)
    private final String newerField;
    @Since(1.0)
    private final String newField;
    private final String field;

    public VersionedClass() {
        this.newerField = "newer";
        this.newField = "new";
        this.field = "old";
    }
}複製代碼

@SerializedName

這個註解使用起來比較簡單,也頗有用。@SerializedName 指定了成員字段被序列化和反序列化時所採用的名稱,便於咱們調整JSON數據與對應實體類字段名不一致的問題,下面是具體使用方式:

public class JSONFieldNamingSupportTest {
    private class SomeObject {
        @SerializedName("custom_naming")
        private final String someField;
        private final String someOtherField;

        public SomeObject(String a, String b) {
            this.someField = a;
            this.someOtherField = b;
        }
    }

    @Test
    void test() {
        SomeObject someObject = new SomeObject("first", "second");
        String jsonRepresentation = gson.toJson(someObject);
        System.out.println(jsonRepresentation);
          // {"custom_naming":"first","someOtherField":"second"}
        SomeObject someObject1 = gson.fromJson(jsonRepresentation, SomeObject.class);
        System.out.println(someObject1);
          // SomeObject{someField='first', someOtherField='second'}
    }
}複製代碼

@JsonAdapter

不一樣於上面的註解,@JsonAdapter 只做用於類上,主要做用就是代替GsonBuilder.registerTypeAdapter 方法的執行,直接經過 @JsonAdapter(aClass.class) 方式指定 JsonDeserializer 對象或者 JsonSerializer 對象,能夠起到相同的想過,而且優先級比GsonBuilder.registerTypeAdapter的優先級更高,因爲只是將 registerTypeAdapter方法執行簡化成了註解方法,這裏就再也不演示,直接在前文自定義反序列化一節的 Result 類上使用就能夠看到效果。

結語

本文主要學習總結了 Gson 框架的序列化和反序列操做使用方式,以及介紹了 Gson 多種特性用法,但願對處理 JSON 數據感到頭疼的小夥伴有所幫助。

img

參考資料

  • https://github.com/google/gson/blob/master/UserGuide.md
  • https://www.jianshu.com/p/e740196225a4
  • https://juejin.im/post/5aad29f8518825558453c6c9
  • https://www.baeldung.com/gson-deserialization-guide
  • https://www.baeldung.com/gson-string-to-jsonobject

img

spring-boot-starter-grpc 不一樣序列化方式性能測試及選型

Spring Boot 2 集成log4j2日誌框架

Java面試通關要點彙總集之核心篇參考答案

Java面試通關要點彙總集之框架篇參考答案

Spring Security 實戰乾貨:如何保護用戶密碼

Spring Boot RabbitMQ - 優先級隊列

可見性有序性,Happens-before來搞定

Java單元測試之JUnit 5快速上手

高薪必備的一些Spring Boot高級面試題

Spring Boot 2.x 中的 Actuator

下一代應用監控指標採集器Prometheus核心介紹

Spring中@Async註解實現「方法」的異步調用

學併發編程,透徹理解這三個核心是關鍵

緩存抽象層Spring cache實戰操做

簡述設計模式原則

併發Bug之源有三,請睜大眼睛看清它們

責任鏈設計模式講解

Java Web 面試中關於Spring MVC的必問題,不收藏血虧!

Micronaut 微服務中使用 Kafka

微服務網關除了zuul、spring cloud gateway還有更出色的

RSocket——Http協議的替代者

java中的數字以及如何判斷字符串是否是數字

學好Spring Security 和Apache Shiro你須要具有這些條件

img

本文由博客一文多發平臺 OpenWrite 發佈!