轉載來源:https://xesam.github.io/java/2017/02/17/Java-Gson%E8%A7%A3%E6%9E%90%E5%A4%8D%E6%9D%82%E6%95%B0%E6%8D%AE.html
本文主要關注所解析的 JSON 對象與已定義的 java 對象結構不匹配的情況,解決方案就是使用 JsonDeserializer 來自定義從 JSON 對象到 Java 對象的映射。
一個簡單的例子
有如下 JSON 對象,表示一本書的基本信息,本書有兩個作者。
{
'title': 'Java Puzzlers: Traps, Pitfalls, and Corner Cases',
'isbn-10': '032133678X',
'isbn-13': '978-0321336781',
'authors': ['Joshua Bloch', 'Neal Gafter']
}
這個 JSON 對象包含 4 個字段,其中有一個是數組,這些字段表徵了一本書的基本信息。如果我們直接使用 Gson 來解析:
Book book = new Gson().fromJson(jsonString, Book.class);
會發現 ‘isbn-10’ 這種字段表示在 Java 中是不合法的,因爲 Java 的變量名中是不允許含有 ‘-‘ 符號的。對於這個例子,我們可以使用 Gson 的 @SerializedName 註解來處理,不過註解也僅限於此,遇到後文的場景,註解就無能爲力了。這個時候,就是 JsonDeserializer 派上用場的時候。
假如 Book 類定義如下:
public class Book {
private String[] authors;
private String isbn10;
private String isbn13;
private String title;
// 其他方法省略
}
這個 Book 也定義有 4 個字段,與 JSON 對象的結構基本類似。爲了將 JSON 對象解析爲 Java 對象,我們需要自定義一個 JsonDeserializer 然後註冊到 GsonBuilder 上,並使用 GsonBuilder 來獲取解析用的 Gson 對象。
因此, 我們先創建一個 BookDeserializer,這個 BookDeserializer 負責將 Book 對應的 JSON 對象解析爲 Java 對象。
public class BookDeserializer implements JsonDeserializer<Book> {
@Override
public Book deserialize(final JsonElement jsonElement, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
//todo 解析字段
final Book book = new Book();
book.setTitle(title);
book.setIsbn10(isbn10);
book.setIsbn13(isbn13);
book.setAuthors(authors);
return book;
}
}
在實現具體的解析之前,我們先來了解一下涉及到的各個類的含義。JsonDeserializer 需要一個 Type,也就是要得到的對象類型,這裏當然就是 Book,同時 deserialize() 方法返回的就是 Book 對象。Gson 解析 Json 對象並在內部表示爲 JsonElement,一個 JsonElement 可以是如下的任何一種:
- JsonPrimitive :Java 基本類型的包裝類,以及 String
- JsonObject:類比 Js 中 Object 的表示,或者 Java 中的 Map<String, JsonElement>,一個鍵值對結構。
- JsonArray:JsonElement 組成的數組,注意:這裏是 JsonElement,說明這個數組是混合類型的。
- JsonNull:值爲 null
開始解析(其實這個解析很直觀的,Json 裏面是什麼類型,就按照上面的類型進行對照解析即可),所以, 我們先將 JsonElement 轉換爲 JsonObject:
// jsonElement 是 deserialize() 的參數
final JsonObject jsonObject = jsonElement.getAsJsonObject();
對照 JSON 定義,然後獲取 JsonObject 中的 title。
final JsonObject jsonObject = jsonElement.getAsJsonObject();
JsonElement titleElement = jsonObject.get("title")
我們知道 titleElement 具體是一個字符串(字符串屬於 JsonPrimitive),因爲可以直接轉換:
final JsonObject jsonObject = jsonElement.getAsJsonObject();
JsonElement titleElement = jsonObject.get("title")
final String title = jsonTitle.getAsString();
此時我們得到了 title 值,其他類似:
public class BookDeserializer implements JsonDeserializer<Book> {
@Override
public Book deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
final JsonObject jsonObject = jsonElement.getAsJsonObject();
final JsonElement jsonTitle = jsonObject.get("title");
final String title = jsonTitle.getAsString();
final String isbn10 = jsonObject.get("isbn-10").getAsString();
final String isbn13 = jsonObject.get("isbn-13").getAsString();
final JsonArray jsonAuthorsArray = jsonObject.get("authors").getAsJsonArray();
final String[] authors = new String[jsonAuthorsArray.size()];
for (int i = 0; i < authors.length; i++) {
final JsonElement jsonAuthor = jsonAuthorsArray.get(i);
authors[i] = jsonAuthor.getAsString();
}
final Book book = new Book();
book.setTitle(title);
book.setIsbn10(isbn10);
book.setIsbn13(isbn13);
book.setAuthors(authors);
return book;
}
}
整體還是很直觀的,我們測試一下:
public static void main(String[] args) {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Book.class, new BookDeserializer());
Gson gson = gsonBuilder.create();
Book book = gson.fromJson("{\"title\":\"Java Puzzlers: Traps, Pitfalls, and Corner Cases\",\"isbn-10\":\"032133678X\",\"isbn-13\":\"978-0321336781\",\"authors\":[\"Joshua Bloch\",\"Neal Gafter\"]}", Book.class);
System.out.println(book);
}
輸出結果:
Book{authors=[Joshua Bloch, Neal Gafter], isbn10='032133678X', isbn13='978-0321336781', title='Java Puzzlers: Traps, Pitfalls, and Corner Cases'}
上面,我們使用 GsonBuilder 來註冊 BookDeserializer,並創建 Gson 對象。此處得到的 Gson 對象,在遇到需要解析 Book 的時候,就會使用 BookDeserializer 來解析。比如我們使用
gson.fromJson(data, Book.class);
解析的時候,大致流程如下:
- Gson 將輸入字符串解析爲 JsonElement,同時,這一步也會校驗 JSON 的合法性。
- 找到對應的 JsonDeserializer 來解析這個 JsonElement,這裏就找到了 BookDeserializer。
- 傳入必要的參數並執行 deserialize(),本例中就是在 deserialize() 將一個 JsonElement 轉換爲 Book 對象。
- 將 deserialize() 的解析結果返回給 fromJson() 的調用者。
對象嵌套
在上面的例子中,一本書的作者都只用了一個名字來表示,但是實際情況中,一個作者可能有很多本書,每個作者實際上還有個唯一的 id 來進行區分。結構如下:
{
'title': 'Java Puzzlers: Traps, Pitfalls, and Corner Cases',
'isbn': '032133678X',
'authors':[
{
'id': 1,
'name': 'Joshua Bloch'
},
{
'id': 2,
'name': 'Neal Gafter'
}
]
}
此時我們不僅有個 Book 類,還有一個 Author 類:
public class Author{
long id;
String name;
}
那麼問題來了,誰來負責解析這個 authors?有幾個選擇:
- 我們可以更新 BookDeserializer,同時在其中解析 authors 字段。這種方案耦合了 Book 與 Author 的解析,並不推薦。
- 我們可以使用默認的 Gson 實現,在本例中,Author 類與 author 的 JSON 字符串是一一對應的,因此,這種實現完全沒問題。
- 我們還可以實現一個 AuthorDeserializer 來處理 author 字符串的解析問題。
這裏我們使用第二種方式,這種方式的改動最小:
JsonDeserializer 的 deserialize() 方法提供了一個 JsonDeserializationContext 對象,這個對象基於 Gson 的默認機制,我們可以選擇性的將某些對象的反序列化工作委託給這個JsonDeserializationContext。JsonDeserializationContext 會解析 JsonElement 並返回對應的對象實例:
Author author = jsonDeserializationContext.deserialize(jsonElement, Author.class);
如上例所示,當遇到有 Author 類的解析需求時,jsonDeserializationContext 會去查找用來解析 Author 的 JsonDeserializer,如果有自定義的 JsonDeserializer 被註冊過,那麼就用自定義的 JsonDeserializer 來解析 Author,如果沒有找到自定義的 JsonDeserializer,那就按照 Gson 的默認機制來解析 Author。下面的代碼中,我們沒有自定義 Author 的 JsonDeserializer,所以 Gson 會自己來處理 authors:
import java.lang.reflect.Type;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
public class BookDeserializer implements JsonDeserializer<Book> {
@Override
public Book deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
final JsonObject jsonObject = json.getAsJsonObject();
final String title = jsonObject.get("title").getAsString();
final String isbn10 = jsonObject.get("isbn-10").getAsString();
final String isbn13 = jsonObject.get("isbn-13").getAsString();
// 委託給 Gson 的 context 來處理
Author[] authors = context.deserialize(jsonObject.get("authors"), Author[].class);
final Book book = new Book();
book.setTitle(title);
book.setIsbn10(isbn10);
book.setIsbn13(isbn13);
book.setAuthors(authors);
return book;
}
}
除了上面的方式,我們同樣可以自定義一個 ArthurDeserialiser 來解析 Author:
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
public class AuthorDeserializer implements JsonDeserializer {
@Override
public Author deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
final JsonObject jsonObject = json.getAsJsonObject();
final Author author = new Author();
author.setId(jsonObject.get("id").getAsInt());
author.setName(jsonObject.get("name").getAsString());
return author;
}
}
爲了使用 ArthurDeserialiser,同樣要用到 GsonBuilder:
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class Main {
public static void main(final String[] args) throws IOException {
// Configure GSON
final GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Book.class, new BookDeserializer());
gsonBuilder.registerTypeAdapter(Author.class, new AuthorDeserializer());
final Gson gson = gsonBuilder.create();
// Read the JSON data
try (Reader reader = new InputStreamReader(Main.class.getResourceAsStream("/part2/sample.json"), "UTF-8")) {
// Parse JSON to Java
final Book book = gson.fromJson(reader, Book.class);
System.out.println(book);
}
}
}
相比委託的方式,自定義 AuthorDeserializer 就根本不需要修改 BookDeserializer 任何代碼,Gson 幫你處理了所有的問題。
對象引用
考慮下面的 json 文本:
{
'authors': [
{
'id': 1,
'name': 'Joshua Bloch'
},
{
'id': 2,
'name': 'Neal Gafter'
}
],
'books': [
{
'title': 'Java Puzzlers: Traps, Pitfalls, and Corner Cases',
'isbn': '032133678X',
'authors':[1, 2]
},
{
'title': 'Effective Java (2nd Edition)',
'isbn': '0321356683',
'authors':[1]
}
]
}
這個 JSON 對象包含兩個 book,兩個 author,每本書都通過 author 的 id 關聯到 author。因此,每個 book 的 author 值都只是一個 id 而已。這種表示形式在網絡響應裏面非常常見,通過共用對象減少重複定義來減小響應的大小。
這又給解析 JSON 帶來新的挑戰,我們需要將 book 和 author 對象組合在一起,但是在解析 JSON文本的時候,Gson 是以類似樹遍歷的路徑來解析的,在解析到 book 的時候,我們只能看到 author 的 id,此時具體的 author 信息卻在另一個分支上,在當前的 JsonDeserializationContext 中無法找到所需要的 author。
有幾種方法可以處理這個問題:
- 第一個方案,分兩段解析。第一段:按照 json 的結構將 json 文本解析成對應的 java 對象,此時,每個 book 對象都包含一個 id 的數組。第二段:直接在得到的 java 對象中,將 author 對象關聯到 book 對象。這種方式的有點就是提供了最大的擴展性,不過缺點也是非常明顯的,這種方式需要額外的一組輔助 java 類來表示對應的 json 文本結構,最後在轉換爲滿足應用需求的 java 對象(即我們定義的 model)。在本例中,我們的 model 只有兩個類: Book 與 Author。但如果使用分段解析的方式,我們還需要額外定義一個輔助 Book 類。在本例中還說得過去,對於那些也有幾十上百的 model 來說,那就相當複雜了。
- 另一個方案是向 BookDeserialiser 傳遞所有的 author 集合,當 BookDeserialiser 在解析到 Author 屬性的時候,直接通過 id 從 author 集合中取得對應的 Author 對象。這樣就省略了中間步驟以及額外的對象定義。這種方式看起來甚好,不過這要求 BookDeserialiser 和 AuthorDeserialiser 共享同一個對象集合。當獲取 author 的時候,BookDeserialiser 需要去訪問這個共享對象,而不是像我們先前那樣直接從 JsonDeserializationContext 中得到。這樣就導致了好幾處的改動,包括 BookDeserialiser,AuthorDeserialiser 以及 main() 方法。
- 第三種方案是 AuthorDeserialiser 緩存解析得到的 author 集合,當後面需要通過 id 得到具體 Author 對象的時候,直接返回緩存值。這個方案的好處是通過 JsonDeserializationContext 來實現對象的獲取,並且對其他部分都是透明的。缺點就是增加了 AuthorDeserialiser 的複雜度,需要修改 AuthorDeserialiser 來處理緩存。
上述方案都有利有弊,可以針對不同的情況,權衡使用。後文以第三種方案來實現,以避免過多的改動。
Observation
原理上講,相較於後兩種方案,第一種方案提供了更直觀的關注點分離,獲得中間結果之後,我們可以在外層的 Data 類中實現最後的裝配。不過第一種方案的改動太大,這一點前面也說過。我們的目標是做到影響面最小,這也是選用第三種方案的主要原因。
JSON 對象包含兩個數組,因此我們需要一個新的類來反映這種結。
public class Data {
private Author[] authors;
private Book[] books;
}
這兩個屬性的順序決定了兩者的解析順序,不過在我們的實現中,解析順序無關緊要,隨便哪個屬性先解析都可以,具體見後文。
先修改 AuthorDeserialiser 來支持緩存 author 集合:
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
public class AuthorDeserializer implements JsonDeserializer<Author> {
private final ThreadLocal<Map<Integer, Author>> cache = new ThreadLocal<Map<Integer, Author>>() {
@Override
protected Map<Integer, Author> initialValue() {
return new HashMap<>();
}
};
@Override
public Author deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
// Only the ID is available
if (json.isJsonPrimitive()) {
final JsonPrimitive primitive = json.getAsJsonPrimitive();
return getOrCreate(primitive.getAsInt());
}
// The whole object is available
if (json.isJsonObject()) {
final JsonObject jsonObject = json.getAsJsonObject();
final Author author = getOrCreate(jsonObject.get("id").getAsInt());
author.setName(jsonObject.get("name").getAsString());
return author;
}
throw new JsonParseException("Unexpected JSON type: " + json.getClass().getSimpleName());
}
private Author getOrCreate(final int id) {
Author author = cache.get().get(id);
if (author == null) {
author = new Author();
author.setId(id);
cache.get().put(id, author);
}
return author;
}
}
我們來逐一看看:
(1) author 集合緩存在下面的對象中:
private final ThreadLocal<Map<Integer, Author>> cache = new ThreadLocal<Map<Integer, Author>>() {
@Override
protected Map<Integer, Author> initialValue() {
return new HashMap<>();
}
};
本實現使用 Map<String, Object> 作爲緩存機制,並保存在 ThreadLocal 中,從而進行線程隔離。當然,可是使用其他更好的緩存方案,這個不關鍵。
(2) 通過下面的方法獲取 author :
private Author getOrCreate(final int id) {
Author author = cache.get().get(id);
if (author == null) {
author = new Author();
cache.get().put(id, author);
}
return author;
}
即先通過 id 在緩存中查找 author,如果找到了,就直接爲返回對應的 author,否則就根據 id 創建一個空的 author,並加入緩存中,然後返回這個新建的 author。
通過這種方式,我們得以先創建一個吻合 id 的空 author 對象,等到 author 正真可用的時候,再填充缺失的信息。這就是爲什麼在本例實現中,解析順序並沒有影響的原因,因爲緩存對象是共享的。我們可以先解析 book 再解析 author,如果是這種順序,那麼當 book 被解析的時候,其內部的 author 屬性只是空有一個 id 的佔位對象而已。等到後續解析到 author 的時候,纔會真正填充其他信息。
(3) 爲了適應新需求,我們修改了 deserialize() 方法。在這個 deserialize() 實現中,其接收的 JsonElement 可能是一個 JsonPrimitive 或者是一個 JsonObject。在 BookDeserialiser 中,碰到解析 author 的時候,傳遞給 AuthorDeserializer#deserialize() 的就是一個 JsonPrimitive,
// BookDeserialiser 中解析 authors
Author[] authors = context.deserialize(jsonObject.get("authors"), Author[].class);
BookDeserialiser 將解析任務委託給 context,context 找到 AuthorDeserializer 來解析這個 Author 數組。此時,AuthorDeserializer#deserialize() 接收到的就是 BookDeserialiser 傳遞過來的 JsonPrimitive。
另一方面,當解析到 authors 的時候,AuthorDeserializer#deserialize() 接收到的就是 JsonObject。因此,在處理的時候,先做一個類型檢測,然互進行恰當的轉換:
// 處理 Id 的情況
if (json.isJsonPrimitive()) {
final JsonPrimitive primitive = json.getAsJsonPrimitive();
final Author author = getOrCreate(primitive.getAsInt());
return author;
}
如果傳遞進來的是 id, 就先將 JsonElement 轉換爲 JsonPrimitive 再得到 int。這個 int 就是用來獲取 Author 緩存的 id 值。
如果是 JsonObject,就如下轉換:
// The whole object is available
if (json.isJsonObject()) {
final JsonObject jsonObject = json.getAsJsonObject();
final Author author = getOrCreate(jsonObject.get("id").getAsInt());
author.setName(jsonObject.get("name").getAsString());
return author;
}
這一步在返回最終的 author 之前,完成了 author 的填充工作。
如果傳遞進來的 JsonElement 既不是 JsonPrimitive 也不是 JsonObject,那就說明出錯了,中斷處理過程。
throw new JsonParseException("Unexpected JSON type: " + json.getClass().getSimpleName());
至此, BookDeserialiser 與 main() 都不需要修改,同時也能滿足我們的需求。
Gson 的 desieralizer 是一個強大而靈活的設計,合理運用也可以使我們的設計靈活而容易擴展。
原文地址:http://www.javacreed.com/gson-deserialiser-example/