【Java】Gson解析複雜數據

轉載來源: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 可以是如下的任何一種:

  1. JsonPrimitive :Java 基本類型的包裝類,以及 String
  2. JsonObject:類比 Js 中 Object 的表示,或者 Java 中的 Map<String, JsonElement>,一個鍵值對結構。
  3. JsonArray:JsonElement 組成的數組,注意:這裏是 JsonElement,說明這個數組是混合類型的。
  4. 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);

解析的時候,大致流程如下:

  1. Gson 將輸入字符串解析爲 JsonElement,同時,這一步也會校驗 JSON 的合法性。
  2. 找到對應的 JsonDeserializer 來解析這個 JsonElement,這裏就找到了 BookDeserializer。
  3. 傳入必要的參數並執行 deserialize(),本例中就是在 deserialize() 將一個 JsonElement 轉換爲 Book 對象。
  4. 將 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?有幾個選擇:

  1. 我們可以更新 BookDeserializer,同時在其中解析 authors 字段。這種方案耦合了 Book 與 Author 的解析,並不推薦。
  2. 我們可以使用默認的 Gson 實現,在本例中,Author 類與 author 的 JSON 字符串是一一對應的,因此,這種實現完全沒問題。
  3. 我們還可以實現一個 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。

有幾種方法可以處理這個問題:

  1. 第一個方案,分兩段解析。第一段:按照 json 的結構將 json 文本解析成對應的 java 對象,此時,每個 book 對象都包含一個 id 的數組。第二段:直接在得到的 java 對象中,將 author 對象關聯到 book 對象。這種方式的有點就是提供了最大的擴展性,不過缺點也是非常明顯的,這種方式需要額外的一組輔助 java 類來表示對應的 json 文本結構,最後在轉換爲滿足應用需求的 java 對象(即我們定義的 model)。在本例中,我們的 model 只有兩個類: Book 與 Author。但如果使用分段解析的方式,我們還需要額外定義一個輔助 Book 類。在本例中還說得過去,對於那些也有幾十上百的 model 來說,那就相當複雜了。
  2. 另一個方案是向 BookDeserialiser 傳遞所有的 author 集合,當 BookDeserialiser 在解析到 Author 屬性的時候,直接通過 id 從 author 集合中取得對應的 Author 對象。這樣就省略了中間步驟以及額外的對象定義。這種方式看起來甚好,不過這要求 BookDeserialiser 和 AuthorDeserialiser 共享同一個對象集合。當獲取 author 的時候,BookDeserialiser 需要去訪問這個共享對象,而不是像我們先前那樣直接從 JsonDeserializationContext 中得到。這樣就導致了好幾處的改動,包括 BookDeserialiser,AuthorDeserialiser 以及 main() 方法。
  3. 第三種方案是 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/


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章