Gson指南——使用Gson库处理Json

2014-05-22

本文翻译自:Gson User Guide

Gson是一个Java库,用来实现Json和Java对象之间的相互转换。Gson是一个托管在http://code.google.com/p/google-gson的开源项目。

Gson中主要的类是Gson,也可以使用类GsonBuilder在创建Gson对象的同时设置一些选项。
Gson对象在处理Json时不会保存任何状态,所以使用者能够很轻松的对同一个Gson对象进行多次序列化、反序列化等操作。

示例:基本使用


//Serialization
Gson gson = new Gson();
gson.toJson(1);            //==> prints 1
gson.toJson("abcd");       //==> prints "abcd"
gson.toJson(new Long(10)); //==> prints 10
int[] values = { 1 };
gson.toJson(values);       //==> prints [1]

//Deserialization
int one = gson.fromJson("1", int.class);
Integer one = gson.fromJson("1", Integer.class);
Long one = gson.fromJson("1", Long.class);
Boolean f = gson.fromJson("false", Boolean.class);
String str = gson.fromJson("\"abc\"", String.class);
String anotherStr = gson.fromJson("[\"abc\"]", String.class);

//Serialization
BagOfPrimitives obj = new BagOfPrimitives();
Gson gson = new Gson();
String json = gson.toJson(obj); 
//==> json is {"value1":1,"value2":"abc"}

示例:对象与Json之间转换


定义BagOfPrimitives类:

class BagOfPrimitives {
  private int value1 = 1;
  private String value2 = "abc";
  private transient int value3 = 3;
  BagOfPrimitives() {
    // no-args constructor
  }
}

序列化为Json:

//Serialization
BagOfPrimitives obj = new BagOfPrimitives();
Gson gson = new Gson();
String json = gson.toJson(obj); 
//==> json is {"value1":1,"value2":"abc"}

不要序列化含有循环引用的对象,否则会造成无限的递归。

反序列化:

//Deserialization
BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class); 
//==> obj2 is just like obj

处理对象时的一些细节:

  • 推荐使用私有字段(译者:可以通过反射获取私有字段的名称和值)
  • 没有必要使用标注指明哪些字段该被序列化或者反序列化。在当前类(也包括其父类)中的所有字段都默认会被序列化/反序列化。
  • 如果某字段在声明时使用了关键字transient,默认情况下不会被序列化/反序列化。
  • Gson如下处理null字段:
    • 序列化时候null字段会被跳过
    • 反序列化时,类中有但Json中没有的字段将设值为null。
  • synthetic字段不会被序列化/反序列化。
  • 在外部类(outer classes)中的内部类(inner classes)、匿名类(anonymous classes)和局部类(local classes)中的字段不会被序列化/反序列化。

嵌套类(包括内部类)的处理


Gson可以很轻松地序列化嵌套类,且能够反序列化静态的嵌套类。Gson无法自动地反序列化纯粹的内部类,是因为内部类的无参构造函数需要引用包含它的对象(即外部类的实例)。要反序列化静态类,可以将内部类静态化或者提供一个自定义的实例创造器(instance creator)。下面是一个示例:

public class A {
  public String a;

  class B {

    public String b;

    public B() {
      // No args constructor for B
    }
  }
}

上面的类B无法被Gson序列化。由于类B是一个(非静态的)内部类,Gson也无法反序列化{"b":"abc"}到类B的实例中。如果B被声明为static class B,那么Gson就能对这个字符串反序列化了。

另外一个解决方法是为B写一个实例创建器:

public class InstanceCreatorForB implements InstanceCreator<A.B> {
  private final A a;
  public InstanceCreatorForB(A a)  {
    this.a = a;
  }
  public A.B createInstance(Type type) {
    return a.new B();
  }
}

这种方法是可行的,但是不推荐。(译者表示没看懂这个实例创建器,不知道该怎么用)

示例:数组


Gson gson = new Gson();
int[] ints = {1, 2, 3, 4, 5};
String[] strings = {"abc", "def", "ghi"};

//Serialization
gson.toJson(ints);     ==> prints [1,2,3,4,5]
gson.toJson(strings);  ==> prints ["abc", "def", "ghi"]

