引言
- 使用
Gson
已经有好长时间了,但是一直停留在使用的层面上,因此在对它的好奇心尚未消失之前,我得跟它做个了断 。 - 关于
Gson
,将会有几篇长篇解读文,用于记录自己浅显的见解。
使用默认配置的Gson
- 对于
Json
数据的解析,我们总是希望能够只是通过input -> output
过程就能得到想要的结果,而不用手动去逐个解析相关字段,因为太费事了。出于这种需求,Gson
也在尽力配合,比如以下示例:
//System.out.println("");
final String jsonStr = "{'data':'this is data', 'result':{'name':'horseLai', 'age':24}}";
Gson gson = new Gson();
//输出:{"data":"hello gson","result":{"name":"horseLai","age":20}}
Data data = new Data();
User user = new User();
data.setResult(user);
data.setData("hello gson");
user.setAge(20);
user.setName("horseLai");
String json = gson.toJson(data);
System.out.println(json);
//输出:Data{data='this is data', result=User{name='horseLai', age=24}}
Data data1 = gson.fromJson(jsonStr, Data.class);
System.out.println(data1 );
static class Data {
private String data;
private User result;
// 省略set/get/toString
}
static class User {
private String name;
private int age;
// 省略set/get/toString
}
-
那么
fromJson
和toJson
发生了什么?-
我们可以思考一下,如果我们需要从
json
字符串中分离出字段名及其对应的值,然后将这些值填充赋值到Java
实体类中对应的字段中,我们要做哪些工作,如何进行?- 我的思路:
- 分解
json
字符串,直觉是使用正则,但是仔细思考一番会发现,对于简单的json
数据来说,Ok,但是对象层级深一点的话就复杂了,有没有更好的方式呢?使用栈和Map
配合的方式,遇到{
或[
,标记为对象或数组的开始,然后往栈中压入数据,遇到:
则表示读取到了一个字段的名称,接着去除''
和一些空白符号后将字段名作为Map
的key
,清空栈,接着压栈,此时要解析值,那么可以将'
',
'}]
'}
等作为值的起点或终点,标记,取值,最终将字段名和值存到Map
中,那么这种方法对于复杂多层级多类型的json
数据而言也是OK的,只要标记好是对象还是数组还是普通数据类型,然后在Map
中做好对应于Json
数据的层级即可。如果数据是HTML
的话,会比较不好判断,因为其中包含的符号可能会比较多,干扰判断。 - 分离了
json
数据中的字段名称和值,接着填充赋值实体类就容易了,反射通过data.getClass().getField("name").set(xxx)
的方式一条龙服务。
- 分解
- 我的思路:
思考过后,带着疑问与好奇,通过追踪,在
Gson#fromJson
方法中有如下代码,可以看出,当使用Gson#fromJson
将Json
数据转换到Java
实体类时,会最终调用TypeAdapter#read
方法进行数据填充操作。
public
T fromJson(String json, Class classOfT) throws JsonIOException, JsonSyntaxException { // 省略无关代码、调用层次若干.. TypeToken typeToken = (TypeToken ) TypeToken.get(typeOfT); TypeAdapter typeAdapter = getAdapter(typeToken); T object = typeAdapter.read(reader); // . . . return Primitives.wrap(classOfT).cast(object); } - 而在
Gson#fromJson
方法中有如下代码,可以看出,当我们使用Gson#fromJson
将Json
数据转换到我们的Java
实体类时,会最终调用TypeAdapter#write
方法进行数据填充操作。
public void toJson(Object src) throws JsonIOException { // 省略无关代码、调用层次若干.. // 最终会调用 Streams.write(jsonElement, writer); } public static void write(JsonElement element, JsonWriter writer) throws IOException { // 实际 TypeAdapters.JSON_ELEMENT.write(writer, element); } // 注意到它的泛型是JsonElement public static final TypeAdapter
JSON_ELEMENT = new TypeAdapter () { // 省略无关代码若干..} -
-
那么
TypeAdapter
有什么用,怎么用?-
TypeAdapter
,类型适配器,包含write``read
两个核心抽象方法,用于往数据流中写/读数据。
public abstract class TypeAdapter
{ // ... public abstract void write(JsonWriter out, T value) throws IOException; public abstract T read(JsonReader in) throws IOException; } - 在
TypeAdapters
中包含有它的若干个基本实现类,比如在Gson#toJson
中使用到的TypeAdapters.JSON_ELEMENT
,代码如下(其中省略了很多操作相同的代码,感兴趣的朋友可以自行查看源码),逻辑可看注释;
public static final TypeAdapter
JSON_ELEMENT = new TypeAdapter () { @Override public JsonElement read(JsonReader in) throws IOException { switch (in.peek()) { // 从JsonReader的栈中拿到对应的字段类型,接着去匹配、读取 case STRING: return new JsonPrimitive(in.nextString()); //. . . case BEGIN_ARRAY: // 数组和对象是比较特殊的,因为他们有自己的字段、对象或数组, // 并代表着层级的深度,因此读写时都需要注意标识好 JsonArray array = new JsonArray(); in.beginArray(); // 标识数组起点,告诉它这是个数组 while (in.hasNext()) { // 接着通过递归读取、匹配字段类型,并放到数组中 array.add(read(in)); } in.endArray(); // 告诉gson,这个数组已经读完了 return array; // . . . } // 上面分析了从流中读取并构建Json对象的过程,类似于拆箱操作,那么写过程就是装箱操作了 @Override public void write(JsonWriter out, JsonElement value) throws IOException { if (value == null || value.isJsonNull()) { out.nullValue(); } else if (value.isJsonPrimitive()) { // 写基本类型,注意String也会归类到基本类型 JsonPrimitive primitive = value.getAsJsonPrimitive(); if (primitive.isNumber()) { out.value(primitive.getAsNumber()); } //. . . } else if (value.isJsonArray()) { // 写数组,跟读一个道理啦 out.beginArray(); // 标识起点 for (JsonElement e : value.getAsJsonArray()) { write(out, e); } out.endArray(); // 标识终点 }// . . . } }; -
定制使用
TypeAdapter
- 上面分析了
TypeAdapter
的作用,并通过TypeAdapters.JSON_ELEMENT
了解了它的用法,很多时候,我们可能需要定制我们自己的TypeAdapter
,以便适应需求,那么接着走起。
final String jsonStr = "{'data':'this is data', 'result':{'name':'horseLai', 'age':24}}";
//输出:Data{data='this is data', result=User{name='horseLai', age=24}}
Gson gson = new GsonBuilder()
.registerTypeAdapter(Data.class, new MyTypeAdapter())
.enableComplexMapKeySerialization()
.serializeNulls()
.setDateFormat(DateFormat.LONG)
.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
.setPrettyPrinting()
.setVersion(1.0)
.create();
TypeAdapter adapter = gson.getAdapter(Data.class);
try {
StringReader stringReader = new StringReader(jsonStr);
Data read = adapter.read(new JsonReader(stringReader));
System.out.println(read);
} catch (IOException e) {
e.printStackTrace();
}
- 注意,自定义
TypeAdapter
时,对于要解析的这个json
数据,gson
是按照字段名->字段值逐条读的(字段名称(nextName
)也会读到,并且如果不先读名称而直接读值(next<类型>
)的话,会出错),写操作的话写值就好了,读/写有个共同的特点,那就是一定要正确标识对象和数组的起始位置和终止位置,不然肯定会出错,标识起始/终止位置的重要方法如下,JsonWriter
和JsonReader
都有:-
beginObject()
用于标识马上要写入/读取的是一个Json
对象; -
endObject()
用于标识这个Json
对象已经写/读完了; -
beginArray()
用于标识马上要写入/读取的是一个Json
数组; -
endArray()
用于标识这个Json
数组已经写/读完了;
-
// 对应于我们的`Json`数据定制
static class MyTypeAdapter extends TypeAdapter {
@Override
public void write(JsonWriter out, Data value) throws IOException
{
if (value == null) {
out.nullValue();
return;
}
out.setLenient(true);
out.beginObject();
out.name("data");
out.value(value.data);
out.name("result");
out.beginObject();
out.name("name");
out.value(value.getResult().name);
out.name("age");
out.value(value.getResult().age);
out.endObject();
out.endObject();
}
@Override
public Data read(JsonReader in) throws IOException
{
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
Data data = new Data();
User user = new User();
in.setLenient(true);
in.beginObject();
while (in.hasNext()) {
switch (in.nextName()) { // 先读取字段名称,然后逐个去匹配
case "data":
data.setData(in.nextString());
break;
case "result":
in.beginObject();
break;
case "name":
user.setName(in.nextString());
break;
case "age":
user.setAge(in.nextInt());
break;
}
}
data.setResult(user);
in.endObject();
in.endObject();
return data;
}
}
- 上面代码中有个叫
setLenient
的方法,设置这个方法有啥作用呢?可以追溯到下面源码,其中lenient
就是setLenient
设置的值,可以看出它用于决定是否对value
进行合法性检查,表示是否容忍,false
表示0
容忍,必须检查,true
表示容忍,你随意。
public JsonWriter value(Number value) throws IOException {
// . . .
String string = value.toString();
if (!lenient && (string.equals("-Infinity")
|| string.equals("Infinity") || string.equals("NaN"))) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
}
beforeValue();
out.append(string);
return this;
}
至此,可能我们会有所疑问,即便我们没有注册我们定制的
TypeAdapter
也可以通过toJson
,fromJson
解析数据,那它是通过谁解析的呢?我们继续追踪。其中
getAdapter
的伪代码如下(源码请自行查看),首先会从TokenCache
和ThreadLocal
中查找,看有没有使用过的缓存,如果有,那么返回对应的值,如果都没有,那么会遍历Gson
中的工厂集合,逐一使用工厂去创建对应于type
的TypeAdapter
,显然不匹配的话会返回null
, 如果最终实在没有找到合适的,那么只能抛异常了。
public TypeAdapter getAdapter(TypeToken type) {
if (TypeToken缓存中是否存在){
return (TypeAdapter) cached;
}
if (ThreadLocal中是否保存有){
return (FutureTypeAdapter) cached;
}
for (TypeAdapterFactory factory : factories) {
TypeAdapter candidate = factory.create(this, type);
if (candidate != null) {
call.setDelegate(candidate); // 缓存至ThreadLocal
typeTokenCache.put(type, candidate); // 缓存至TokenCache
return candidate;
}
}
// 实在没找到可以匹配的
throw new IllegalArgumentException("GSON cannot handle " + type);
// . . .
}
- 那么到谁是幕后操纵者呢?通过调试追踪,最终定位到
ReflectiveTypeAdapterFactory
,从名称上看就知道它用到了反射。它有个嵌套类Adapter
, 其read
方法源码如下,可见实际上它的逻辑就是利用反射构造一个对应于T
的对象,然后从读取Json
字段,接着去T
对象中找到相应的字段,并通过反射赋值过去。至于write
操作,原理是一致的,因此感兴趣的童鞋可以查阅源码。
@Override public T read(JsonReader in) throws IOException {
// . . .
T instance = constructor.construct();
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
field.read(in, instance);
}
// . . .
in.endObject();
return instance;
}
JsonParser
-
JsonParser
用于将json
数据转换成JsonElement
对象,它的使用方法非常简单,如下示例:
final String jsonStr = "{'data':'this is data', 'result':{'name':'horseLai', 'age':24}}";
JsonParser jsonParser = new JsonParser();
JsonObject jsonObject = jsonParser.parse(jsonStr).getAsJsonObject();
String data = jsonObject.get("data").getAsString();
JsonObject result = jsonObject.get("result").getAsJsonObject();
Data data1 = new Data();
data1.setData(data);
User user = new User();
user.setAge(result.get("age").getAsInt());
user.setName(result.get("name").getAsString());
data1.setResult(user);
//输出:Data{data='this is data', result=User{name='horseLai', age=24}}
System.out.println(data1);
- 显然,直接使用
JsonParser
需要耗费很多体力,因为我们需要手动提取json
数据中的每一个字段属性数据,对于复杂度高的json
数据解析而言,可以说相当的繁琐了。 - 那么
JsonParser
背后做了什么呢?- 我们已经知道了
JsonParser#parse
会将json
数据解析成JsonElement
对象,那么结合之前对Gson#fromJson
方法的分析,我们可以肯定的是,它也会最终通过TypeAdaper#read
方法来从Json
数据流中读取字段和值。 - 我们查看一下
JsonParser#parse
源码,可以很容易的定位到以下源码,得知他最终是通过TypeAdapters.JSON_ELEMENT
来解析成JsonElement
,至此,处理流程就与前面分析的一致了。
- 我们已经知道了
public JsonElement parse(JsonReader json) throws JsonIOException, JsonSyntaxException {
// . . .
return Streams.parse(json);
}
// Streams#parse
public static JsonElement parse(JsonReader reader) throws JsonParseException {
// . . .
reader.peek();
return TypeAdapters.JSON_ELEMENT.read(reader);
}
小结
- 至此,我们已经分析了
Gson
直接将json
数据解析成java
实体类,以及将java
实体类转换成json
的处理流程,以下是Gson#toJson
处理过程的总结,对于Gson#fromJson
而言也是类似的。
Gson#toJson
-> 建立JsonWriter.AppendableWriter 数据流
-> 创建对应于我们所需数据类型的TypeToken
-> 根据TypeToken 查找TypeToken缓存中是否已经存在已经对应的使用过的TypeAdapter
-> 根据TypeToken 查找ThreadLocal是否存在对应的使用过的TypeAdapter
-> 如果没有,则遍历所有TypeAdapterFactory并创建对应于我们所需类型的TypeAdapter,
如果还没匹配,那么也会ReflectiveTypeAdapterFactory,只要我们指定的类型是Java实体类的话,毕竟是通过反射操作。
Gson中实现的TypeAdapter几乎已经覆盖到了我们常用的所有类型,具体可查阅TypeAdapters类。
-> 在TypeAdapter.write中提取Java实体类字段值写入到流中,转换成json数据