前言
這個週末被幾個技術博主的同一篇公眾號文章 fastjson又被發現漏洞,這次危害可導致服務癱瘓! 刷屏,離之前漏洞事件沒多久,fastjson 又出現嚴重 Bug。目前專案中不少使用了 fastjson 做物件與JSON資料的轉換,又需要更新版本重新部署,可以說是費時費力。與此同時,也帶給我新的思考,面對大量功能強大的開源庫,我們不能盲目地引入到專案之中,眾多開源框架中某個不穩定因素就足以讓一個專案遭受滅頂之災。趁著週末,在家學習下同樣具備JSON與物件轉換功能的優秀開源框架 Gson,並且打算將今後專案使用 fastjson 的地方逐漸換成使用 Gson,記錄下學習總結的內容,希望對小夥伴也有所幫助。
本文所涉及所有程式碼片段均在下面倉庫中,感興趣的小夥伴歡迎參考學習:
版本支援:
- JDK 8
- Gson 2.8.5
- JUnit 5.5.1
- 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 字串轉化成物件的過程稱為反序列化。
Gson 基本使用
使用 Gson 框架進行序列化與反序列操作,都離不開 com.google.gson.Gson
物件,它也是 Gson 框架的關鍵物件,提供的公共 API 具備了多種序列化和反序列方式。
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;
}
}
}
複製程式碼
執行該測試用例,在控制檯可以看到如下日誌輸出:
從結果可以看出,預設的 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<T> 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.name").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<List<String>>() {}.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<User> 物件,示例程式碼如下:
@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<User> , 然後在 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
這個註解只能用在欄位上,作用就是註明對應的欄位是否將在序列化或者反序列化時暴露出來,有兩個屬性 serialize
和 deserialize
,預設都為 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);
// {"id":1,"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 指定了成員欄位被序列化和反序列化時所採用的名稱下面是具體使用方式:
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 資料感到頭疼的小夥伴有所幫助。