//Deserialization
int[] ints2 = gson.fromJson("[1,2,3,4,5]", int[].class);
==> ints2 will be same as ints

Gson也支持具有复杂数据类型的多维数组。

示例:集合(Collection)


Gson gson = new Gson();
Collection<Integer> ints = Lists.immutableList(1,2,3,4,5);

//Serialization
String json = gson.toJson(ints); //==> json is [1,2,3,4,5]

//Deserialization
Type collectionType = new TypeToken<Collection<Integer>>(){}.getType();
Collection<Integer> ints2 = gson.fromJson(json, collectionType);
//ints2 is same as ints

处理集合(Collection)时的限制:

  • 可以序列化任意对象的集合,反序列化就不行了。
  • 反序列化时,集合必须是指定的泛型。

序列化/反序列化泛型


当使用toJson(obj)时,Gson调用obj.getClass()获取字段信息以在序列化中使用。类似的,也可以将对象MyClass.class作为参数传递给fromJson(json, MyClass.class)方法,这可以在在对象不是泛型的时候使用。不过,当对象是一个泛型类型的对象,由于Java中类型擦除(Type Erasure)这一机制,泛型类型信息会丢失。下面的例子将说明这一点:

class Foo<T> {
  T value;
}
Gson gson = new Gson();
Foo<Bar> foo = new Foo<Bar>();
gson.toJson(foo); // May not serialize foo.value correctly

gson.fromJson(json, foo.getClass()); // Fails to deserialize foo.value as Bar

上面的代码将value解释为Bar类型,这是因为Gson调用foo.getClass()获取类的信息,但是这种那个方法返回的是一个原始的类,即Foo.class。这意味着Gson无法知道这是一个Foo<Bar>类型的对象。

要解决这个问题,可以是为你的泛型指定正确的参数化的类型。可以使用TypeToken类做到:

Type fooType = new TypeToken<Foo<Bar>>() {}.getType();
gson.toJson(foo, fooType);
gson.fromJson(json, fooType);

fooType实际上定义了一个匿名的内部类,这个内部类含有一个可以返回全部参数化类型的getType()方法。

序列化/反序列化含有任意类型的对象的集合


有时候处理的JSON包含了混合的类型,例如:

['hello',5,{name:'GREETINGS',source:'guest'}]

对应的集合应该是:

Collection collection = new ArrayList();
collection.add("hello");
collection.add(5);
collection.add(new Event("GREETINGS", "guest"));

其中的Event类如下定义:

class Event {
  private String name;
  private String source;
  private Event(String name, String source) {
    this.name = name;
    this.source = source;
  }
}

通过Gson,你不需要做任何特殊的事情就可以序列化集合:toJson(collection)会输出令人满意的结果。

然而,通过fromJson(json, Collection.class)反序列化是不行的,这是因为Gson无法将json中的的内容与类型对应起来。Gson需要你在fromJson中提供一个通用版本的集合类型。你有三个选择:

  • 方案1:使用Gson解析器的API(低级的流解析器或者DOM解析器JsonParser)去解析数组元素,然后使用Gson.fromJson()处理每一个数组元素。这是首选的方案。
  • 方案2:为Collection.class注册一类型适配器将数组中的元素映射到合适的对象。这种方法的缺点是会使你在处理其他的集合类型时候产生不便。
  • 方案3:为MyCollectionMemberType注册一个类型适配器,在fromJson中使用Collection<MyCollectionMemberType>。只有当数组看起来像一个高级的元素或者你能够将字段类型改成Collection<MyCollectionMemberType>,这种方法才比较可行。

内置的序列化/反序列化器


Gson为常用的但是默认表示可能并不合适的类提供了序列化/反序列化器。
下面是这些类的一个列表:

  • java.net.URL,例如会序列化为字符串 http://code.google.com/p/google-gson/
  • java.net.URI,例如会序列化为字符串/p/google-gson/

自定义序列化/反序列化


有时候,Gson的默认实现并不是你想要的,这在处理一些类库时(例如DateTime)时比较常见。

Gson允许你注册自定义的序列化/反序列化器。要这样做的话,你需要实现以下几个部分:

  • Json序列化器:需要为一个对象自定义序列化
  • Json反序列化器:需要为一个类型自定义反序列化
  • 类创建器:如果存在无参构造函数或者已经注册了一个反序列化器,就不需要了。
GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(MyType2.class, new MyTypeAdapter());
gson.registerTypeAdapter(MyType.class, new MySerializer());
gson.registerTypeAdapter(MyType.class, new MyDeserializer());
gson.registerTypeAdapter(MyType.class, new MyInstanceCreator());

registerTypeAdapter会检查类型适配器是否实现了多个接口,并为这些接口注册类型适配器。

写一个序列化器

下面是一个为DateTime自定义序列化器的示例:

private class DateTimeSerializer implements JsonSerializer<DateTime> {
  public JsonElement serialize(DateTime src, Type typeOfSrc, JsonSerializationContext context) {
    return new JsonPrimitive(src.toString());
  }
}

Gson在序列化DateTime实例时会调用toJson()

写一个反序列化器

下面的示例是讲如何写一个DateTime类的反序列化器:

private class DateTimeDeserializer implements JsonDeserializer<DateTime> {
  public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException {
    return new DateTime(json.getAsJsonPrimitive().getAsString());
  }
}

当Gson需要将一个JSON字符串发反序列化为DateTime对象时,会调用 fromJson()

对于序列化器/反序列化器,应注意:

很多情况下,你想要注册一个处理程序将泛型与一个原始类型对应起来,

  • 例如,假如你有一个叫做Id的类来表示和转换Id
  • Id<T>类型中,所有的泛型拥有相同的序列化器,这个序列化器就是输出id的值
  • 反序列化器很像,但并不会完全一样。例如要返回一个Id<T>对象,需要调用new Id(Class<T>, String)

Gson支持注册一个处理程序,你也可以为指定的泛型注册指定的处理程序。toJsonfromJsonType参数包含了泛型信息以帮助你写出一个可以将所有的泛型与同一个原始类型对应起来的处理程序。

写一个实例创建器


在反序列化一个对象时,Gson需要创建一个类的实例。在序列化/反序列化时具有良好表现的类是指这个类拥有一个无参构造函数。通常,当处理一个类库中没有无参构造函数的类时,需要使用实例创建器。

实例创建器示例

private class MoneyInstanceCreator implements InstanceCreator<Money> {
  public Money createInstance(Type type) {
    return new Money("1000000", CurrencyCode.USD);
  }
}

参数化类型的实例创建器

有时候要实例化的类型会是一个参数化类型。总的来说,由于真正的实例是一个原始类型,所以这不是什么问题。下面是一个示例:

class MyList<T> extends ArrayList<T> {
}

class MyListInstanceCreator implements InstanceCreator<MyList<?>> {
    @SuppressWarnings("unchecked")
  public MyList<?> createInstance(Type type) {
    // No need to use a parameterized list since the actual instance will have the raw type anyway.
    return new MyList();
  }
}

不过,有时你需要基于真正的参数化类型来创建实例。在这种情况下,你可以将类型参数传递给createInstance方法。下面是一个例子:

public class Id<T> {
  private final Class<T> classOfId;
  private final long value;
  public Id(Class<T> classOfId, long value) {
    this.classOfId = classOfId;
    this.value = value;
  }
}

class IdInstanceCreator implements InstanceCreator<Id<?>> {
  public Id<?> createInstance(Type type) {
    Type[] typeParameters = ((ParameterizedType)type).getActualTypeArguments();
    Type idType = typeParameters[0]; // Id has only one parameterized type T
    return Id.get((Class)idType, 0L);
  }
}

在上面的示例中,如果没有将真正的类型传递给参数化类型,Id类的实例是无法创建的。通过给方法传递参数type,我们才得以解决这个问题。这里,type对象可以看做是Id<Foo>的Java参数化类型的表示,相应的实例应该被绑定到Id<Foo>。由于类Id只有一个参数化类型的参数T,我们使用getActualTypeArgument()返回的类型数组的第0个元素,在这个例子中就是Foo.class

紧凑的输出 VS 优美的输出

Gson中Json默认的输出是紧凑的JSON格式。也就是说在JSON中没有多余的空白符。所以在JSON的输出中字段名和字段值之间、字段之间、数组元素之间是没有空白的。另外,null字段不会被输出(注意:在集合和数组对象中null会被保留的)。

