Java-XML-和-JSON-教程-三-

43 阅读38分钟

Java XML 和 JSON 教程(三)

原文:Java XML and JSON

协议:CC BY-NC-SA 4.0

九、使用 Gson 解析和创建 JSON 对象

Gson 是用于解析和创建 JSON 对象的另一个 API。本章探索了这个开源谷歌产品的最新版本。

Gson 是什么?

Gson(也称为 Google Gson)是一个基于 Java 的小型库,用于解析和创建 JSON 对象。谷歌为自己的项目开发了 Gson,但后来从 1.0 版本开始,Gson 公开可用。根据维基百科,最新版本(在撰写本文时)是 2.6.1。

Gson 的开发目标如下:

  • 提供简单的toJson()fromJson()方法将 Java 对象转换成 JSON 对象,反之亦然。
  • 允许预先存在的不可修改对象与 JSON 相互转换。
  • 为 Java 泛型提供广泛的支持。
  • 允许对象的自定义表示。
  • 支持任意复杂的对象(具有深度继承层次和广泛使用泛型类型)。

Gson 通过将 JSON 对象反序列化为 Java 对象来解析 JSON 对象。类似地,它通过将 Java 对象序列化为 JSON 对象来创建 JSON 对象。Gson 依靠 Java 的反射 API 来协助序列化和反序列化。

获取和使用 Gson

Gson 作为单个 Jar 文件分发;gson-2.6.1.jar是编写时最新的 Jar 文件。要获取这个 Jar 文件,将浏览器指向 http://search.maven.org/#artifactdetails|com.google.code.gson|gson|2.6.1|jar ,从页面底部附近的列表中选择gson-2.6.1.jar,并下载。另外,您可能想下载gson-2.6.1-javadoc.jar,它包含这个 API 的 Javadoc。

Note

Gson 根据 Apache 许可证版本 2.0 ( www.apache.org/licenses/ )进行许可。

使用gson-2.6.1.jar很容易。编译源代码或运行应用程序时,只需将它包含在类路径中,如下所示:

javac -cp gson-2.6.1.jar source file

java -cp gson-2.6.1.jar;. main classfile

探索 GSon

Gson 由 30 多个类和接口组成,分布在四个包中:

  • com.google.gson:这个包提供了对Gson的访问,它是使用 Gson 的主类。
  • com.google.gson.annotations:这个包提供了与 Gson 一起使用的注释类型。
  • 这个包提供了一个从泛型类型中获取类型信息的工具类。
  • com.google.gson.stream:这个包提供了用于读写 JSON 编码值的实用程序类。

在本节中,我首先向您介绍Gson类。然后,我将重点放在Gson反序列化(解析 JSON 对象)和Gson序列化(创建 JSON 对象)上。最后,我简要讨论了 Gson 的其他特性,比如注释和类型适配器。

Gson 类简介

Gson类处理 JSON 和 Java 对象之间的转换。您可以通过使用Gson()构造函数实例化这个类,或者通过使用com.google.gson.GsonBuilder类获得一个Gson实例。以下代码片段演示了这两种方法:

Gson gson1 = new Gson();
Gson gson2 = new GsonBuilder()
     .registerTypeAdapter(Id.class, new IdTypeAdapter())
     .serializeNulls()
     .setDateFormat(DateFormat.LONG)
     .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
     .setPrettyPrinting()
     .setVersion(1.0)
     .create();

当您想要使用默认配置时调用Gson(),当您想要覆盖默认配置时使用GsonBuilder。配置方法调用被链接在一起,最后调用GsonBuilderGson create()方法以返回结果Gson对象。

Gson支持以下默认配置(列表不完整;查看GsonGsonBuilder文档了解更多信息):

  • Gsonjava.lang.Enumjava.util.MapURLjava.net.URI、、java.util.Localejava.util.Datejava.math.BigDecimaljava.math.BigInteger实例提供了默认的序列化和反序列化。您可以通过GsonBuilder.registerTypeAdapter(Type, Object)注册一个类型适配器(稍后讨论)来改变默认表示。
  • 生成的 JSON 文本省略了所有的null字段。然而,它在数组中保留了null s,因为数组是一个有序列表。同样,如果一个字段不是null,但是它生成的 JSON 文本是空的,那么这个字段将被保留。您可以通过调用GsonBuilder.serializeNulls()配置Gson来序列化null值。
  • 默认Date格式与java.text. DateFormat.DEFAULT 相同。这种格式在序列化过程中忽略日期的毫秒部分。您可以通过调用GsonBuilder.setDateFormat(int)GsonBuilder.setDateFormat(String)来更改默认格式。
  • 输出 JSON 文本的默认字段命名策略与 Java 中的相同。例如,一个名为versionNumber的 Java 类字段在 JSON 中将被输出为"versionNumber"。同样的规则也适用于将传入的 JSON 映射到 Java 类。您可以通过调用GsonBuilder.setFieldNamingPolicy(FieldNamingPolicy )来更改此策略。
  • toJson()方法生成的 JSON 文本被简洁地表示出来:所有不需要的空白都被删除。您可以通过调用GsonBuilder.setPrettyPrinting()来改变这种行为。
  • 默认情况下,Gson忽略@ Since@Until标注。您可以通过调用GsonBuilder.setVersion(double)来启用Gson使用这些注释。
  • 默认情况下,Gson忽略@ Expose标注。您可以通过调用GsonBuilder.excludeFieldsWithoutExposeAnnotation()使Gson只序列化/反序列化那些用这个注释标记的字段。
  • 默认情况下,Gsontransientstatic字段排除在序列化和反序列化考虑之外。您可以通过调用GsonBuilder.excludeFieldsWithModifiers(int...)来改变这种行为。

一旦有了一个Gson对象,就可以调用各种fromJson()toJson()方法在 JSON 和 Java 对象之间进行转换。例如,清单 9-1 给出了一个简单的应用程序,它获得了一个Gson对象,并根据 JSON 原语演示了 JSON-Java 对象转换。

 import com.google.gson.Gson;

public class GsonDemo
{
   public static void main(String[] args)
   {
      Gson gson = new Gson();
      String name = gson.fromJson("\"John Doe\"", String.class);
      System.out.println(name);
      gson.toJson(256, System.out);
   }

}

Listing 9-1.Converting Between JSON and Java Primitives

清单 9-1 的main()方法首先实例化Gson,保持其默认配置。然后,它调用Gson<T> T fromJson(String json, Class<T> classOfT)泛型方法将指定的基于java.lang.String的 JSON 文本(在json中)反序列化为指定类的对象(classOfT),这个对象恰好是String

JSON 字符串"John Doe"(双引号是必需的),被表示为 Java String对象,被转换(去掉双引号)为 Java String对象。对该对象的引用被分配给name

在输出返回的名字后,main()调用Gsonvoid toJson(Object src, Appendable writer)方法将自动装箱的整数256(由编译器存储在java.lang.Integer对象中)转换成 JSON 整数,并将结果输出到标准输出流。

编译清单 9-1 如下:

javac -cp gson-2.6.1.jar GsonDemo.java

运行生成的应用程序,如下所示:

java -cp gson-2.6.1.jar;. GsonDemo

您应该观察到以下输出:

John Doe

256

产量并不可观,但这是一个开始。在接下来的两节中,您将看到更多有用的反序列化和序列化示例。

通过反序列化解析 JSON 对象

除了将 JSON 原语(比如数字或字符串)解析成它们的 Java 等价物之外,Gson还允许您将 JSON 对象解析成 Java 对象。例如,假设您有下面的 JSON 对象,它描述了一个人:

{ "name": "John Doe", "age": 45 }

此外,假设您有以下 Java 类:

class Person
{
   String name;
   int age;
}

您可以使用前面的fromJson()方法将 JSON 对象解析成Person类的实例,如清单 9-2 所示。

import com.google.gson.Gson;

public class GsonDemo
{
   static class Person
   {
      String name;
      int age;

      Person(String name, int age)
      {
         this.name = name;
         this.age = age;
      }

      @Override
      public String toString()
      {
         return name + ": " + age;
      }
   }

   public static void main(String[] args)
   {
      Gson gson = new Gson();
      String json = "{ name: \"John Doe\", age: 45 }";
      Person person = gson.fromJson(json, Person.class);
      System.out.println(person);
   }
}

Listing 9-2.Parsing a JSON Object into a Java Object

清单 9-2 声明了一个GsonDemo类和一个嵌套的Person类,后者根据姓名和年龄描述了一个人。

GsonDemomain()方法首先实例化Gson,保持其默认配置。然后,它构造一个基于String的 JSON 对象来表示一个人,并将该对象与Person.class一起传递给fromJson(String json, Class<T> classOfT). fromJson(),解析存储在传递给json的字符串中的姓名和年龄,并使用Person.class和反射 API 来创建一个Person对象,并用姓名和年龄填充它。对Person对象的引用被返回并存储在person变量中,随后被传递给System.out.println()。这个方法最终调用PersontoString()方法来返回Person对象的字符串表示,然后将这个字符串写入标准输出流。

编译清单 9-2 并运行生成的应用程序。您应该观察到以下输出:

John Doe: 45

定制的 JSON 对象解析

前面的gson.fromJson(json, Person.class)方法调用依赖于Gson的默认反序列化机制来解析 JSON 对象。您经常会遇到需要将复杂的 JSON 对象解析成 Java 对象的情况,这些 Java 对象的类与要解析的 JSON 对象的结构不同。您可以使用定制的反序列化器来执行这种解析,反序列化器控制 JSON 对象如何映射到 Java 对象。

com.google.gson.JsonDeserializer<T>接口描述了一个定制的反序列化器。传递给T的参数标识了反序列化器所使用的类型。例如,当需要解析结构稍有不同的 JSON 对象时,可以将Person传递给T

JsonDeserializer声明一个处理反序列化的方法(JSON 对象解析):

T deserialize(JsonElement json,Type typeOfT,
              JsonDeserializationContext context)

deserialize()Gson在反序列化过程中调用的回调方法。使用以下参数调用此方法:

  • json标识被反序列化的 JSON 元素。
  • typeOfT标识要反序列化的 Java 对象的类型json
  • context标识执行反序列化的上下文。(我将在后面详细介绍上下文。)

当传递给json的 JSON 元素与传递给typeOfT的类型不兼容时deserialize()抛出com.google.gson.JsonParseException。因为JsonParseException扩展了 java.lang.RuntimeException ,所以不用追加throws子句。

About Jsonelement

com.google.gson.JsonElement类表示一个 JSON 元素(比如一个数字、一个布尔值或者一个数组)。它提供了多种获取元素值的方法,如double getAsDouble()boolean getAsBoolean()JsonArray getAsJsonArray()

