Gson解析复杂数据

2017-02-17

本文主要关注所解析的 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/


文档信息 by XiaoPingYuan

版权声明:自由转载-非商用-非衍生-保持署名。发表日期:2017-02-17 by XiaoPingYuan(https://xesam.github.io/)