如果要输出的优美些,你需要使用GsonBuilder对Gson的实例进行配置。JsonFormatter不存在于公有API中,所以客户端无法配置默认的输出设置。现在我们只提供了JsonPrintFormatter,其默认情况下每行80个字符,缩进使用2个字符,右边距是4个字符。

下面的示例展示了如何让Gson实例使用JsonPrintFormatter,而不是使用默认的JsonCompactFormatter

Gson gson = new GsonBuilder().setPrettyPrinting().create();
String jsonOutput = gson.toJson(someObject);

空对象


在Gson的默认实现中,null对象是被忽略的。这可以让输出格式(既可以认为是序列化的结果)更加紧密;不过客户端必须为其定义一个默认的值,以使得JSON能够正常的反序列化。

如果要让Gson实例可以序列化null,可以:

Gson gson = new GsonBuilder().serializeNulls().create();

注意,当序列化null的时,会在JsonElement结构中添加一个JsonNull元素。因此,我们可以可以在自定义的序列化器/反序列化器中使用这个对象(gson)。

下面是一个例子:

public class Foo {
  private final String s;
  private final int i;

  public Foo() {
    this(null, 5);
  }

  public Foo(String s, int i) {
    this.s = s;
    this.i = i;
  }
}

Gson gson = new GsonBuilder().serializeNulls().create();
Foo foo = new Foo();
String json = gson.toJson(foo);
System.out.println(json);

json = gson.toJson(null);
System.out.println(json);

//======== OUTPUT ========
//{"s":null,"i":5}
//null

版本支持


可以使用@Since标注来维护同一个对象的多个版本。这个标注可以用在类和字段上,将来也会支持用在方法上。为了使用这个特性,你需要配置Gson实例,让其忽略大于某个版本号的字段和对象。如果没有在Gson对象中设置版本,序列化/反序列化时会使用所有的字段和类。

public class VersionedClass {
  @Since(1.1) private final String newerField;
  @Since(1.0) private final String newField;
  private final String field;

  public VersionedClass() {
    this.newerField = "newer";
    this.newField = "new";
    this.field = "old";
  }
}

VersionedClass versionedObject = new VersionedClass();
Gson gson = new GsonBuilder().setVersion(1.0).create();
String jsonOutput = gson.toJson(someObject);
System.out.println(jsonOutput);
System.out.println();

gson = new Gson();
jsonOutput = gson.toJson(someObject);
System.out.println(jsonOutput);

//======== OUTPUT ========
//{"newField":"new","field":"old"}

//{"newerField":"newer","newField":"new","field":"old"}

从序列化/反序列化中排除字段


Gson支持使用很多方法来去除类、字段、字段类型。如果下面的方法无法满足你的需求,可以使用自定义序列化/反序列化器的方法。

Java Modifier Exclusion

默认情况下,如果将一个字段声明为transient,这个字段就会被排除。另外,如果一个字段被声明为static,默认情况下这个字段也会被排除。如果要包含某些声明为transient的字段,你可以这样做:

import java.lang.reflect.Modifier;

Gson gson = new GsonBuilder()
    .excludeFieldsWithModifiers(Modifier.STATIC)
    .create();

注意,在excludeFieldsWithModifiers方法中,你可以使用任意数量的Modifier常量。例如:

Gson gson = new GsonBuilder()
    .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT, Modifier.VOLATILE)
    .create();

使用@Expose字段排除

这个特性允许你在类中标记特定的字段使其在序列化/反序列化中不被排除/被排除。要使用这个标注,你应该使用new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create()创建Gson。Gson实例会排除类中所有没被@Expose标注的字段。

用户定义排除策略

如果上面的排除方法无法满足需求,你也可以自定义自己的排除策略。更多内容,可以参考ExclusionStrategy JavaDoc