JsonElement是一个抽象类,作为以下 JSON 元素类的超类(在com.google.gson包中):

  • JsonArray:表示 JSON 的数组类型的具体类。一个数组是一组JsonElement的列表,每一个都可以是不同的类型。这是一个有序列表,意味着元素添加的顺序是保留的。
  • JsonNull:表示 JSON null值的具体类。
  • JsonObject:表示 JSON 对象类型的具体类。一个对象由名称-值对组成,其中名称是字符串,值是任何其他类型的JsonElement,这导致了一个JsonElement的树。
  • JsonPrimitive:表示 JSON 的数字、字符串或布尔类型之一的具体类。

除了JsonNull,这些子类都提供了各种获取元素值的方法。

创建一个JsonDeserializer对象后,需要用Gson注册它。通过调用下面的GsonBuilder方法来完成这个任务:

GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) 

传递给type的对象标识了解串器的类型,传递给typeAdapter的对象标识了解串器。因为registerTypeAdapter(Type, Object)返回一个GsonBuilder对象,所以只能在GsonBuilder上下文中使用这个方法。

为了演示定制的 JSON 对象解析,考虑前面的 JSON 对象的扩展版本:

{ "first-name": "John", "last-name": "Doe", "age": 45, "address": "Box 1 " }

这个 JSON 对象与之前的 JSON 对象有很大的不同,之前的 JSON 对象由nameage字段组成:

  • 字段name已经被重构为first-namelast-name字段。注意,连字符(-)不是 Java 标识符的合法字符。
  • 添加了一个address字段。

如果您通过用这个新对象替换分配给json的对象来修改清单 9-2 ,您应该不会对下面的输出感到惊讶:

null: 45

解析完全混乱了。但是,您可以通过引入以下自定义反序列化程序来解决此问题:

class PersonDeserializer implements JsonDeserializer<Person>
{
   @Override
   public Person deserialize(JsonElement json, Type typeOfT,  JsonDeserializationContext context)
   {
      JsonObject jsonObject = json.getAsJsonObject();
      String firstName = jsonObject.get("first-name").getAsString();
      String lastName = jsonObject.get("last-name").getAsString();
      int age = jsonObject.getAsJsonPrimitive("age").getAsInt();
      String address = 

jsonObject.get("address").getAsString();
      return new Person(firstName + " " + lastName, 45);
   }
}

当定制的反序列化器与之前的 JSON 对象一起使用时,deserialize()只被调用一次,并且类型为JsonObject的对象被传递给json。您可以将这个值转换成一个JsonObject,就像在JsonObject jsonObject = (JsonObject) json;中一样。或者,您可以调用JsonElementJsonObject getAsJsonObject()方法来获取JsonObject引用,这是deserialize()首先要完成的。

获得JsonObject引用后,deserialize()调用其JsonElement get( String memberName)方法返回一个JsonElement作为所需的memberName值。第一个调用从first-name传递到get();您希望获得这个 JSON 字段的值。因为返回一个JsonPrimitive来代替JsonElement,所以对JsonPrimitiveString getAsString()方法的调用被链接到JsonPrimitive引用,并获得first-name的值。按照这种模式获取last-nameaddress字段的值。

为了多样化,我决定在age领域做一些不同的事情。我调用JsonObjectJsonPrimitive getAsJsonPrimitive( String memberName)方法返回一个JsonPrimitive引用对应age。然后,我调用JsonPrimitiveint getAsInt()方法返回整数值。

获得所有字段值后,创建并返回一个Person对象。因为我重用了清单 9-2 中所示的Person类,并且因为这个类中没有address字段,所以我丢弃了address的值。您可能想要修改Person来包含这个字段。

下面的代码片段展示了如何实例化PersonDeserializer并用GsonBuilder实例注册它,该实例也用于获取Gson实例,以便调用fromJson(),通过 person 反序列化器解析之前的 JSON 对象:

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Person.class, new PersonDeserializer());
Gson gson = gsonBuilder.create();

我已经将这些代码片段组合成一个工作应用程序。清单 9-3 展示了应用程序的源代码。

import java.lang.reflect.Type;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
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 GsonDemo
{
   static class Person
   {
      String name;
      int age;

      Person(String name, int age)
      {
         this.name = name;
         this.age = age;
      }

      @Override
      public String toString()
      {
         return name + ": " + age;
      }
   }

   public static void main(String[] args)
   {
      class PersonDeserializer implements JsonDeserializer<Person>
      {
         @Override
         public Person deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
         {
            JsonObject jsonObject = json.getAsJsonObject();
            String firstName = jsonObject.get("first-name").getAsString();
            String lastName = jsonObject.get("last-name").getAsString();
            int age = jsonObject.getAsJsonPrimitive("age").getAsInt();
            String address = jsonObject.get("address").getAsString();
            return new Person(firstName + " " + lastName, 45);
         }
      }
      GsonBuilder gsonBuilder = new GsonBuilder();
      gsonBuilder.registerTypeAdapter(Person.class, new PersonDeserializer());
      Gson gson = gsonBuilder.create();
      String json = "{ first-name: \"John\", last-name: \"Doe\", " + "age: 45, address: \"Box 1\" }";
      Person person = gson.fromJson(json, Person.class);
      System.out.println(person);
   }
}

Listing 9-3.Parsing a JSON Object into a Java Object via a Custom Deserializer

编译清单 9-3 并运行生成的应用程序。您应该观察到以下输出:

John Doe: 45

通过序列化创建 JSON 对象

Gson让您通过调用GsontoJson()方法之一,从 Java 对象创建 JSON 对象。清单 9-4 提供了一个简单的演示。

import com.google.gson.Gson;

public class GsonDemo
{
   static class Person
   {
      String name;
      int age;

      Person(String name, int age)
      {
         this.name = name;
         this.age = age;
      }
   }

   public static void main(String[] args)
   {
      Person p = new Person("Jane Doe", 59);
      Gson gson = new Gson();
      String json = gson.toJson(p);
      System.out.println(json);
   }
}

Listing 9-4.Creating a JSON Object from a Java Object

清单 9-4 的main()方法首先从嵌套的Person类创建一个Person对象。然后它创建一个Gson对象,并调用该对象的StringtoJson(Objectsrc)方法将Person对象序列化为其等效的 JSON 字符串表示,由toJson(Object)返回。

编译清单 9-4 并运行生成的应用程序。您应该观察到以下输出:

{"name":"Jane Doe","age":59}

如果您更喜欢将 JSON 对象写到文件、字符串缓冲区或其他一些java.lang.Appendable中,您可以调用void toJson(Object src, Appendable writer)来完成这项任务。这个toJson()变体将其输出发送到指定的writer,如清单 9-5 所示。

import java.io.FileWriter;
import java.io.IOException;

import com.google.gson.Gson;

public class GsonDemo
{
   static class Student
   {
      String name;
      int id;
      int[] grades;

      Student(String name, int id, int... grades)
      {
         this.name = name;
         this.id = id;
         this.grades = grades;
      }

   }

   public static void main(String[] args) throws IOException
   {
      Student s = new Student("John Doe", 820787, 89, 78, 97, 65);
      Gson gson = new Gson();
      FileWriter fw = new FileWriter("student.json");
      gson.toJson(s, fw);
      fw.close();
   }
}

Listing 9-5.Creating a JSON Object from a Java Object and Writing the JSON Object to a File

清单 9-5 的main()方法首先从嵌套的Student类中创建一个Student对象。然后创建Gsonjava.io.FileWriter对象,并调用Gson对象的toJson( Object , Appendable)方法将Student对象序列化为其等效的 JSON 字符串表示,并将结果写入student.json。然后关闭文件写入器,以便将缓冲的内容写入文件(您可以改为指定fw.flush();)。

如果您运行这个应用程序,您将不会观察到任何输出。但是,您应该观察到一个包含以下内容的student.json文件:

{"name":"John Doe","id":820787,"grades":[89,78,97,65]}

Note

当出现 I/O 错误时,void toJson(Object src, Appendable writer)抛出未检查的com.google.gson.JsonIOException

定制的 JSON 对象创建

前面的gson.toJson(p)gson. toJson (s, fw)方法调用依靠Gson的默认序列化机制来创建 JSON 对象。您经常会遇到需要从 Java 对象创建 JSON 对象的情况,这些 Java 对象的类与要创建的 JSON 对象的结构不同。您可以使用定制的序列化程序来执行这种创建,它控制 Java 对象如何映射到 JSON 对象。

com.google.gson.JsonSerializer<T>接口描述了一个定制的串行化器。传递给T的参数标识了正在使用的序列化程序的类型。例如,当需要创建结构稍有不同的 JSON 对象时,可以将Person传递给T

JsonSerializer声明一个处理序列化(JSON 对象创建)的方法:

JsonElement serialize(T src, Type typeOfSrc,
                      JsonSerializationContext context)

serialize()Gson在序列化过程中调用的回调方法。使用以下参数调用此方法:

  • src标识需要序列化的 Java 对象。
  • typeOfSrc标识由src指定的要序列化的 Java 对象的实际类型。
  • context标识执行序列化的上下文。(我将在后面详细介绍上下文。)

创建一个JsonSerializer对象后,需要用Gson注册它。通过调用下面的GsonBuilder方法来完成这个任务:

GsonBuilder registerTypeAdapter(Type type, Object typeAdapter)

传递给type的对象标识串行器的类型,传递给typeAdapter的对象标识串行器。因为registerTypeAdapter(Type, Object)返回一个GsonBuilder对象,所以只能在GsonBuilder上下文中使用这个方法。

为了演示定制的 JSON 对象创建,考虑清单 9-6 中的Book类。

public class Book
{
   private String title;
   private String[] authors;
   private String isbn10;
   private String isbn13;

   public Book(String title, String[] authors, String isbn10, String isbn13)
   {
      this.title = title;
      this.authors = authors;
      this.isbn10 = isbn10;
      this.isbn13 = isbn13;
   }

   public String getTitle()
   {
      return title;
   }

   public String[] getAuthors()
   {
      return authors;
   }

   public String getIsbn10()
   {
      return isbn10;
   }

   public String getIsbn13()
   {
      return isbn13;
   }
}

Listing 9-6.Describing a Book as a Title, List of Authors, and ISBN Numbers

继续,假设Book对象将被序列化为具有以下格式的 JSON 对象:

{
   "title": title

   "lead-author": author0

   "other-authors": [ author1, author2, ... ]
   "isbn-10": isbn10

   "isbn-13": isbn13

}

您不能使用默认序列化,因为Book类没有声明lead-authorother-authorsisbn-10isbn-13字段。在任何情况下,缺省序列化都会创建与 Java 类的字段名相匹配的 JSON 属性名(对于 Java 标识符,连字符是非法的)。为了证明您无法使用默认序列化获得所需的 JSON 对象,假设您尝试执行以下代码片段:

