1、简介
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它的作用是数据标记,存储,传输,具有读写速度快、解析简单、轻量级、独立于语言和平台、具有自我描述性等特点。
像 gson、fastjson、jackson 这些都是 JSON 的解析器。而 gson 又称为 Google Gson,是 Google 发布的开源 Java 库,主要用于把 Java 对象序列化为 JSON 字符串,或把 JSON 字符串反序列化为 Java 对象。
2、JSON 语法
JSON 的语法构建于 key-value 键值对和值的有效列表(数组)之上。这两种都是常见的数据结构,大部分计算机语言都以某种形式支持它们,这使得 JSON 数据格式在同样基于这些结构的编程语言之间交换成为可能。
JSON 具有对象(Object)、数组(Array)、值(Value)、字符串(String)、数值(Number)等形式。
2.1 对象 Object
无序的键值对集合,以"{“开始,以”}“结束,在 key 后面跟一个”:“,键值对之间用”,"隔开。
{
"name": "英语",
"score": 78.3
}
2.2 值 Value
可以是双引号括起来的字符串、数值、true、false、null、对象或数组,可以嵌套。
{
"url":"https://qqe2.com",
"name": "欢迎使用JSON在线解析编辑器",
"array": {
"JSON校验": "http://jsonlint.qqe2.com/",
"Cron生成": "http://cron.qqe2.com/",
"JS加密解密": "http://edit.qqe2.com/" },
"boolean": true,
"null": null,
"number": 123,
"object": {
"a": "b",
"c": "d",
"e": "f"
}
}
2.3 数组 Array
有序的值的集合。以"[“开始,以”]“结束。值之间使用”,"分隔。
"courses": [
// 一个对象{}作为一个 value,中间用逗号隔开
{
"name": "英语",
"score": 78.3
},
{
"name": "数学",
"score": 88.3
}
]
2.4 字符串 String
是由双引号包围的任意数量 Unicode 字符的集合,使用反斜线转义。一个字符(character)即一个单独的字符串(character string)。
{
"name": "Zero",
}
2.5 数值 Number
与 C 或者 Java 的数值非常相似。除去未曾使用的八进制与十六进制格式和一些编码细节。
{
"age": 28,
}
3、Gson 基础
3.1 Gson 的基本使用
public class GsonTest {
static class GsonBean {
private int i;
private String string;
public GsonBean(int i, String string) {
this.i = i;
this.string = string;
}
@Override
public String toString() {
return "GsonBean{" +
"i=" + i +
", string='" + string + '\'' +
'}';
}
}
public static void main(String[] args) {
Gson gson = new Gson();
// 基本类型的序列化
System.out.println(gson.toJson(1)); // 1
System.out.println(gson.toJson("test")); // "test"
System.out.println(gson.toJson(new int[]{1, 2, 3})); // [1,2,3]
// 反序列化
Integer i = gson.fromJson("1", int.class);
System.out.println("i=" + i); // i=1
// 引用类型的序列化与反序列化
GsonBean gsonBean = new GsonBean(2, "test");
String json = gson.toJson(gsonBean);
System.out.println("json:" + json); // json:{"i":2,"string":"test"}
GsonBean gsonBean1 = gson.fromJson(json, GsonBean.class);
System.out.println("gsonBean1" + gsonBean1); // gsonBean1GsonBean{i=2, string='test'}
}
/**
* 将 gsonBean 序列化成 JSON 格式并写入 filePath 指定的文件中
*/
private static void writeJsonToFile(GsonBean gsonBean, String filePath) {
try (OutputStream os = new FileOutputStream(filePath);
JsonWriter jsonWriter = new JsonWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) {
Gson gson = new Gson();
gson.toJson(gsonBean, new TypeToken<GsonBean>() {
}.getType(), jsonWriter);
jsonWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
private static GsonBean readJsonObjectFromFile(String filePath) {
try (JsonReader jsonReader = new JsonReader(new InputStreamReader(new FileInputStream(filePath)))) {
Gson gson = new Gson();
return gson.fromJson(jsonReader, new TypeToken<GsonBean>() {
}.getType());
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
此外,给定一个 JSON 格式,需要熟练的将其转换成 JavaBean,如:
// 最外层是一个对象,里面有key、simpleArray、arrays、innerclass四个对象
{
// 字符串对象
"key": "value",
// [] 表示数组,数组内对象只有value没有key就是纯数组
"simpleArray": [1,2,3],
// 两组[]说明是数组嵌套,内层[]内放了一个对象,该对象对应Java的内部类
"arrays": [
[{
"arrInnerClsKey": "arrInnerClsValue",
"arrInnerClsKeyNub": 1
}]
],
// 对象嵌套,对应Java的内部类,其对象名为父对象的key,即innerclass
"innerclass": {
"name": "zero",
"age": 25,
"sex": "男"
}
}
转换成 JavaBean 是:
public class GsonBean {
private String key;
private List<Integer> simpleArray;
private List<List<ArraysBean>> arrays;
private InnerClassBean innerclass;
// getters and setters...
public static class ArraysBean {
private String arrInnerClsKey;
private int arrInnerClsKeyNub;
// getters and setters...
}
public static class InnerClassBean {
private String name;
private int age;
private String sex;
// getters and setters...
}
}
3.2 Gson 中的注解
Gson 中有一些注解,会给序列化/反序列化工作提供很大的便利。
SerializedName 注解
@SerializedName 表示序列化时,会用给定的名字作为 key:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface SerializedName {
/**
* @return 在序列化/反序列化时,返回该字段想要得到的名字
*/
String value();
/**
* @return 反序列化时,返回该字段可选的名字
*/
String[] alternate() default {};
}
示例代码:
public class MyClass {
@SerializedName("name")
String a;
@SerializedName(value = "name1", alternate = {"name2", "name3"})
String b;
String c;
public MyClass(String a, String b, String c) {
this.a = a;
this.b = b;
this.c = c;
}
}
private void test() {
MyClass target = new MyClass("v1", "v2", "v3");
Gson gson = new Gson();
String json = gson.toJson(target);
System.out.println(json); // {"name":"v1","name1":"v2","c":"v3"}
MyClass target1 = gson.fromJson("{'name1':'v1'}", MyClass.class);
assertEquals("v1", target1.b);
target1 = gson.fromJson("{'name2':'v2'}", MyClass.class);
assertEquals("v2", target1.b);
target1 = gson.fromJson("{'name3':'v3'}", MyClass.class);
assertEquals("v3", target1.b);
}
private void assertEquals(String value, String keyValue) {
assert value.equals(keyValue);
}
MyClass 中,字段 a 在序列化到 Json 中之后的名字为 name,字段 b 序列化后的名字为 name1(如果指定了多个值,会用第一个),而字段 c 则采用默认策略,序列化的名字就为变量名 c。
反序列化时,Json 中的 name1、name2、name3 都是字段 b 所指定的名字,所以它们都会被反序列化到字段 b 上。
@SerializedName 主要用途就是解决后端返回的 Json 字段名字不便建立 JavaBean 字段的情况,比如说后端给的 Json 是这样的:
{
"Type": 6,
"Result": {
"State": "1",
"Msg": "成功"
},
"Data": {
"1": 1000000001,
"2": 2800,
"3": "UPDATE",
"4": 201901021130,
"5": 2854,
"6": 2,
"7": 2.58,
}
}
Data 中的1、2、3、4、5、6、7根本无法作为 Java 字段声明,这时就可以用 @SerializedName 注解:
public class TestPojo {
@SerializedName("1")
private int totalAccount;
@SerializedName("2")
private int numIns;
@SerializedName("3")
private String type;
@SerializedName("4")
private long time;
}
Expose 注解
还有一个 @Expose 注解用来标明该字段是否参与序列化/反序列化:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Expose {
/**
* If {@code true}, the field marked with this annotation is written out in the JSON while
* serializing. If {@code false}, the field marked with this annotation is skipped from the
* serialized output. Defaults to {@code true}.
* @since 1.4
*/
public boolean serialize() default true;
/**
* If {@code true}, the field marked with this annotation is deserialized from the JSON.
* If {@code false}, the field marked with this annotation is skipped during deserialization.
* Defaults to {@code true}.
* @since 1.4
*/
public boolean deserialize() default true;
}
Since 和 Until 注解
此外还有用于版本控制的注解 @Since 和 @Until:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Since {
/**
* the value indicating a version number since this member
* or type has been present.
*/
double value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Until {
/**
* the value indicating a version number until this member
* or type should be ignored.
*/
double value();
}
@Since 表示当被标记字段的当前版本大于等于 value() 指定的值时才参与序列化/反序列化,否则忽略;@Until 表示当前被标记的作用域的当前版本小于(注意没有等于) value() 指定的值时才参与序列化/反序列化,否则忽略。
代码示例:
public class User {
private String name;
@Since(1.0)
@Until(1.3)
private String emailAddress;
@Since(1.0)
@Until(1.2)
private String password;
// 构造方法和 toString() 省略
}
name 字段会参加所有版本的序列化/反序列化,emailAddress 会参加 [1.0,1.3) 版本之间的序列化/反序列化,password 会参加 [1.0,1.2) 版本之间的序列化/反序列化。注意需要使用 GsonBuilder 指定当前版本后创建 Gson,不能使用 Gson 的空构造方法,否则 User 中使用 @Since、@Until 指定的版本将无效:
// 使用空参构造方法创建 Gson
private void testUser() {
Gson gson = new Gson();
User user = new User("Test", "xyz@test.com", "123456");
String json = gson.toJson(user);
System.out.println(json); // {"name":"Test","emailAddress":"xyz@test.com","password":"123456"}
User user1 = gson.fromJson(json, User.class);
System.out.println(user1); // User{name='Test', emailAddress='xyz@test.com', password='123456'}
}
// 使用 GsonBuilder 创建 Gson 并设置其版本号
private void testUserWithVersion() {
Gson gson1 = new GsonBuilder().setVersion(1.1).create();
User user = new User("Test", "xyz@test.com", "123456");
String json1 = gson1.toJson(user);
System.out.println(json1); // {"name":"Test","emailAddress":"xyz@test.com","password":"123456"}
User user1 = gson1.fromJson(json1, User.class);
System.out.println(user1); // User{name='Test', emailAddress='xyz@test.com', password='123456'}
Gson gson2 = new GsonBuilder().setVersion(1.2).create();
String json2 = gson2.toJson(user);
System.out.println(json2); // {"name":"Test","emailAddress":"xyz@test.com"}
User user2 = gson2.fromJson(json2, User.class);
System.out.println(user2); // User{name='Test', emailAddress='xyz@test.com', password='null'}
}
当指定 gson2 的版本号为 1.2 时,password 字段没有参与序列化/反序列化,说明 @Until 指定的边界不在参与序列化/反序列化的版本之内。
JsonAdapter 注解
Gson 中的解析使用了反射技术,由于反射会使性能受到影响,因此 Gson 提供了 @JsonAdapter 注解用来指定一个具体的 JavaBean 的解析类来代替默认的反射。源码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface JsonAdapter {
/** Either a {@link TypeAdapter} or {@link TypeAdapterFactory}, or one or both of {@link JsonDeserializer} or {@link JsonSerializer}. */
Class<?> value();
/** false, to be able to handle {@code null} values within the adapter, default value is true. */
boolean nullSafe() default true;
}
@JsonAdapter 既可以作用于类,也可以作用于字段,先看如何在类上使用。JavaBean 类上加注解 @JsonAdapter,值为代替默认的反射方式解析 Json 的类对象:
@JsonAdapter(PersonJsonAdapter.class)
public class Person {
public final String firstName, lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// toString()...
}
PersonJsonAdapter 类需要继承 TypeAdapter 并实现 write() 和 read():
public class PersonJsonAdapter extends TypeAdapter<Person> {
// implement write: combine firstName and lastName into name
@Override
public void write(JsonWriter out, Person value) throws IOException {
out.beginObject();
out.name("name");
out.value(value.firstName + " " + value.lastName);
out.endObject();
}
// implement read: split name into firstName and lastName
@Override
public Person read(JsonReader in) throws IOException {
in.beginObject();
in.nextName();
String[] nameParts = in.nextString().split(" ");
in.endObject();
return new Person(nameParts[0], nameParts[1]);
}
}
write() 其实就是执行序列化过程,把 Person 类写成 Json 字符串,read() 就是反序列化把 Json 字符串转换成 Person 对象。
测试代码:
private void testPerson() {
Person person = new Person("first", "last");
Gson gson = new Gson();
String json = gson.toJson(person);
System.out.println(json); // {"name":"first last"}
System.out.println(gson.fromJson(json, Person.class)); // Person{firstName='first', lastName='last'}
}
该注解作用于字段:
private static final class Gadget {
@JsonAdapter(UserJsonAdapter.class)
final User user;
Gadget(User user) {
this.user = user;
}
}
可以在一个字段、该字段的类型以及 GsonBuilder 中(通过 registerTypeAdapter())指定不同的类型适配器(TypeAdapter),三者的优先级由高到低是:作用于字段的注解 -> 在 GsonBuilder 上注册的适配器 -> 作用于类上的注解。
这个注解引用的类必须是 TypeAdapter 或者 TypeAdapterFactory,或者是至少实现了 JsonSerializer 或 JsonDeserializer 接口之一。使用 TypeAdapterFactory 可以使该注解委派给一个封闭的 Gson 实例。
3.3 其它实用方法
主要是 GsonBuilder 中提供的常用方法:
- setPrettyPrinting():输出 Json 的时候会按照 Json 的格式输出,默认是在一行中输出。
- registerTypeAdapter():设置自定义的解析器(需要继承 TypeAdapter),是 @JsonAdapter 的等价替换方式。
3.4 对字段空值的处理
实际开发时可能会遇到后端返回的 Json 数据某个字段的值是一个空值,类似于:
{
"name":"java",
"authors":""
}
JavaBean 如下:
public class GsonError {
private String name;
private List<AuthorsBean> authors;
public static class AuthorsBean {
private String id;
private String name;
}
}
使用默认的 Gson 进行反序列化:
public static void test2() {
String json = "{\n" +
" \"name\": \"java\",\n" +
" \"authors\": \"\"\n" +
"}";
Gson gson = new Gson();
GsonError gsonError = gson.fromJson(json, GsonError.class);
System.out.println(gsonError);
}
会抛出异常:
Caused by: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 3 column 17 path $.authors
异常原因是 authors 字段的值应该是一个数组,但是 JSON 数据给出的却是一个空的字符串,默认的 Gson 解析器无法处理这种类型不匹配的情况,因此抛出了异常。既然默认的解析器无法处理这种情况,那我们可以自定义反序列化过程对此做专门的处理,有两种方式:
1. 自定义 JsonDeserializer:实现 JsonDeserializer 接口,在处理反序列化的方法 deserialize() 内对可能出现问题的 key 进行特殊处理。
2. 自定义 TypeAdapter:继承 TypeAdapter,重写反序列化方法 read() 时对可能出现问题的 key 进行特殊处理。如果不想在 read()、write() 中手动处理空值,可以调用 TypeAdapter 的 nullSafe() 保证空值安全。
两种方式都需要通过 GsonBuilder 的 registerTypeAdapter() 注册才可生效。
自定义 JsonDeserializer
先来看第一种方式:
public static class GsonErrorDeserializer implements JsonDeserializer<GsonError> {
@Override
public GsonError deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
GsonError gsonError = new GsonError();
// 先处理 name
String name = jsonObject.get("name").getAsString();
gsonError.setName(name);
// 再处理 author
JsonElement authorElement = jsonObject.get("authors");
if (!authorElement.isJsonArray()) {
// 不是 Json 数组,视为无效值
gsonError.setAuthors(null);
} else {
AuthorsBean[] authors = context.deserialize(authorElement, AuthorsBean[].class);
gsonError.setAuthors(Arrays.asList(authors));
}
return gsonError;
}
}
public static class AuthorDeserializer implements JsonDeserializer<GsonError.AuthorsBean> {
@Override
public AuthorsBean deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String id = jsonObject.get("id").getAsString();
String name = jsonObject.get("name").getAsString();
return new GsonError.AuthorsBean(id, name);
}
}
private static void test2() {
String json = "{\n" +
" \"name\": \"java\",\n" +
" \"authors\": \"\"\n" +
"}";
Gson gson = new GsonBuilder()
.registerTypeAdapter(GsonError.class, new GsonErrorDeserializer()) // 注册 GsonErrorDeserializer
.registerTypeAdapter(GsonError.AuthorsBean.class, new AuthorDeserializer()) // 注册 AuthorDeserializer
.create();
GsonError gsonError = gson.fromJson(json, GsonError.class);
System.out.println(gsonError); // GsonError{name='java', authors=null}
}
这里是把两个 JsonDeserializer 的实现类作为 GsonError 的静态内部类了。通过 jsonObject.get(xxx) 得到的是 JsonElement 对象,可以用它的 isJsonArray() 判断当前的 Json 元素是否为一个数组。
自定义 TypeAdapter
再看第二种方式:
public static void test3() {
String json = "{\n" +
" \"name\": \"java\",\n" +
" \"authors\": \"\"\n" +
"}";
Gson gson = new GsonBuilder().registerTypeAdapter(GsonError.class,
new TypeAdapter<GsonError>() {
@Override
public void write(JsonWriter out, GsonError value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
// 写 GsonError 对象
out.beginObject(); // {
out.name("name").value(value.getName());
// 写数组
out.name("authors");
List<GsonError.AuthorsBean> authors = value.getAuthors();
if (authors == null) {
// 如果数组为空,这里有多种写法
out.value("null");
//out.nullValue();
//out.value("");
} else {
// 数组不为空,开始写数组
out.beginArray(); // [
for (int i = 0; i < authors.size(); i++) {
out.beginObject(); // {
out.name("id");
out.value(authors.get(i).getId());
out.name("name");
out.value(authors.get(i).getName());
out.endObject(); // }
}
out.endArray(); // ]
}
out.endObject(); // }
}
@Override
public GsonError read(JsonReader in) throws IOException {
// 先进行非空判断
if (in.peek() == JsonToken.NULL) {
// 如果做 doPeek() 操作拿出的 JsonToken 确实为 null,
// 就消费掉这个 JsonToken 并断言它是 null。
in.nextNull();
return null;
}
GsonError gsonError = new GsonError();
in.beginObject();
List<GsonError.AuthorsBean> authors = new ArrayList<>();
while (in.hasNext()) {
// 取下一个 key,并对 key 的值进行判断
switch (in.nextName()) {
// 如果 key 的值为 name,那么就把对应的 value 取出设置给 GsonError 的 name 字段。
case "name":
gsonError.setName(in.nextString());
break;
case "authors":
// 先对 authors 的值进行判断,如果不是 [ 开头,说明是一个无效值。
if (in.peek() != JsonToken.BEGIN_ARRAY) {
authors = null;
in.skipValue(); // 跳过这个 value
continue;
}
// 如果 authors 的值是有效的数组值,开始解析
in.beginArray();
while (in.hasNext()) {
String id = in.nextString();
String name = in.nextString();
authors.add(new GsonError.AuthorsBean(id, name));
}
in.endArray();
break;
}
}
in.endObject();
gsonError.setAuthors(authors);
return gsonError;
}
}).setPrettyPrinting().create();
GsonError gsonError = gson.fromJson(json, GsonError.class);
System.out.println(gsonError); // GsonError{name='java', authors=null}
}
操作过程已经在注释中详细标出,其实就是在 read() 读取到 authors 的值时,对值做一个判断,如果是一个有效的数组值,那么它肯定是以 [ 开始的,如果不是,而是诸如 “”、“null”、null 等形式,就直接跳过这个 value 不处理。
另外在 write() 写值的时候,有个细节,就是如果 List<GsonError.AuthorsBean> authors 为空的时候值该怎么写的问题。示例代码中给出了三种写法,得到的序列化结果分别是:
out.value(""):
{
"name": "java",
"authors": ""
}
out.nullValue():
{
"name": "java"
}
out.value("null"):
{
"name": "java",
"authors": "null"
}
可以看到当使用 out.nullValue() 时,生成的 Json 中没有对应的 key-value,即 “authors” 及其 value。
TypeAdapter 的 nullSafe()
倘若你在自定义 TypeAdapter 时不想对繁杂的空值进行处理,那么可以使用 TypeAdapter 的包装方法 nullSafe():
/**
* 此包装方法可使类型适配器具有空容忍能力。通常,需要类型适配器来处理写和读方法中的空值。下面
* 是通常的做法:
* <pre> {@code
*
* Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class,
* new TypeAdapter<Foo>() {
* public Foo read(JsonReader in) throws IOException {
* if (in.peek() == JsonToken.NULL) {
* in.nextNull();
* return null;
* }
* // read a Foo from in and return it
* }
* public void write(JsonWriter out, Foo src) throws IOException {
* if (src == null) {
* out.nullValue();
* return;
* }
* // write src as JSON to out
* }
* }).create();
* }</pre>
* 使用此方法包装你的类型适配器,可以避免这种对空值的样板处理。以下代码展示了如何
* 重写上面的例子。
* <pre> {@code
*
* Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class,
* new TypeAdapter<Foo>() {
* public Foo read(JsonReader in) throws IOException {
* // read a Foo from in and return it
* }
* public void write(JsonWriter out, Foo src) throws IOException {
* // write src as JSON to out
* }
* }.nullSafe()).create();
* }</pre>
* 注意,使用了 nullSafe 之后,就不需要在我们的类型适配器中检查空值了。
*/
public final TypeAdapter<T> nullSafe() {
return new TypeAdapter<T>() {
@Override public void write(JsonWriter out, T value) throws IOException {
// 如果是空值就不记录这条key-value
if (value == null) {
out.nullValue();
} else {
// 否则就用当前的 TypeAdapter 正常序列化
TypeAdapter.this.write(out, value);
}
}
@Override public T read(JsonReader reader) throws IOException {
// 如果读到一个空值,就消费掉它
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
// 否则就用当前的 TypeAdapter 正常反序列化
return TypeAdapter.this.read(reader);
}
};
}
参考资料:
@SerializedName注解