下面的例子展示了如何排除使用了@Foo标注的字段,排除String类的顶级类型或者声明的字段类型:

  @Retention(RetentionPolicy.RUNTIME)
  @Target({ElementType.FIELD})
  public @interface Foo {
    // Field tag only annotation
  }

  public class SampleObjectForTest {
    @Foo private final int annotatedField;
    private final String stringField;
    private final long longField;
    private final Class<?> clazzField;

    public SampleObjectForTest() {
      annotatedField = 5;
      stringField = "someDefaultValue";
      longField = 1234;
    }
  }

  public class MyExclusionStrategy implements ExclusionStrategy {
    private final Class<?> typeToSkip;

    private MyExclusionStrategy(Class<?> typeToSkip) {
      this.typeToSkip = typeToSkip;
    }

    public boolean shouldSkipClass(Class<?> clazz) {
      return (clazz == typeToSkip);
    }

    public boolean shouldSkipField(FieldAttributes f) {
      return f.getAnnotation(Foo.class) != null;
    }
  }

  public static void main(String[] args) {
    Gson gson = new GsonBuilder()
        .setExclusionStrategies(new MyExclusionStrategy(String.class))
        .serializeNulls()
        .create();
    SampleObjectForTest src = new SampleObjectForTest();
    String json = gson.toJson(src);
    System.out.println(json);
  }

//======== OUTPUT ========
//{"longField":1234}

JSON字段命名的支持

Gson的一些预定义的字段命名策略,可以将标准的Java字段名称(也就是驼峰命名法,例如sampleFieldNameInJava)转换成一个Json的字段名(也就是sample_field_name_in_java或者SampleFieldNameInJava)。更多信息,可以参考FieldNamingPolicy

Gson也有一个基于标注的策略让客户端自定义字段的名称。这个策略下,如果提供了一个非法的字段名作为标注的值,会使Gson抛出Runtime异常。

下面的示例展示了如何使用这两种Gson命名策略:

private class SomeObject {
  @SerializedName("custom_naming") private final String someField;
  private final String someOtherField;

  public SomeObject(String a, String b) {
    this.someField = a;
    this.someOtherField = b;
  }
}

SomeObject someObject = new SomeObject("first", "second");
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
String jsonRepresentation = gson.toJson(someObject);
System.out.println(jsonRepresentation);

//======== OUTPUT ========
//{"custom_naming":"first","SomeOtherField":"second"}

如果要自定义名称,可以使用@SerializedName标注。

在序列化器和反序列化器之间共享状态


有时你会需要在序列化器和反序列化器之间共享状态,你可以使用下面的三个方法达到目的:

  • 在一个静态字段中存储共享状态
  • 将序列化/反序列化器声明为一个父类型的内部类,然后使用父类型的实例的字段存储共享状态
  • 使用Java中的ThreadLocal

前两种方法不是线程安全的,第三种是。


除了Gson的对象模型和数据绑定,你可以使用Gson从流中读取数据或者向流中写入数据。具体信息,可以参考Streaming

译者补充


在Java中,关键字 transient 用来声明一个字段不应被序列化。

关于synthetic字段,可以参考What are synthetic fields in Java?

关于内部类,可以参考Inner Class Example

关于匿名类,可以参考Anonymous Classes

关于局部类,可以参考Local Classes

Java泛型的处理几乎都在编译器中进行,编译器生成的bytecode是不包涵泛型信息的,泛型类型信息将在编译处理时被擦除,这个过程即类型擦除。(摘自Java泛型-类型擦除

关于ThreadLocal,这两篇文章很不错:Java Thread Local – How to use and code sampleA Painless Introduction to Java’s ThreadLocal Storage

Converting JSON to Java中有一个关于Gson的很不错的示例。

在GSON库中还有一个很实用的JsonParser类。下面是一个示例:

package test;

import com.google.gson.JsonParser;
import com.google.gson.JsonElement;
public class LearnJSON {
    public static void main(String[] args) {
        String json = "{\"name\":\"reiz\"}";   
        JsonElement root = new JsonParser().parse(json);
        String value1 = root.getAsJsonObject().get("name").getAsString();
        System.out.println(value1);
    }
}

关于JsonParser,可以参考灵活组装Json的数据使用Gson的JsonParser和JsonReader解析Json详解例子JSON parsing using Gson for java

笔者无心在翻译上花费太大精力,另外也可能在正文中加入笔者的一些想法,有不当处还请指正。

( 完 )