Book book = new Book("PHP and MySQL Web Development, Second Edition", new String[] { "Luke Welling", "Laura Thomson" }, "067232525X", "075-2063325254");
Gson gson = new Gson();
System.out.println(gson.toJson(book));

此代码片段生成以下输出:

{"title":"PHP and MySQL Web Development, Second Edition","authors":["Luke Welling","Laura Thomson"],"isbn10":"067232525X","isbn13":"075-2063325254"}

输出与预期的 JSON 对象不匹配。但是,您可以通过引入以下自定义序列化程序来解决此问题:

class BookSerializer implements JsonSerializer<Book>
{
   @Override
   public JsonElement serialize(Book src, Type typeOfSrc,                                 JsonSerializationContext context)
   {
      JsonObject jsonObject = new JsonObject();
      jsonObject.addProperty("title", src.getTitle());
      jsonObject.addProperty("lead-author", src.getAuthors()[0]);
      JsonArray jsonOtherAuthors = new JsonArray();
      for (int i = 1; i < src.getAuthors().length; i++)
      {

         JsonPrimitive jsonAuthor =
            new JsonPrimitive(src.getAuthors()[i]);
         jsonOtherAuthors.add(jsonAuthor);
      }
      jsonObject.add("other-authors", jsonOtherAuthors);
      jsonObject.addProperty("isbn-10", src.getIsbn10());
      jsonObject.addProperty("isbn-13", src.getIsbn13());
      return jsonObject;
   }
}

当自定义序列化程序与之前的 Java Book对象一起使用时,serialize()只被调用一次,而Book对象被传递给src。因为这个方法需要一个 JSON 对象,serialize()首先创建一个JsonObject实例。

JsonObject声明了几个addProperty()方法,用于向一个JsonObject实例表示的 JSON 对象添加属性。serialize()调用void addProperty(Stringproperty,Stringvalue)方法添加titlelead-authorisbn-10isbn-13属性。

other-authors属性的处理方式不同。首先,serialize()创建一个JsonArray实例,并用除第一作者之外的所有作者填充它。然后,调用JsonObjectvoid add( String property, JsonElement value)方法将JsonArray对象添加到JsonObject中。

序列化完成后,serialize()返回创建并填充的JsonObject

下面的代码片段展示了如何实例化BookSerializer并用一个GsonBuilder实例注册它,该实例也用于获得一个Gson实例以便调用toJson(),通过图书序列化程序创建所需的 JSON 对象:

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Book.class, new BookSerializer());

Gson gson = gsonBuilder.create();

我已经将这些代码片段组合成一个工作应用程序。清单 9-7 展示了应用程序的源代码。

import java.lang.reflect.Type;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class GsonDemo
{
   public static void main(String[] args)
   {
      class BookSerializer implements JsonSerializer<Book>
      {
         @Override
         public JsonElement serialize(Book src, Type typeOfSrc, JsonSerializationContext context)
         {
            JsonObject jsonObject = new JsonObject();
            jsonObject.addProperty("title", src.getTitle());
            jsonObject.addProperty("lead-author", src.getAuthors()[0]);
            JsonArray jsonOtherAuthors = new JsonArray();
            for (int i = 1; i < src.getAuthors().length; i++)
            {
               JsonPrimitive jsonAuthor =
                  new JsonPrimitive(src.getAuthors()[i]);
               jsonOtherAuthors.add(jsonAuthor);
            }
            jsonObject.add("other-authors", jsonOtherAuthors); 

            jsonObject.addProperty("isbn-10", src.getIsbn10());
            jsonObject.addProperty("isbn-13", src.getIsbn13());
            return jsonObject;
         }
      }
      GsonBuilder gsonBuilder = new GsonBuilder();
      gsonBuilder.registerTypeAdapter(Book.class, new BookSerializer());
      Gson gson = gsonBuilder.setPrettyPrinting().create();
      Book book = new Book("PHP and MySQL Web Development, Second Edition", new String[] { "Luke Welling", "Laura Thomson" }, "067232525X", "075-2063325254");
      System.out.println(gson.toJson(book));
   }
}

Listing 9-7.Creating a JSON Object from a Java Object via a Custom Serializer

编译清单 9-7 并运行生成的应用程序。您应该观察到下面的输出,它已经被漂亮地打印出来(通过对GsonBuilder对象的setPrettyPrinting()方法调用)以使输出更加清晰:

{
  "title": "PHP and MySQL Web Development, Second Edition",
  "lead-author": "Luke Welling",
  "other-authors": [
    "Laura Thomson"
  ],
  "isbn-10": "067232525X",
  "isbn-13": "075-2063325254"
}

了解更多关于 Gson 的信息

现在您已经对 Gson 库的基础有了相当好的理解,您可能想了解这个库提供的其他特性。在这一节中,我将向您介绍注释、上下文、Gson 对泛型的支持以及类型适配器。

Note

我对 Gson 其他特性的介绍并不详尽。查看“Gson 用户指南”( https://github.com/google/gson/blob/master/UserGuide.md )来了解我没有涉及的主题,比如实例创建者。

释文

Gson 提供了几种注释类型(在com.google.gson.annotations包中)来简化序列化和反序列化:

  • Expose:向 Gson 的序列化和/或反序列化机制暴露或隐藏带注释的字段。
  • JsonAdapter:标识与类或字段一起使用的类型适配器。(我将在以后关注类型适配器时讨论这种注释类型。)
  • SerializedName:表示应该将带注释的字段或方法序列化为 JSON,并将提供的名称值作为其名称。
  • Since:标识序列化字段或类型的起始版本号。如果创建的Gson对象的版本号小于@Since注释中的值,那么带注释的字段/类型将不会被序列化。
  • Until:标识序列化字段或类型的结束版本号。如果创建的Gson对象的版本号等于或大于@Until注释中的值,那么带注释的字段/类型将不会被序列化。

Note

根据 Gson 文档,SinceUntil对于在 web 服务上下文中管理 JSON 类的版本非常有用。

显示和隐藏字段

默认情况下,Gson不会序列化和反序列化标记为transient(或static)的字段。你可以调用GsonBuilderGsonBuilder excludeFieldsWithModifiers(int... modifiers)方法来改变这种行为。另外,Gson允许您通过使用Expose注释类型的实例来注释这些字段,从而有选择地确定要序列化和/或反序列化哪些非transient字段。

Expose提供了以下元素,用于确定字段是否可以序列化以及是否可以反序列化:

  • serialize:当true时,标有此@Expose注释的字段被序列化为 JSON 文本;否则,该字段不会被序列化。默认值为true
  • deserialize:当true时,标有此@Expose注释的字段从 JSON 文本反序列化;否则,该字段不会被反序列化。默认值为true

以下代码片段显示了如何使用Expose和这些元素,以便名为someField的字段将被序列化而不是反序列化:

@Expose(serialize = true, deserialize = false)
int someField;

默认情况下,Gson忽略Expose。您必须配置Gson,通过调用下面的GsonBuilder方法来公开/隐藏用@Expose注释的字段:

GsonBuilder excludeFieldsWithoutExposeAnnotation()

创建一个GsonBuilder对象,然后调用GsonBuilderexcludeFieldsWithoutExposeAnnotation()方法,然后调用该对象的Gson create()方法,以返回一个已配置的Gson对象:

GsonBuilder gsonb = new GsonBuilder();
gsonb.excludeFieldsWithoutExposeAnnotation();
Gson gson = gsonb.create();

清单 9-8 描述了一个演示Expose注释类型的应用程序。

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import com.google.gson.annotations.Expose;

public class GsonDemo
{
   static class SomeClass
   {
      transient int id;
      @Expose(serialize = true, deserialize = true)
      transient String password;
      @Expose(serialize = false, deserialize = false)
      int field1;
      @Expose(serialize = false, deserialize = true)
      int field2;
      @Expose(serialize = true, deserialize = false)
      int field3;
      @Expose(serialize = true, deserialize = true)
      int field4;
      @Expose(serialize = true, deserialize = true)
      static int field5;
      static int field6;
   }

   public static void main(String[] args)
   {
      SomeClass sc = new SomeClass();
      sc.id = 1;
      sc.password = "abc";
      sc.field1 = 2;
      sc.field2 = 3;
      sc.field3 = 4;
      sc.field4 = 5;
      sc.field5 = 6;
      sc.field6 = 7;
      GsonBuilder gsonb = new GsonBuilder();
      gsonb.excludeFieldsWithoutExposeAnnotation();
      Gson gson = gsonb.create();
      String json = gson.toJson(sc);
      System.out.println(json);
      SomeClass sc2 = gson.fromJson(json, SomeClass.class);
      System.out.printf("id = %d%n", sc2.id);
      System.out.printf("password = %s%n", sc2.password);
      System.out.printf("field1 = %d%n", sc2.field1);
      System.out.printf("field2 = %d%n", sc2.field2);
      System.out.printf("field3 = %d%n", sc2.field3);
      System.out.printf("field4 = %d%n", sc2.field4);
      System.out.printf("field5 = %d%n", sc2.field5);
      System.out.printf("field6 = %d%n", sc2.field6);
   }
}

Listing 9-8.Exposing and Hiding Fields to and from Serialization and Deserialization

清单 9-8 展示了带有transient实例字段的Expose以及非transient实例字段和static字段。

编译清单 9-8 并运行生成的应用程序。您应该观察到以下输出:

{"field3":4,"field4":5}
id = 0
password = null
field1 = 0
field2 = 0
field3 = 0

field4 = 5
field5 = 6
field6 = 7

第一个输出行显示只有field3field4被序列化。其他字段不序列化。

第二行和第三行显示transient idpassword字段接收默认值。transient字段未被序列化/反序列化。

第四、第五和第六行显示默认的0值被分配给field1field2field3。对于field1field3deserialize被分配给false,因此只能将默认值分配给这些字段。因为field2没有序列化,所以唯一可以赋给它的值是0

第七行显示5被分配给field4。这是有意义的,因为serializedeserialize元素被赋予了true

因为static字段没有被序列化或反序列化,所以它们保持初始值,如第八和第九行所示(对于field5field6)。

Note

即使Gson序列化了static字段,field6也不会被序列化,因为它没有用@Expose注释,也因为gsonb.excludeFieldsWithoutExposeAnnotation()方法调用,导致Gson绕过没有用@ Expose注释的字段。

更改字段名称

当您只想在序列化和反序列化期间更改字段和/或方法名时,您不必使用JsonSerializer<T>JsonDeserializer<T>;比如把isbn10改成isbn-10,把isbn13改成isbn-13。您可以使用SerializedName来代替,如下所示:

@SerializedName("isbn-10")
String isbn10;
@SerializedName("isbn-13")
String isbn13;

JSON 对象表示isbn-10isbn-13属性名,而 Java 类表示isbn10isbn13字段名称。

清单 9-9 描述了一个演示SerializedName注释类型的应用程序。

import com.google.gson.Gson;

import com.google.gson.annotations.SerializedName;

public class GsonDemo
{
   static class Book
   {
      String title;
      @SerializedName("isbn-10")
      String isbn10;
      @SerializedName("isbn-13")
      String isbn13;
   }

   public static void main(String[] args)
   {
      Book book = new Book();
      book.title = "PHP and MySQL Web Development, Second Edition";
      book.isbn10 = "067232525X";
      book.isbn13 = "075-2063325254";
      Gson gson = new Gson();
      String json = gson.toJson(book);
      System.out.println(json);
      Book book2 = gson.fromJson(json, Book.class);
      System.out.printf("title = %s%n", book2.title);
      System.out.printf("isbn10 = %s%n", book2.isbn10);
      System.out.printf("isbn13 = %s%n", book2.isbn13);
   }
}

Listing 9-9.Changing Names

编译清单 9-9 并运行生成的应用程序。您应该观察到以下输出:

{"title":"PHP and MySQL Web Development, Second Edition","isbn-10":"067232525X","isbn-13":"075-2063325254"}
title = PHP and MySQL Web Development, Second Edition
isbn10 = 067232525X
isbn13 = 075-2063325254

版本控制

SinceUntil对你的类版本化很有用。使用这些注释类型,您可以确定哪些字段和/或类型被序列化为 JSON 对象。

每个@Since@Until注释都接收一个双精度浮点值作为其参数。该值指定版本号,如下所示:

@Since(1.0) private String userID;
@Since(1.0) private String password;
@Until(1.1) private String emailAddress;

@Since(1.0)表示它注释的字段将被序列化为大于或等于1.0的所有版本。类似地,@Until(1.1)表示它注释的字段将被序列化为小于1.1的所有版本。

@Since@Until版本参数相比较的版本号由下面的GsonBuilder方法指定:

GsonBuilder setVersion(double ignoreVersionsAfter)

Expose一样,您首先创建一个GsonBuilder对象,然后在该对象上用期望的版本号调用该方法,最后在GsonBuilder对象上调用create()以返回一个新创建的Gson对象:

GsonBuilder gsonb = new GsonBuilder();
gsonb.setVersion(2.0);
Gson gson = gsonb.create();

清单 9-10 描述了一个演示SinceUntil注释类型的应用程序。

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;

public class GsonDemo
{
   @Since(1.0)
   @Until(2.5)
   static class SomeClass
   {
      @Since(1.1)
      @Until(1.5)
      int field;
   }

   public static void main(String[] args)
   {
      SomeClass sc = new SomeClass();
      sc.field = 1;
      GsonBuilder gsonb = new GsonBuilder();
      gsonb.setVersion(0.9);
      Gson gson = gsonb.create();
      System.out.printf("%s%n%n", gson.toJson(sc));
      gsonb.setVersion(1.0);
      gson = gsonb.create();
      System.out.printf("%s%n%n", gson.toJson(sc));
      gsonb.setVersion(1.1);
      gson = gsonb.create();
      System.out.printf("%s%n%n", gson.toJson(sc));
      gsonb.setVersion(1.5);
      gson = gsonb.create();
      System.out.printf("%s%n%n", gson.toJson(sc));
      gsonb.setVersion(2.5);
      gson = gsonb.create();
      System.out.printf("%s%n", gson.toJson(sc));
   }
}

Listing 9-10.Versioning a Class and Its Fields

清单 9-10 呈现了一个嵌套的SomeClass,只要传递给setVersion()的版本号范围从1.0到几乎2.5,它就会被序列化。这个类提供了一个名为field的字段,只要传递给setVersion()的版本号范围从1.1到几乎1.5,这个字段就会被序列化。

编译清单 9-10 并运行生成的应用程序。您应该观察到以下输出:

null

{}

{"field":1}

{}

Null

内容

JsonSerializerJsonDeserializer接口声明的serialize()deserialize()方法分别用com.google.gson.JsonSerializationContextcom.google.gson.JsonDeserializationContext对象调用,作为它们的最终参数。这些对象提供了对特定 Java 对象执行默认序列化和默认反序列化的serialize()deserialize()方法。在处理不需要特殊处理的嵌套 Java 对象时,您会发现它们非常方便。

假设您有下面的DateEmployee类:

class Date
{
   int year;
   int month;
   int day;

   Date(int year, int month, int day)
   {
      this.year = year;
      this.month = month;
      this.day = day;
   }
}

class Employee
{
   String name;
   Date hireDate;
}

现在,假设您决定创建一个定制的序列化程序,将emp-namehire-date属性(而不是namehireDate属性)添加到生成的 JSON 对象中。因为在序列化过程中没有改变Date字段的名称或顺序,所以可以利用传递给JsonSerializerserialize()方法的上下文来处理序列化的这一部分。

以下代码片段显示了序列化Employee对象及其嵌套的Date对象的序列化程序:

class EmployeeSerializer implements JsonSerializer<Employee>
{
   @Override
   public JsonElement serialize(Employee emp, Type typeOfSrc,                                 JsonSerializationContext context)
   {
      JsonObject jo = new JsonObject();
      jo.addProperty("emp-name", emp.name);
      jo.add("hire-date", context.serialize(emp.hireDate));
      return jo;
   }
}

serialize()首先创建一个JsonObject来描述序列化的 JSON 对象。然后,它将一个以雇员姓名为值的emp-name属性添加到这个JsonObject中。因为默认序列化可以序列化hireDate字段,serialize()调用context.serialize(emp.hireDate)生成属性值。这个值和hire-date属性名被添加到从方法返回的JsonObject中。

清单 9-11 展示了演示这个serialize()方法的应用程序的源代码。

import java.lang.reflect.Type;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class GsonDemo
{

   static class Date
   {
      int year;
      int month;
      int day;

      Date(int year, int month, int day)
      {
         this.year = year;
         this.month = month;
         this.day = day;
      }
   }

   static class Employee
   {
      String name;
      Date hireDate;
   }

   public static void main(String[] args)
   {
      Employee e = new Employee();
      e.name = "John Doe";
      e.hireDate = new Date(1982, 10, 12);
      GsonBuilder gb = new GsonBuilder();
      class EmployeeSerializer implements JsonSerializer<Employee>
      {
         @Override
         public JsonElement serialize(Employee emp, Type typeOfSrc, JsonSerializationContext context)
         {
            JsonObject jo = new JsonObject();
            jo.addProperty("emp-name", emp.name);
            jo.add("hire-date", context.serialize(emp.hireDate));
            return jo;
         }
      }

      gb.registerTypeAdapter(Employee.class, new EmployeeSerializer());
      Gson gson = gb.create();
      System.out.printf("%s%n%n", gson.toJson(e));
   }
}

Listing 9-11.Leveraging a Context to Serialize a Date

编译清单 9-11 并运行生成的应用程序。您应该观察到以下输出:

{"emp-name":"John Doe","hire-date":{"year":1982,"month":10,"day":12}}

泛型支持

当您调用StringtoJson(Objectsrc)void toJson(Object src, Appendable writer)时,Gson调用src.getClass()来获取srcjava.lang.Class对象,以便它可以反射性地了解要序列化的字段。同样,当您调用一个反序列化方法,如<T> T fromJson(Stringjson,Class<T> classOfT)Gson使用传递给classOfTClass对象来帮助它反射性地构建一个结果 Java 对象。对于从非泛型类型实例化的对象,这些操作可以正常工作。但是,当从泛型类型创建对象时,可能会出现问题,因为泛型类型信息会因类型擦除而丢失。考虑下面的代码片段:

List<String> weekdays = Arrays.asList("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
String json = gson.toJson(weekdays);
System.out.printf("%s%n%n", json);
System.out.printf("%s%n%n",
                  gson.fromJson(json, weekdays.getClass()));

变量weekdays是通用类型java.util.List<String>的对象。toJson()方法调用weekdays.getClass(),并发现List作为类型。但是,它仍然成功地将weekdays序列化为下面的 JSON 对象:

["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]

反序列化不成功。当调用gson.fromJson(json, weekdays.getClass())时,这个方法抛出一个java.lang.ClassCastException类的实例。在内部,它试图将java.util.ArrayList转换为java.util.Arrays$ArrayList,但这不起作用。

这个问题的解决方案是指定正确的List<String>参数化类型(泛型类型实例),而不是从weekdays.getClass()返回的原始List类型。为此,您使用了com.google.gson.reflect.TypeToken<T>类。

TypeToken<T>表示一个通用类型T,并在运行时启用类型信息的检索,这是Gson所需要的。使用如下表达式实例化TypeToken:

Type listType = new TypeToken<List<String>>() {}.getType();

这个习惯用法定义了一个匿名的本地内部类,其继承的getType()方法将完全参数化的类型作为一个java.lang.reflect.Type对象返回。在这段代码中,返回了以下类型:

java.util.List<java.lang.String>

将得到的Type对象传递给<T> T fromJson(Stringjson,TypetypeOfT)方法,如下:

gson.fromJson(json, listType)

这个方法调用解析 JSON 对象并将其作为List<String>返回。

您可能希望使用如下表达式输出结果:

System.out.printf("%s%n%n", gson.fromJson(json, listType));

但是,您会收到一个抛出的ClassCastException,声明您不能将ArrayList强制转换为java.lang.Object[],而是观察输出。问题的解决方案是向List引入一个强制转换,如下所示:

System.out.printf("%s%n%n", (List) gson.fromJson(json, listType));

进行此更改后,您将观察到以下输出:

[Sun, Mon, Tue, Wed, Thu, Fri, Sat]

清单 9-12 展示了一个应用程序的源代码,演示了这个问题以及其他面向泛型的序列化/反序列化问题,以及如何解决它们。

import java.lang.reflect.Type;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

import com.google.gson.Gson;

import com.google.gson.reflect.TypeToken;

import java.util.Arrays;
import java.util.List;

public class GsonDemo
{
   static
   class Vehicle<T>
   {
      T vehicle;

      T get()
      {
         return vehicle;
      }

      void set(T vehicle)
      {
         this.vehicle = vehicle;
      }

      @Override
      public String toString()
      {
         System.out.printf("Class of vehicle: %s%n", vehicle.getClass());
         return "Vehicle: " + vehicle.toString();
      }
   }

   static
   class Truck
   {
      String make;
      String model;

      Truck(String make, String model)
      {
         this.make = make;
         this.model = model;
      }

      @Override
      public String toString()
      {
         return "Make: " + make + " Model: " + model;
      }
   }

   public static void main(String[] args)
   {
      Gson gson = new Gson();

      // ...

      System.out.printf("PART 1%n");
      System.out.printf("------%n%n");

      List<String> weekdays = Arrays.asList("Sun", "Mon", "Tue", "Wed", "Thu

", "Fri", "Sat");
      String json = gson.toJson(weekdays);
      System.out.printf("%s%n%n", json);
      try
      {
         System.out.printf("%s%n%n", gson.fromJson(json, weekdays.getClass()));
      }
      catch (ClassCastException cce)
      {
         cce.printStackTrace();
         System.out.println();
      }
      Type listType = new TypeToken<List<String>>() {}.getType(); System.out.printf("Type = %s%n%n", listType);
      try
      {
         System.out.printf("%s%n%n", gson.fromJson(json, listType));
      }
      catch (ClassCastException cce)
      {
         cce.printStackTrace();
         System.out.println();
      }
      System.out.printf("%s%n%n", (List) gson.fromJson(json, listType));

      // ...

      System.out.printf("PART 2%n");
      System.out.printf("------%n%n");

      Truck truck = new Truck("Ford", "F150");
      Vehicle<Truck> vehicle = new Vehicle<>();
      vehicle.set(truck);

      json = gson.toJson(vehicle);
      System.out.printf("%s%n%n", json);
      System.out.printf("%s%n%n", gson.fromJson(json, vehicle.getClass()));

      // ...

      System.out.printf("PART 3%n");
      System.out.printf("------%n%n");

      Map<String, String> map = new HashMap<String, String>()
      {
         {
            put("key", "value");
         }
      };
      System.out.printf("Map = %s%n%n", map);
      System.out.printf("%s%n%n", gson.toJson(map));
      System.out.printf("%s%n%n", gson.fromJson(gson.toJson(map),
                                                map.getClass()));

      // ...

      System.out.printf("PART 4%n");
      System.out.printf("------%n%n");

      Type vehicleType = new TypeToken<Vehicle<Truck>>() {}.getType();
      json = gson.toJson(vehicle, vehicleType);
      System.out.printf("%s%n%n", json);
      System.out.printf("%s%n%n", (Vehicle) gson.fromJson(json, vehicleType));

      Type mapType = new TypeToken<Map<String,String>>() {}.getType();
      System.out.printf("%s%n%n", gson.toJson(map, mapType));
      System.out.printf("%s%n%n", (Map) gson.fromJson(gson.toJson(map, mapType), mapType));
   }
}

Listing 9-12.Serializing and Deserializing Objects Based on Generic Types

清单 9-12 的GsonDemo类被组织成嵌套的VehicleTruck static类,后跟main()入口点方法。这种方法分为四个部分,演示问题和解决方案。下面是输出,我将在讨论main()时引用它:

PART 1
------

["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]

java.lang.ClassCastException: Cannot cast java.util.ArrayList to java.util.Arrays$ArrayList
        at java.lang.Class.cast(Class.java:3369)
        at com.google.gson.Gson.fromJson(Gson.java:766)
        at GsonDemo.main(GsonDemo.java:75)

Type = java.util.List<java.lang.String>

java.lang.ClassCastException: java.util.ArrayList cannot be cast to [Ljava.lang.Object;
        at GsonDemo.main(GsonDemo.java:86)

[Sun, Mon, Tue, Wed, Thu, Fri, Sat]

PART 2
------

{"vehicle":{"make":"Ford","model":"F150"}}

Class of vehicle: class com.google.gson.internal.LinkedTreeMap
Vehicle: {make=Ford, model=F150}

PART 3
------

Map = {key=value}

null

null

PART 4
------

{"vehicle":{"make":"Ford","model":"F150"}}

Class of vehicle: class GsonDemo$Truck
Vehicle: Make: Ford Model: F150

{"key":"value"}

{key=value}

第一部分主要关注前面讨论的List<String>例子。输出显示通过toJson()成功序列化,接着是通过gson.fromJson(json, weekdays.getClass()不成功反序列化,接着是存储在第一个创建的TypeToken实例中的类型,接着是有强制转换问题的成功反序列化,接着是没有强制转换问题的成功反序列化。

第二部分关注名为vehicleVehicle<Truck>对象的序列化和反序列化。这个通用对象通过一个gson.toJson(vehicle)调用被成功序列化。虽然您经常可以成功地将通用对象传递给toJson(Object src),但是这个方法偶尔会失败,正如我将要展示的。对gson.fromJson(json, vehicle.getClass())的后续调用试图反序列化输出,但是有一个问题:您观察到的是Vehicle: {make=Ford, model=F150}而不是Vehicle: Make: Ford Model: F150。因为指定了Vehicle而不是完整的Vehicle<Truck>通用类型,所以Vehicle类中的vehicle字段被指定为com.google.gson.internal.LinkedTreeMap而不是Truck作为其类型。

第三部分试图基于一个匿名子类java.util.HashMap序列化和反序列化一个 map。第一个null值显示toJson()没有成功:toJson()的内部map.getClass()调用返回一个GsonDemo$2引用,它没有提供对要序列化的对象的洞察。第二个null值是通过fromJson(Stringjson,Class<T> classOfT)中的nulljson得到的。

第四部分展示了如何修复第二部分和第三部分中的问题。这个部分创建了TypeToken<Vehicle<Truck>>TypeToken<Map<String,String>>对象来存储Vehicle<Truck>Map<String, String>参数化类型。这些对象然后被传递给String toJson(Object src, Type typeOfSrc)<T> T fromJson(Stringjson,TypetypeOfT)方法的type参数。(虽然gson.toJson(vehicle, vehicleType)不是必需的,因为序列化与gson.toJson(vehicle)一起工作,但是为了安全起见,您应该养成基于TypeToken实例传递一个Type对象作为第二个参数的习惯。)

Note

当指定对象(src和从classOfT派生的对象)的任何字段基于通用类型时,toJson(Object src)<T> T fromJson(String json,<T> classOfT)和类似的方法都能正常工作。唯一的规定是指定的对象不能是类属的。

类型适配器

在本章的前面,我展示了如何使用JsonSerializerJsonDeserializer(分别)将 Java 对象序列化为 JSON 字符串,反之亦然。这些接口简化了 Java 对象和 JSON 字符串之间的转换,但是增加了一个处理中间层。

中间层由将 Java 对象和 JSON 字符串转换为JsonElement的代码组成。这种转换降低了解析或创建无效 JSON 字符串的风险,但它确实需要时间来执行,这会影响性能。通过使用com.google.gson.TypeAdapter<T>类,您可以避开中间层并创建更高效的代码,其中T标识 Java 类序列化源和反序列化目标。

Note

你应该更喜欢效率更高的TypeAdapter而不是效率更低的JsonSerializerJsonDeserializer。事实上,Gson使用内部的TypeAdapter实现来处理 Java 对象和 JSON 字符串之间的转换。

TypeAdapter是一个抽象类,它声明了几个具体的方法以及下面的一对抽象方法:

  • T read( JsonReader in):读取一个 JSON 值(数组、对象、字符串、数字、布尔或null),转换成 Java 对象,返回。返回值可能是null
  • void write( JsonWriter out, T value):写一个 JSON 值(数组、对象、字符串、数字、布尔或null),传递给value

当一个 I/O 问题发生时,每个方法抛出java.io.IOException

read()write()方法分别读取 JSON 标记序列和写入 JSON 标记序列。对于read(),这些令牌的来源是具体的com.google.gson.stream.JsonReader类的一个实例。对于write(),这些令牌的目的地是具体的com.google.gson.stream.JsonWriter类。令牌由com.google.gson.stream.JsonToken枚举描述(例如BEGIN_ARRAY表示左方括号)。通过调用JsonReaderJsonWriter方法来读写它们,如下所示:

  • void beginObject():这个JsonReader方法使用 JSON 流中的下一个令牌,并断言它是一个新对象的开始。一个伴随的void endObject()方法消耗来自 JSON 流的下一个令牌,并断言它是当前对象的结尾。当出现 I/O 问题时,这两种方法都会抛出IOException
  • JsonWriter name(String name):这个JsonWriter方法对属性名进行编码,不能是发生 I/O 问题时抛出的null. IOException

创建了一个TypeAdapter子类后,通过调用我之前介绍的GsonBuilder registerTypeAdapter(Type type, Object typeAdapter)方法,实例化它并向Gson注册实例。传递给type的对象表示其对象被序列化或反序列化的类。传递给typeAdapter的对象是类型适配器实例。

清单 9-13 展示了演示类型适配器的应用程序的源代码。

import java.io.IOException;

import java.util.ArrayList;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

public class GsonDemo
{

   static
   class Country
   {
      String name;
      int population;
      String[] cities;

      Country() {}

      Country(String name, int population, String... cities)
      {
         this.name = name;
         this.population = population;
         this.cities = cities;
      }
   }

   public static void main(String[] args)
   {
      class CountryAdapter extends TypeAdapter<Country>
      {
         @Override
         public Country read(JsonReader in) throws IOException
         {
            Country c = new Country();
            List<String> cities = new ArrayList<>();
            in.beginObject();
            while (in.hasNext())
               switch (in.nextName())
               {
                  case "name":
                     c.name = in.nextString();
                              break;

                  case "population":
                     c.population = in.nextInt();
                     break;

                  case "cities":
                     in.beginArray();
                     while (in.hasNext())
                        cities.add(in.nextString());
                     in.endArray();
                     c.cities = cities.toArray(new String[0]); 

               }
            in.endObject();
            return c;
         }

         @Override
         public void write(JsonWriter out, Country c) throws IOException
         {
            out.beginObject();
            out.name("name").value(c.name);
            out.name("population").value(c.population);
            out.name("cities");
            out.beginArray();
            for (int i = 0; i < c.cities.length; i++)
               out.value(c.cities[i]);
            out.endArray();
            out.endObject();
         }
      }
      Gson gson = new GsonBuilder().
                     registerTypeAdapter(Country.class,                                         new CountryAdapter()).
                     create();

      Country c = new Country("England", 53012456 /* 2011 census */, "London", "Birmingham", "Cambridge");
      String json = gson.toJson(c);
      System.out.println(json);
      c = gson.fromJson(json, c.getClass());
      System.out.printf("Name = %s%n", c.name);
      System.out.printf("Population = %d%n", c.population);
      System.out.print("Cities = ");
      for (String city: c.cities)
         System.out.print(city + " ");
      System.out.println();
   }
}

Listing 9-13.Serializing and Deserializing a Country Object via a Type Adapter

清单 9-13 的GsonDemo类嵌套了一个Country类(它将一个国家描述为一个名称、一个人口数和一组城市名),并且还提供了一个main()入口点方法。

main()方法首先声明一个本地CountryAdapter类,该类扩展了TypeAdapter<Country>. CountryAdapter并覆盖了read()write()方法来处理序列化和反序列化任务。

read()方法首先创建一个新的Country对象,它将存储从被反序列化的 JSON 对象中读取的值(并从JsonReader参数中访问)。

在创建一个列表来存储将要读取的城市名数组之后,read()调用beginObject()来断言从令牌流中读取的下一个令牌是 JSON 对象的开始。

此时,read()进入一个while循环。当JsonReaderboolean hasNext()方法返回true时,这个循环继续:有另一个对象元素。

每个while循环迭代执行一个switch语句,调用JsonReaderString nextName()方法返回下一个令牌,这是 JSON 对象中的一个属性名。然后,它将令牌与三种可能性(namepopulationcities)进行比较,并执行相关代码来检索属性值,并将该值分配给之前创建的Country对象中的适当字段。

如果属性是name,调用JsonReaderString nextString()方法返回下一个令牌的字符串值。如果属性是 population,JsonReaderint nextInt()方法被调用来返回令牌的int值。

处理cities属性更加复杂,因为它的值是一个数组:

JsonReader’s void beginArray() method is called to signify that a new array has been detected and to consume the open square bracket token.   A while loop is entered to repeatedly obtain the next array string value and add it to the previously created cities list.   JsonReader’s void endArray() method is called to signify the end of the current array and to consume the close square bracket token.   The cities list is converted to a Java array, which is assigned to the Country object’s cities member.  

外层while循环结束后,read()调用endObject()断言从令牌流中读取的下一个令牌是当前 JSON 对象的结尾,然后返回Country对象。

write()方法有点类似于read()。它调用JsonWriterJsonWriter name(String name)方法将name指定的属性名编码成一个 JSON 属性名。同样,它调用JsonWriterJsonWriter value(long value)JsonWriter value(String value)方法将value编码为一个 JSON 数字或一个 JSON 字符串。

main()方法继续从一个GsonBuilder对象创建一个Gson对象,该对象执行registerTypeAdapter(Country.class, new CountryAdapter())来实例化CountryAdapter并用将要返回的Gson对象进行注册。Country.class表示Country对象将被序列化和反序列化。

最后,创建一个Country对象,将其序列化为一个字符串,然后反序列化为一个新的Country对象。

编译清单 9-13 并运行生成的应用程序。您应该观察到以下输出:

{"name":"England","population":53012456,"cities":["London","Birmingham","Cambridge"]}
Name = England
Population = 53012456
Cities = London Birmingham Cambridge

方便地将类型适配器与类和字段相关联

JsonAdapter注释类型与TypeAdapter Class对象参数一起使用,将TypeAdapter实例与类或字段相关联。这样做之后,您就不需要向Gson注册TypeAdapter,这样会减少一点编码。

清单 9-14 重构清单 9-13 来演示JsonAdapter

import java.io.IOException;

import java.util.ArrayList;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;

import com.google.gson.annotations.JsonAdapter;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

public class GsonDemo
{
   @JsonAdapter(CountryAdapter.class)

   static
   class Country
   {
      String name;
      int population;
      String[] cities;

      Country() {}

      Country(String name, int population, String... cities)
      {
         this.name = name;
         this.population = population;
         this.cities = cities;
      }
   }

   static
   class CountryAdapter extends TypeAdapter<Country>
   {
      @Override
      public Country read(JsonReader in) throws IOException
      {
         System.out.println("read() called");
         Country c = new Country();
         List<String> cities = new ArrayList<>();
         in.beginObject();
         while (in.hasNext())
            switch (in.nextName())
            {
               case "name":
                  c.name = in.nextString();
                           break;

               case "population":
                  c.population = in.nextInt();
                  break;

               case "cities":
                  in.beginArray();
                  while (in.hasNext())
                     cities.add(in.nextString());
                  in.endArray();
                  c.cities = cities.toArray(new String[0]);
            }
         in.endObject();
         return c;
      }

      @Override
      public void write(JsonWriter out, Country c) throws IOException
      {

         System.out.println("write() called");
         out.beginObject();
         out.name("name").value(c.name);
         out.name("population").value(c.population);
         out.name("cities");
         out.beginArray();
         for (int i = 0; i < c.cities.length; i++)
            out.value(c.cities[i]);
         out.endArray();
         out.endObject();
      }
   }

   public static void main(String[] args)
   {
      Gson gson = new Gson();

      Country c = new Country("England", 53012456 /* 2011 census */, "London", "Birmingham", "Cambridge");
      String json = gson.toJson(c);
      System.out.println(json);
      c = gson.fromJson(json, c.getClass());
      System.out.printf("Name = %s%n", c.name);
      System.out.printf("Population = %d%n", c.population);
      System.out.print("Cities = ");
      for (String city: c.cities)
         System.out.print(city + " ");
      System.out.println();
   }
}

Listing 9-14.Serializing and Deserializing a Country Object Annotated with a Type Adapter

在清单 9-14 中,我加粗了与清单 9-13 的两个本质区别:Country类型适配器类被标注为@JsonAdapter(CountryAdapter.class),并且指定了Gson gson = new Gson();而不是使用GsonBuilder对象及其create()方法。

编译清单 9-14 并运行生成的应用程序。您应该观察到以下输出:

write() called
{"name":"England","population":53012456,"cities":["London","Birmingham","Cambridge"]}
read() ca

lled
Name = England
Population = 53012456
Cities = London Birmingham Cambridge

read() calledwrite called()输出行证明Gson使用自定义类型适配器而不是其内部类型适配器。

Exercises

以下练习旨在测试你对第九章内容的理解。

Define Gson.   Identify and describe Gson’s packages.   What are the two ways to obtain a Gson object?   Identify the types for which Gson provides default serialization and deserialization.   How would you enable pretty-printing?   True or false: By default, Gson excludes transient or static fields from consideration for serialization and deserialization.   Once you have a Gson object, what methods can you call to convert between JSON and Java objects?   How do you use Gson to customize JSON object parsing?   Describe the JsonElement class.   Identify the JsonElement subclasses.   What GsonBuilder method do you call to register a serializer or deserializer with a Gson object?   What method does JsonSerializer provide to serialize a Java object to a JSON object?   What annotation types does Gson provide to simplify serialization and deserialization?   True or false: To use Expose, it’s enough to annotate a field, as in @Expose(serialize = true, deserialize = false).   What do JsonSerializationContext and JsonDeserializationContext provide?   True or false: You can call <T> T fromJson( String json, Class <T> classOfT) to deserialize any kind of object.   Why should you prefer TypeAdapter to JsonSerializer and JsonDeserializer?   Modify Listing 9-8 so that the static field named field5 is also serialized and deserialized.  

摘要

Gson 是一个小型的基于 Java 的库,用于解析和创建 JSON 对象。谷歌为自己的项目开发了 Gson,但后来从 1.0 版本开始,Gson 公开可用。

Gson 通过将 JSON 对象反序列化为 Java 对象来解析 JSON 对象。类似地,它通过将 Java 对象序列化为 JSON 对象来创建 JSON 对象。Gson 依靠 Java 的反射 API 来协助完成这些任务。

Gson 由 30 多个类和接口组成,分布在四个包中:com.google.gson(提供对主类Gson的访问);com.google.gson.annotations(提供用于 Gson 的注释类型);com.google.gson.reflect(提供用于从泛型类型获取类型信息的实用程序类);com.google.gson.stream(提供用于读写 JSON 编码值的实用程序类)。

Gson类处理 JSON 和 Java 对象之间的转换。您可以通过使用Gson()构造函数实例化这个类,或者通过使用GsonBuilder类获得一个Gson实例。

一旦有了一个Gson对象,就可以调用各种fromJson()toJson()方法在 JSON 和 Java 对象之间进行转换。因为这些方法分别依赖于 Gson 的默认反序列化和序列化机制,所以您可以通过使用JsonDeserializer<T>JsonSerializer<T>接口来自定义反序列化和序列化。

Gson 提供了额外的有用特性,包括用于简化序列化和反序列化的注释、用于自动化嵌套对象和数组序列化的上下文、对泛型的支持以及类型适配器。

第十章介绍了用于提取 JSON 值的 JsonPath。

十、用 JsonPath 提取 JSON 值

XPath 用于从 XML 文档中提取值。JsonPath 为 JSON 文档执行这项任务。本章向您介绍 JsonPath。

Note

如果您不熟悉 XPath,我建议您在阅读本章之前先阅读第五章。JsonPath 派生自 XPath。

JsonPath 是什么?

JsonPath 是一种声明式查询语言(也称为路径表达式语法),用于选择和提取 JSON 文档的属性值。例如,可以使用 JsonPath 在{"firstName": "John"}中定位"John"并返回这个值。JsonPath 基于 XPath 1.0。

JsonPath 是由 Stefan Goessner ( http://goessner.net )创建的。Goessner 还创建了 JsonPath 的基于 JavaScript 和基于 PHP 的实现。要获得完整的文档,请查看高斯纳的网站( http://goessner.net/articles/JsonPath/index.html )。

瑞典软件公司 Jayway ( www.jayway.com )随后将 JsonPath 改编为 Java。他们的 JSON path Java 版本是本章的重点。你会在 https://github.com/jayway/JsonPath 找到 Jayway 实现 JsonPath 的完整文档。

学习 JsonPath 语言

JsonPath 是一种简单的语言,具有与 XPath 相似的各种特性。这种语言用于构造路径表达式。

JsonPath 表达式以美元符号($)字符开始,它表示查询的根元素。美元符号后面是一系列子元素,通过点(.)符号或方括号([])符号分隔。例如,考虑以下 JSON 对象:

{
   "firstName": "John",
   "lastName": "Smith",
   "age": 25,
   "address":
   {
      "streetAddress": "21 2nd Street",
      "city": "New York",
      "state": "NY",
      "postalCode": "10021-3100"
   },
   "phoneNumbers":
   [
      {
         "type": "home",
         "number": "212 555-1234"
      },
      {
         "type": "office",
         "number": "646 555-4567"
      }
   ]
}

以下基于点符号的 JsonPath 表达式从前面的匿名 JSON 对象中提取电话号码(212 555-1234),该电话号码分配给匿名 JSON 对象中的number字段,该字段分配给phoneNumbers数组中的第一个元素:

$.phoneNumbers[0].number

$字符代表匿名的根 JSON 对象。最左边的点字符将对象根与phoneNumbers属性名分开,因为分配给phoneNumbers的值是一个数组。[0]语法标识分配给phoneNumbers的数组中的第一个元素。

第一个数组元素存储由"type": "home""number": "212 555-1234"属性组成的匿名对象。最右边的点字符访问这个对象的number子属性名,它被赋值为212 555-1234。该值从表达式中返回。

或者,我可以指定以下方括号符号来提取相同的电话号码:

$['phoneNumbers'][0]['number']

Jayway 文档将$标识为一个操作符,还标识了其他几个基本操作符。表 10-1 描述了这些操作符。

表 10-1。

JsonPath Basic Operators

| 操作员 | 描述 | | --- | --- | | `$` | 要查询的根元素。该运算符启动所有路径表达式。相当于 XPath 的`/`符号。 | | `@` | 过滤器谓词正在处理的当前节点。相当于 XPath 的`.`符号。 | | `*` | 通配符。适用于任何需要名称或数值的地方。 | | `..` | 深度扫描(也称为递归下降)。适用于任何需要名称的地方。相当于 XPath 的`//`符号。 | | `.`姓名 | 带点号的孩子。点相当于 XPath 的`/`符号。 | | `['`姓名`' (, '`姓名`')]` | 带括号的一个或多个孩子。 | | `[`号`(,`号`)]` | 一个或多个数组索引。 | | `[`开始`:`结束`]` | 数组切片运算符。 | | `[?(`表情`)]` | 过滤运算符。表达式的计算结果必须是布尔值。换句话说,它是一个谓语。 |

Jayway 文档还确定了几个可以在路径末端调用的函数——函数的输入是路径表达式的输出;函数输出由函数本身决定。表 10-2 描述了这些功能。

表 10-2。

JsonPath Functions

| 功能 | 描述 | | --- | --- | | `min()` | 返回一个数字数组中的最小值(作为一个`double`)。 | | `max()` | 返回数字数组中的最大值(作为`double`)。 | | `avg()` | 返回一个数字数组的平均值(作为一个`double`)。 | | `stddev()` | 返回数字数组的标准偏差值(作为`double`)。 | | `length()` | 返回数组的长度(作为一个`int`)。 |

最后,Jayway 文档确定了过滤器的各种操作符,这些操作符使用谓词(布尔表达式)来限制返回的项目列表。谓词可以使用表 10-3 中的过滤操作符来确定等式、匹配正则表达式以及测试包含性。

表 10-3。

JsonPath Filter Operators

| 操作员 | 描述 | | --- | --- | | `==` | 当左操作数等于右操作数时,返回`true`。注意`1`不等于`'1'`(也就是数字`1`和字符串`1`是两回事)。 | | `!=` | 当左操作数不等于右操作数时,返回`true`。 | | `<` | 当左操作数小于右操作数时,返回`true`。 | | `<=` | 当左操作数小于或等于右操作数时,返回`true`。 | | `>` | 当左操作数大于右操作数时,返回`true`。 | | `>=` | 当左操作数大于或等于右操作数时,返回`true`。 | | `=∼` | 当左操作数匹配右操作数指定的正则表达式时,返回`true`;比如`[?(@.name =∼ /foo.*?/i)]`。 | | `In` | 当左操作数存在于右操作数中时,返回`true`;比如`[?(@.grade in ['A', 'B'])]`。 | | `Nin` | 当左操作数不在右操作数中时,返回`true`。 |

这个表显示了作为简单谓词的@.name =∼ /foo.*?/i@.grade in ['A', 'B']。您可以通过使用逻辑 AND 运算符(&&)和逻辑 or 运算符(||)来创建更复杂的谓词。此外,在谓词中,必须用单引号或双引号将任何字符串括起来。

获取和使用 JsonPath 库

与第章 8 的 mJson 和第章 9 的 Gson 一样,可以从中央 Maven 资源库( http://search.maven.org/ )获取 JsonPath。

Note

如果你对 Maven 不熟悉,可以把它当成 Java 项目的构建工具,尽管 Maven 开发者认为 Maven 不仅仅是一个构建工具——参见 http://maven.apache.org/background/philosophy-of-maven.html

如果您熟悉 Maven,将下面的 XML 片段添加到依赖于 JsonPath 的 Maven 项目的项目对象模型(POM)文件中,您就可以开始了!(要了解 POM,请查看 https://maven.apache.org/pom.html#What_is_the_POM )。)

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.2.0</version>
</dependency>

这个 XML 片段显示了我在本章中使用的 Jayway JsonPath 的版本 2.2.0。

Note

Maven 项目依赖于其他项目是很常见的。比如我在第八章讨论的 mJson 项目,就依赖于 TestNG ( https://en.wikipedia.org/wiki/TestNG )。我在那一章中没有提到或讨论下载 TestNG,因为这个库不是正常使用所必需的。还有,我在第九章中讨论的 Gson 项目依赖于 JUnit ( https://en.wikipedia.org/wiki/JUnit )。我在那一章中没有提到或讨论下载 JUnit,因为这个库不是正常使用所必需的。

因为我目前没有使用 Maven,所以我下载了 JsonPath Jar 文件和 JsonPath 依赖的所有 Jar 文件,然后将所有这些 Jar 文件添加到我的类路径中。我完成下载任务最简单的方法是将浏览器指向 https://github.com/jayway/JsonPath/releases and download json-path-2.2.0-SNAPSHOT-with-dependencies.zip

解压 Zip 文件后,我发现了json-path-2.2.0-SNAPSHOT-with-dependencies主目录的以下子目录:

  • api:包含 JsonPath 的基于 Javadoc 的 API 文档。
  • lib:包含要添加到类路径中的 Jar 文件,以便使用 JSON path——并非在所有情况下都需要所有的 Jar 文件,但是最好将它们都包含在类路径中。
  • lib-optional:用于配置 JsonPath 的可选 Jar 文件。
  • source:包含 JsonPath API 的 Java 源代码的 Jar 文件。

Note

Jayway JsonPath 是根据 Apache 许可证版本 2.0 ( www.apache.org/licenses/ )进行许可的。

对于编译访问 JsonPath 的 Java 源代码,我发现只有json-path-2.2.0-SNAPSHOT.jar需要包含在类路径中:

javac -cp json-path-2.2.0-SNAPSHOT.jar source file

为了运行访问 JsonPath 的应用程序,我使用下面的命令行:

java -cp accessors-smart-1.1.jar;asm-5.0.3.jar;json-path-2.2.0-SNAPSHOT.jar;json-smart-2.2.1.jar;slf4j-api-1.7.16.jar;tapestry-json-5.4.0.jar;. main classfile

Tip

为了便于使用这些命令行,请将它们放在 Windows 平台上的一对批处理文件中,或者放在其他平台上的对应文件中。

探索 JsonPath 库

JsonPath 库被组织成几个包。您通常会与com.jayway.jsonpath包及其类型进行交互。在这一节中,我将专门关注这个包,同时向您展示如何从 JSON 对象中提取值并使用谓词来过滤项目。

从 JSON 对象中提取值

com.jayway. jsonpath包提供了JsonPath类作为使用 JsonPath 库的入口点。清单 10-1 介绍了这个类。

import java.util.HashMap;
import java.util.List;

import com.jayway.jsonpath.JsonPath;

public class JsonPathDemo
{
   public static void main(String[] args)
   {
      String json =
      "{" +
      "   \"store\":" +
      "   {" +
      "      \"book\":" +
      "      [" +
      "         {" +
      "            \"category\": \"reference\"," +
      "            \"author\": \"Nigel Rees\"," +
      "            \"title\": \"Sayings of the Century\"," +
      "            \"price\": 8.95" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"Evelyn Waugh\"," +
      "            \"title\": \"Sword of Honour\"," +
      "            \"price\": 12.99" +
      "         }" +
      "      ]," +
      "      \"bicycle\":" +
      "      {" +
      "         \"color\": \"red\"," +
      "         \"price\": 19.95" +
      "      }" +
      "   }" +
      "}";

      JsonPath path = JsonPath.compile("$.store.book[1]");
      HashMap books = path.read(json);
      System.out.println(books);
      List<Object> authors = JsonPath.read(json, "$.store.book[*].author");
      System.out.println(authors);
      String author = JsonPath.read(json, "$.store.book[1].author");
      System.out.println(author);
   }
}

Listing 10-1.A First Taste of JsonPath

清单 10-1 提供了一个JsonPathDemo类,其main()方法使用JsonPath类从 JSON 对象中提取值。main()首先声明一个基于字符串的 JSON 对象,并将其引用赋给变量json。然后,它调用下面的static JsonPath方法来编译 JsonPath 表达式(以提高性能),并将编译后的结果作为JsonPath对象返回:

JsonPath compile(String jsonPath, Predicate... filters)

Predicate varargs 列表允许您指定一个过滤器谓词数组,以响应jsonPath字符串中的过滤器谓词占位符(标识为?字符)。我将在本章后面演示Predicate和相关类型。

编译完$.store.book[1] JsonPath 表达式后,main()将该表达式传递给下面的JsonPath方法,该表达式标识了分配给store属性的匿名对象的book属性数组的第二个元素中的匿名对象:

<T> T read(String json) 

这个通用方法在先前编译的JsonPath实例上被调用。它接收基于字符串的 JSON 对象(分配给json)作为其参数,并将编译后的JsonPath实例中的 JsonPath 表达式应用于该参数。结果是由$.store.book[1]标识的 JSON 对象。

read()方法是通用的,因为它可以返回几种类型中的一种。在这个例子中,它返回了一个用于存储 JSON 对象属性名及其值的java.util.LinkedHashMap类的实例(java.util.Hashmap的子类)。

当您打算重用 JsonPath 表达式时,最好编译它们,这样可以提高性能。因为我不重用$.store.book[1],我可以使用JsonPathstatic read()方法之一来代替。例如,main()接下来演示了下面的read()方法:

<T> T read(String json, String jsonPath, Predicate... filters)

该方法为jsonPath参数创建一个新的JsonPath对象,并将其应用于json字符串。我忽略例子中的filters

传递给jsonPath的 JsonPath 表达式是$.store.book[*].author。这个表达式包含了用于匹配book数组中所有元素的*通配符。它返回这个数组中每个元素的author属性的值。

read()将该值作为net.minidev.json.JSONArray类的实例返回,它存储在必须包含在类路径中的json-smart-2.2.1.jar文件中。因为JSONArray扩展了java.util.ArrayList<Object>,所以将返回的对象强制转换为List<Object>是合法的。

为了进一步演示read()main()最后用 JsonPath 表达式$.store.book[1].author调用这个方法,它返回存储在book数组的第二个元素中的匿名对象的author属性的值。这次,read()返回一个java.lang.String对象。

Note

对于一般的read()方法,JsonPath自动尝试将结果转换为方法调用程序所期望的类型,比如 JSON 对象的 hashmap、JSON 数组的对象列表和 JSON 字符串的 string。

编译清单 10-1 如下:

javac -cp json-path-2.2.0-SNAPSHOT.jar JsonPathDemo.java

运行生成的应用程序,如下所示:

java -cp accessors-smart-1.1.jar;asm-5.0.3.jar;json-path-2.2.0-SNAPSHOT.jar;json-smart-2.2.1.jar;slf4j-api-1.7.16.jar;tapestry-json-5.4.0.jar;. JsonPathDemo

您应该观察到以下输出:

{category=fiction, author=Evelyn Waugh, title=Sword of Honour, price=12.99}
["Nigel Rees","Evelyn Waugh"]
Evelyn Waugh

您可能还会看到一些关于 SLF4J(Java 的简单日志外观)无法加载StaticLoggerBinder类并默认为无操作日志实现的消息。您可以安全地忽略这些消息。

使用谓词过滤项目

JsonPath 支持过滤器,用于将从 JSON 文档中提取的节点限制为与谓词(布尔表达式)指定的标准相匹配的节点。您可以使用内联谓词、过滤谓词或自定义谓词。

行内谓词

内联谓词是基于字符串的谓词。清单 10-2 向应用程序展示了源代码,展示了几个内联谓词。

import java.util.List;

import com.jayway.jsonpath.JsonPath;

public class JsonPathDemo
{
   public static void main(String[] args)
   {
      String json =
      "{" +
      "   \"store\":" +
      "   {" +
      "      \"book\":" +
      "      [" +
      "         {" +
      "            \"category\": \"reference\"," +
      "            \"author\": \"Nigel Rees\"," +
      "            \"title\": \"Sayings of the Century\"," +
      "            \"price\": 8.95" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"Evelyn Waugh\"," +
      "            \"title\": \"Sword of Honour\"," +
      "            \"price\": 12.99" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"J. R. R. Tolkien\"," +
      "            \"title\": \"The Lord of the Rings\"," +
      "            \"isbn\": \"0-395-19395-8\"," +
      "            \"price\": 22.99" +
      "         }" +
      "      ]," +
      "      \"bicycle\":" +
      "      {" +
      "         \"color\": \"red\"," +
      "         \"price\": 19.95" +
      "      }" +
      "   }" +

      "}";

      String expr = "$.store.book[?(@.isbn)].title";
      List<Object> titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$.store.book[?(@.category == 'fiction')].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$..book[?(@.author =∼ /.*REES/i)].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$..book[?(@.price > 10 && @.price < 20)].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$..book[?(@.author in ['Nigel Rees'])].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$..book[?(@.author nin ['Nigel Rees'])].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
   }
}

Listing 10-2.Demonstrating Inline Predicates

清单 10-2 的main()方法使用以下 JsonPath 表达式来缩小返回的书名字符串列表:

  • $.store.book[?(@.isbn)].title返回包含isbn属性的所有book元素的title值。
  • $.store.book[?(@.category == 'fiction')].title返回所有book元素的title值,这些元素的category属性被赋予字符串值fiction.
  • $..book[?(@.author =∼ /.*REES/i)].title返回其author属性值以rees结尾的所有book元素的title值(大小写无关紧要)。
  • $..book[?(@.price >= 10 && @.price <= 20)].title返回所有book元素的title值,这些元素的price属性值介于1020.之间
  • $..book[?(@.author in ['Nigel Rees'])].title返回所有book元素的title值,这些元素的author属性值与Nigel Rees.匹配
  • $..book[?(@.author nin ['Nigel Rees'])].title返回所有book元素的title值,这些元素的author属性值与Nigel Rees.不匹配

编译清单 10-2 并运行生成的应用程序。您应该会发现以下输出:

["The Lord of the Rings"]
["Sword of Honour","The Lord of the Rings"]
["Sayings of the Century"]
["Sword of Honour"]
["Sayings of the Century"]
["Sword of Honour","The Lord of the Rings"]

过滤谓词

过滤器谓词是一个被表达为抽象Filter类的实例的谓词,它实现了Predicate接口。

为了创建一个过滤谓词,您通常将位于Criteria类中的各种 fluent 方法( https://en.wikipedia.org/wiki/Fluent_interface )的调用链接在一起,该类还实现了Predicate,并将结果传递给FilterFilter filter( Predicate predicate)方法。

Filter filter = Filter.filter(Criteria.where("price").lt(20.00));

CriteriaCriteria where(String key) static方法返回一个Criteria对象,存储提供的key,在本例中为price。它的Criteria lt(Object o) static方法为<操作符返回一个Criteria对象,该对象标识与key的值进行比较的值。

要使用过滤器谓词,首先在路径中插入一个过滤器谓词的?占位符:

String expr = "$['store']['book'][?].title";

Note

当提供多个过滤谓词时,它们以占位符从左到右的顺序应用,其中占位符的数量必须与提供的过滤谓词的数量相匹配。您可以在一个筛选操作[?, ?]中指定多个谓词占位符;两个谓词必须匹配。

接下来,因为Filter实现了Predicate,所以您将过滤谓词传递给一个带Predicate参数的read()方法:

List<Object> titles = JsonPath.read(json, expr, filter);

对于每个book元素,read()方法在检测到 JsonPath 表达式中的?占位符时执行过滤谓词。

清单 10-3 展示了一个应用程序的源代码,演示了前面的过滤器谓词代码片段。

import java.util.List; 

import com.jayway.jsonpath.Criteria;
import com.jayway.jsonpath.Filter;
import com.jayway.jsonpath.JsonPath;

public class JsonPathDemo
{
   public static void main(String[] args)
   {
      String json =
      "{" +
      "   \"store\":" +
      "   {" +
      "      \"book\":" +
      "      [" +
      "         {" +
      "            \"category\": \"reference\"," +
      "            \"author\": \"Nigel Rees\"," +
      "            \"title\": \"Sayings of the Century\"," +
      "            \"price\": 8.95" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"Evelyn Waugh\"," +
      "            \"title\": \"Sword of Honour\"," +
      "            \"price\": 12.99" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"J. R. R. Tolkien\"," +
      "            \"title\": \"The Lord of the Rings\"," +
      "            \"isbn\": \"0-395-19395-8\"," +
      "            \"price\": 22.99" +
      "         }" +
      "      ]," +
      "      \"bicycle\":" +
      "      {" +
      "         \"color\": \"red\"," +
      "         \"price\": 19.95" +
      "      }" +
      "   }" +
      "}";

      Filter filter = Filter.filter(Criteria.where("price").lt(20.00));
      String expr = "$['store']['book'][?].title";
      List<Object> titles = JsonPath.read(json, expr, filter);
      System.out.println(titles);
   }
}

Listing 10-3.Demonstrating Filter Predicates

编译清单 10-3 并运行生成的应用程序。您应该会发现以下输出(两本书的价格都低于 20 美元):

["Sayings of the Century","Sword of Honour"]

自定义谓词

自定义谓词是从实现Predicate接口的类创建的谓词。

要创建自定义谓词,请实例化一个实现Predicate并覆盖以下方法的类:

boolean apply(Predicate.PredicateContext ctx)

PredicateContext是一个嵌套接口,其方法提供了关于调用apply()的上下文的信息。例如,Object root()返回一个对整个 JSON 文档的引用,而Object item()返回这个谓词正在评估的当前项。

apply()返回谓词值:true(项目被接受)或false(项目被拒绝)。

以下代码片段创建了一个自定义谓词,用于返回包含值超过20美元的price属性的Book元素:

Predicate expensiveBooks =
   new Predicate()
   {
      @Override
      public boolean apply(PredicateContext ctx)
      {

         String value = ctx.item(Map.class).get("price").toString();
         return Float.valueOf(value) > 20.00;
      }
   };

PredicateContext<T> T item(java.lang.Class<T> class)泛型方法将Book元素中的 JSON 对象映射到一个java.util.Map

要使用定制谓词,首先在路径中插入一个定制谓词的?占位符:

String expr = "$.store.book[?]";

接下来,将自定义谓词传递给一个采用Predicate参数的read()方法:

List<Map<String, Object>> titles = JsonPath.read(json, expr,
                                                 expensiveBooks);

对于每个book元素,read()执行与?相关联的定制谓词,并返回一个映射列表(每个接受的项目一个映射)。

清单 10-4 展示了一个应用程序的源代码,演示了前面的定制谓词代码片段。

import java.util.List;
import java.util.Map;

import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Predicate;

public class JsonPathDemo
{
   public static void main(String[] args)
   {

      String json =
      "{" +
      "   \"store\":" +
      "   {" +
      "      \"book\":" +
      "      [" +
      "         {" +
      "            \"category\": \"reference\"," +
      "            \"author\": \"Nigel Rees\"," +
      "            \"title\": \"Sayings of the Century\"," +
      "            \"price\": 8.95" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"Evelyn Waugh\"," +
      "            \"title\": \"Sword of Honour\"," +
      "            \"price\": 12.99" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"J. R. R. Tolkien\"," +
      "            \"title\": \"The Lord of the Rings\"," +
      "            \"isbn\": \"0-395-19395-8\"," +
      "            \"price\": 22.99" +
      "         }" +
      "      ]," +
      "      \"bicycle\":" +
      "      {" +
      "         \"color\": \"red\"," +
      "         \"price\": 19.95" +
      "      }" +
      "   }" +
      "}";

      Predicate expensiveBooks =
         new Predicate()
         {
            @Override
            public boolean apply(PredicateContext ctx)
            {
               String value = ctx.item(Map.class).get("price").toString();
               return Float.valueOf(value) > 20.00;
            }
         };
      String expr = "$.store.book[?]";
      List<Map<String, Object>> titles = JsonPath.read(json, expr,
                                                       expensiveBooks); 

      System.out.println(titles);
   }
}

Listing 10-4.Demonstrating Custom Predicates

编译清单 10-4 并运行生成的应用程序。您应该会发现以下输出(一本书的价格超过 20 美元):

[{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}]

Exercises

以下练习旨在测试你对第十章内容的理解。

Define JsonPath.   True or false: JsonPath is based on XPath 2.0.   Identify the operator that represents the root JSON object.   In what notations can you specify JsonPath expressions?   What operator represents the current node being processed by a filter predicate?   True or false: JsonPath’s deep scan operator (..) is equivalent to XPath’s / symbol.   What does JsonPath’s JsonPath compile(String jsonPath, Predicate... filters) static method accomplish?   What is the return type of the <T> T read(String json) generic method that returns JSON object property names and their values?   Identify the three predicate categories.   Given JSON object { "number": [10, 20, 25, 30] }, write a JsonPathDemo application that extracts and outputs the maximum (30), minimum (10), and average (21.25) values.  

摘要

JsonPath 是一种声明式查询语言(也称为路径表达式语法),用于选择和提取 JSON 文档的属性值。

JsonPath 是一种简单的语言,具有与 XPath 相似的各种特性。这种语言用于构造路径表达式。每个表达式都以$操作符开始,它标识查询的根元素,并且对应于 XPath /符号。

与第章第 8 的 mJson 和第章第 9 的 Gson 一样,您可以从中央 Maven 资源库获得 JsonPath。或者,如果您没有使用 Maven,可以下载 JsonPath Jar 文件和 JsonPath 依赖的所有 Jar 文件,然后将所有这些 Jar 文件添加到您的类路径中。

JsonPath 库被组织成几个包。您通常会与com.jayway.jsonpath包及其类型进行交互。在本章中,您专门关注了这个包,同时学习了如何从 JSON 对象中提取值并使用谓词来过滤项目。

附录 A 给出了每章练习的答案。