引言
- 在上一篇文章中,我们主要从
Gson#from
和Gson#toJson
两个方法着手分析了Gson
在解析过程中进行了那些处理,以及走了什么流程,具体欢迎查看从使用到源码—Gson(上). -
目标:那么通过之前的分析,我们已经知道
Gson
对json
的解析最终都会通过TypeAdapter<T>
来进行read
和write
,但是至此我们对Gson
的处理并不了解,而只是了解了它经历怎样的过程,因此在本篇文章中,我们希望能够尽可能的分析、了解到Gson
对json
解析的核心,也就是JsonReader
和JsonWriter
。 - 好了,废话就不多说了,开工开工。
JsonReader
一、先用起来再谈原理
-
JsonReader
的功能:简单的说就是将Json
数据流按字段及其对应的值分解出来,并做好标记,这样调用者就能够轻易地得到Json
的字段和值了。我们不妨先通过一个实例来佐证一下,比方说我们通过下面这个ADAPTER
来将Json
数据转换成Property
类,并记录下每个字段的对应的Java
数据类型,那么我们可以这样实现一个TypeAdapter
:-
注意两个点:
JsonReader#peek()
、case BEGIN_OBJECT:
。
-
注意两个点:
private final TypeAdapter<Property> ADAPTER = new TypeAdapter<Property>() {
@Override
public void write(JsonWriter out, Property value) throws IOException {
}
@Override
public Property read(JsonReader in) throws IOException {
in.setLenient(true);
switch (in.peek()) { // 注意点一
case STRING:
return new Property<String>().setType(String.class).setValue(in.nextString());
case NUMBER:
return new Property<String>().setType(Number.class).setValue(in.nextString());
case BOOLEAN:
return new Property<Boolean>().setType(Boolean.class).setValue(in.nextBoolean());
case NULL:
Property result = new Property<Object>().setType(Object.class).setValue(null);
in.nextNull();
return result;
case BEGIN_ARRAY:
List<Property> array = new ArrayList<>();
in.beginArray();
while (in.hasNext()) {
array.add(read(in));
}
in.endArray();
return new Property< List<Property>>().setType(List.class).setValue(array);
case BEGIN_OBJECT: // 注意点二
ArrayList<Property> obj = new ArrayList<>();
in.beginObject();
while (in.hasNext()) { // 是否到了对象末尾
String s = in.nextName(); // 读取字段名
Property read = read(in); // 读取字段值
obj.add(read.setName(s)) ;
}
in.endObject();
return new Property<ArrayList<Property>>().setType(Object.class).setValue(obj);
case END_DOCUMENT:
case NAME:
case END_OBJECT:
case END_ARRAY:
default:
throw new IllegalArgumentException();
}
}
};
static class Property<T> {
String name;
T value;
Class<?> type;
}
-
为啥注意这两个点呢?在这段代码中,我们是
JsonReader
的调用者,站在调用者的角度,这两个点是理解这个功能实现的核心点,并且从中我们可以了解、推测到它的使用内部功能实现,在这个思考过程中,再次留下疑问(强忍不点开源码)。-
注意点一:
in.peek()
用于从JsonReader
中获取当前位置对应的字段类型,叫做JsonToken
,它是个枚举类。 -
注意点二:
BEGIN_OBJECT
标识一个Json
对象的起点,这点显而易见,但是需要理解的是,它在上述解析过程中起到了根基的作用,因为除了类似于['a','b','c'...]
这种只有值的数组外,都会有Json
对象的存在,因此对它的处理在整个程序中至关重要。比方说下面这串Json
数据(来自豆瓣),Json
对象可以看做是一个数据块,每个数据块中包含若干个以key<->value
形式存在的字段及值,而Json
数组就是若干个数据块的集合,显然是绕不开Json
对象。
[ { "alt": "https://movie.douban.com/celebrity/1002667/", "avatars": { "small": "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1501385708.56.jpg", "large": "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1501385708.56.jpg", "medium": "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1501385708.56.jpg" }, "name": "保罗·路德", "id": "1002667" }, { . . . } ]
-
注意点一:
- 那么我们再回头理一理这段逻辑,看似很简单的逻辑,却隐含着坑,因为从
JsonReader
获取数据,必须遵循先读字段名称,后读字段数据的顺序,多读少读、调换顺序都会出错,这点跟它的内部设计有关,具体原因我们稍后分析,这里先打个断点,留下疑问。
switch (in.peek()) {
case STRING:
return new Property<String>().setType(String.class).setValue(in.nextString());
// . . .
case BEGIN_OBJECT:
ArrayList<Property> obj = new ArrayList<>();
in.beginObject();
while (in.hasNext()) { // 是否到了对象末尾
String s = in.nextName(); // 读取字段名
Property read = read(in); // 读取字段值
obj.add(read.setName(s)) ;
}
in.endObject();
return new Property<ArrayList<Property>>().setType(Object.class).setValue(obj);
// . . .
}
- 如果不遵守原则,大概你会收到以下异常礼包:
Exception in thread "main" java.lang.IllegalArgumentException
二、该聊聊原理了
-
在上面使用分析中,我们大概都了解了作为调用者如何使用
JsonReader
了,并且知道了需要遵守它的什么规则,但是我们依然不知道这家伙到底怎么实现的,用了什么魔法,因此要扒它的衣服,看看这家伙到底是不是男孩。。在此之前不妨带着以下疑问进行:- 它是以什么方式分解
Json
数据的?跟自己当初思考的解决方案有何不同? - 为什么必须严格遵守它的规则,不然就坑我?
-
in.peek()
?难道用的栈?用栈怎么存储的?为啥不是Map
? - . . . .
- 它是以什么方式分解
-
在正式开始之前,我们可以留意一下这些标识,这样有助于我们理解
final class JsonScope {
static final int EMPTY_ARRAY = 1; // 空数组,如:[]
static final int NONEMPTY_ARRAY = 2; // 非空数组,如:[a,b]
static final int EMPTY_OBJECT = 3; // 如:{}
static final int DANGLING_NAME = 4; // 如: 'a':'b'
static final int NONEMPTY_OBJECT = 5; // 如: {'a':'b'}
static final int EMPTY_DOCUMENT = 6;
static final int NONEMPTY_DOCUMENT = 7;
static final int CLOSED = 8; // 遇到了 "];"、"};"、"]};"等
}
- 纵观全局,可以看到以下几个数组,
stack
之前看到了,用来存储是比如数组、对象、空对象等标识,而其他几个我们根据变量名可以大概推测到:-
buffer
极有可能用来缓冲Json
数据; -
pathNames
和pathIndices
两者肯定是下标索引相互对应的,并且很有可能pathIndices
记录的是pathNames
的长度
-
public class JsonReader implements Closeable {
private final char[] buffer = new char[1024];
private int[] stack = new int[32];
private int stackSize = 0;
{
stack[stackSize++] = JsonScope.EMPTY_DOCUMENT;
}
private String[] pathNames = new String[32];
private int[] pathIndices = new int[32];
// . . .
}
- OK, 那咱就正式开始吧。我们先从最显眼的
in.peek()
着手,扒开外衣发现这家伙确实是个栈,毕竟stack
都这么明显了,而从栈中取出的是类似于JsonScope.EMPTY_ARRAY
的这种东东,用于标识Json
数据当前读取到的位置上属于什么类型。-
对于
doPeek
中的逻辑,为什么如果是EMPTY_ARRAY
又赋值为stack[stackSize - 1] =NONEMPTY_ARRAY
呢?-
逻辑准备:++默认
peeked=PEEKED_NONE
,并且每次在结束一个操作,如endArray
、endObject
后都会将其初始化为PEEKED_NONE
,因此每次进行取值、标识操作时,比如begainArray()
,那么执行时必定会执行一次doPeek
进行出栈当前位置的数据类型++。 - 有了上面逻辑准备,我们可以假设一个实际场景,对于数据
[{'a':'aa'}, {'b':'bb'}]
,如果我们要从JsonReader
中解读它,该做什么?首先需要手动执行begainArray()
告诉JsonReader
这是个数组,而在begainArray()
会先假设这是个空数组EMPTY_ARRAY
,那么在下次取key:value
时必定也会doPeek
,于是发现标识为空数组,而为了能够读取到数组中的数据,就得接着假设它是个非空数组NONEMPTY_ARRAY
,于是接着doPeek
,读取数组中的数据,读取完成,即遇到]
后就结束数组读取了,并标记peeked=PEEKED_END_ARRAY
,此时我们需要手动执行endArray
告诉JsonReader
我们结束了数组读取,于是又会设置peeked=PEEKED_NONE
。 -
综上:你会发现,除了需要我们手动标记的数组、对象入口和出口,中间过程就是个来回假设、假设、验证假设的循环过程,这也就是为什么如果是
EMPTY_ARRAY
又赋值为stack[stackSize - 1] =NONEMPTY_ARRAY
的原因了,这两者都是假设,而验证假设是从if (peekStack == JsonScope.NONEMPTY_ARRAY)
开始的。对于解析Json
对象类型的话,也是这个逻辑,因此就不再单独拿出来分析了。
-
逻辑准备:++默认
-
对于
public JsonToken peek() throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
// . . .
}
int doPeek() throws IOException {
int peekStack = stack[stackSize - 1];
if (peekStack == JsonScope.EMPTY_ARRAY) { // 空数组?
stack[stackSize - 1] = JsonScope.NONEMPTY_ARRAY; // 假设它是非空数组,尝试读取看看有没有数据
} else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
int c = nextNonWhitespace(true); // Look for a comma before the next element.
switch (c) {
case ']':
return peeked = PEEKED_END_ARRAY;
// . . .
}
}else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) { // 对象?
stack[stackSize - 1] = JsonScope.DANGLING_NAME; // 假设是个类似于'a':'b'的数据
// Look for a comma before the next element.
if (peekStack == JsonScope.NONEMPTY_OBJECT) {
int c = nextNonWhitespace(true);
switch (c) {
case '}':
return peeked = PEEKED_END_OBJECT;
// . . .
}
}
} else if (peekStack == JsonScope.DANGLING_NAME) { // 是个类似于'a':'b'的数据?
stack[stackSize - 1] = JsonScope.NONEMPTY_OBJECT; // 假设是个非空对象
// ...
} else if (peekStack == JsonScope.EMPTY_DOCUMENT) { // 是个空文档?
stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT; // 假设是个非空文档
// ...
}
// ...
int c = nextNonWhitespace(true);
switch (c) {
case '[': // 数组起始位置
return peeked = PEEKED_BEGIN_ARRAY;
// . . .
}
}
// 假设遇到的是数组,需要执行这个beginArray()标记这是数组的初始位置
public void beginArray() throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek(); // 实际每次都会执行
}
if (p == PEEKED_BEGIN_ARRAY) {
push(JsonScope.EMPTY_ARRAY); // 假设是空数组
pathIndices[stackSize - 1] = 0; // 初始化计数器
peeked = PEEKED_NONE; // 初始化出栈类型,保证下一次doPeek()出栈
} else {
throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek() + locationString());
}
}
// 压栈时stackSize才会增加,意味着
private void push(int newTop) {
if (stackSize == stack.length) { // 数组扩容. . . }
stack[stackSize++] = newTop;
}
-
结合上面这段源码以及我们的实际场景,实际上我们已经理清了整个解析过程的逻辑,但是感觉还是缺些什么。那么结合上面的
stack、pathIndices、pathNames关系图.pngbeginArray
以及下面的几个方法,我们可以观察到stackSize
会在push
时自增,而在对象和数组结束时自减,stack、pathIndices、pathNames的关系如下图所示。
-
总的来说就是:
-
pathIndices
记录的是字段值的数量,也可以看作是元素读取到的位置,需要注意的是:- 对于数组,数组中的对象本身不会计入数量,只有当读取到
key:value
的value
后才会计数; - 对于对象,如果类似于
key:{}
这种有key
对应的对象,{}
对象也会算作一个value
进行计数,而其内部字段元素则依然按照key:value
进行计数。
- 对于数组,数组中的对象本身不会计入数量,只有当读取到
-
pathNames
数组中记录的是每个字段的名称,并且在读取到对象的末尾位置时,就会 将这个元素清空掉
-
-
综上: 实际上
stack
一直在复用stack[0]
这个元素,它并不会跟我们当初预想的那样保存每一个元素的数据类型,而只记录当前位置的类型,相应的pathIndices
、pathNames
也是如此。至此,就可以解释为什么我们一定要遵循JsonReader
的读取顺序了,因为这家伙只保存了当前位置上的数据类型、字段名称等信息,不按顺序读取铁定出错的,并且调换顺序也是不存在的。
public void endArray() throws IOException {
// . . .
if (p == PEEKED_END_ARRAY) {
stackSize--; // 实际上与push中的stackSize++相对应
pathIndices[stackSize - 1]++;
peeked = PEEKED_NONE;
} // . . .
}
// 读取读取字段名称
public String nextName() throws IOException {
// . . .
if (p == PEEKED_UNQUOTED_NAME) {
result = nextUnquotedValue();
} // . . .
peeked = PEEKED_NONE;
pathNames[stackSize - 1] = result; //将字段名称保存到对应于stack栈顶的pathNames中
return result;
}
// 读取字段值
public String nextString() throws IOException {
// . . .
if (p == PEEKED_UNQUOTED) {
result = nextUnquotedValue();
} else if (p == PEEKED_NUMBER) {
result = new String(buffer, pos, peekedNumberLength);
pos += peekedNumberLength;
}else if // . . .
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++; // 每个字段值读取完成之后都会自增对应于stack栈顶位置的pathIndices值,表示读取完一对key:value键值对
return result;
}
public void beginObject() throws IOException {
// . . .
if (p == PEEKED_BEGIN_OBJECT) { // 没有使用到pathIndices和pathNames
push(JsonScope.EMPTY_OBJECT);
peeked = PEEKED_NONE;
}// . . .
}
public void endObject() throws IOException {
// . . .
if (p == PEEKED_END_OBJECT) {
stackSize--; //
pathNames[stackSize] = null; // 回收stack[stackSize]对应位置上的空间
pathIndices[stackSize - 1]++;
peeked = PEEKED_NONE;
} // . . .
}
public void skipValue() throws IOException {
do { // ...
if (p == PEEKED_BEGIN_ARRAY) {
push(JsonScope.EMPTY_ARRAY);
} else if (p == PEEKED_BEGIN_OBJECT) {
push(JsonScope.EMPTY_OBJECT);
} else if (p == PEEKED_END_ARRAY) {
stackSize--;
} else if (p == PEEKED_END_OBJECT) {
stackSize--;
} // ...
peeked = PEEKED_NONE;
} while (count != 0);
pathIndices[stackSize - 1]++;
pathNames[stackSize - 1] = "null";
}
- 至于
buffer
,作为直接数据缓冲,它的目的是为了方便缓冲、读取即将到来的字符数据,减少使用StringBuilder
作为中间对象而直接创建String
对象,它的长度需要大于或等于每个token
(比如:JsonToken.STRING
)对应数据的最大长度。 - 个人觉得最能体现
buffer
的上述目的的是fillBuffer(int minimum)
方法,粗略的看可知它用于从in
数据流往buffer
数据中读取数据,但细看的话,这个方法还是比较难理解的(我比较菜~),根据官方注释可以得知,当limit - pos >= minimum
时返回true
,而当buffer
耗尽时返回false
private boolean fillBuffer(int minimum) throws IOException {
char[] buffer = this.buffer;
lineStart -= pos;
if (limit != pos) {
limit -= pos;
System.arraycopy(buffer, pos, buffer, 0, limit); // 这个为什么这样拷贝,没看明白...
} else {
limit = 0;
}
pos = 0;
int total;
while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) { // 可看作双指针往buffer的 [limit,buffer.length - limit]区域中写数据,注意到它是从数组两边往中间写的
limit += total; // 可写区域[limit,buffer.length - limit]在往中间靠拢
// if this is the first read, consume an optional byte order mark (BOM) if it exists
if (lineNumber == 0 && lineStart == 0 && limit > 0 && buffer[0] == '\ufeff') {
pos++;
lineStart++;
minimum++;
}
if (limit >= minimum) { // 一旦读取到了数据,并且字节数>= minimum 立马返回
return true;
}
}
return false;
}
-
小结:
JsonReader
作为Gson
的核心处理类,设计的很精妙,很灵活,不过也是基于基本的处理思想的优化,通过对它的分析,可以得出以下结论:-
JsonReader
内部是个基于数组(stack
)的栈实现,而stack
(缓存每个数组、对象(token)的类型)与pathNames
(缓存字段名称)和pathIndices
(缓存每个数组、对象中key:value
对的数量),三者形成基于数组索引的映射关系,而实际上它们只会缓存一个token
的状态,并且重复使用这个位置上的空间,因此当读取到下一个token
时,就会覆盖掉上一个token
的状态,这也就导致我们使用JsonReader
中读取数据时必须遵循先读字段名,后读字段数据的顺序进行,否则就会出错。 -
JsonReader
对数据的解析过程实际上是个假设、假设、验证假设的过程,而验证过程会根据Json
数据的标记符,也就是诸如{
、[
、]
、}
、:
、"
等符号进行判断token
的起始和终止点,并记录对应的状态,这是一个精致的循环过程,直到所有假设都不成立或者遇到不合法数据时才会终止循环。
-
JsonWriter
- 如果老哥你都研究到这里了,那么
JsonWriter
就比较好理解了。JsonWriter
的作用是将Java
实体类的数据转换成Json
数据,它和JsonReader
的内部数据结构基本一致,只是操作逻辑正好可看作是JsonReader
的逆向过程,因此这里将不再贴源码分析(感兴趣的老哥可以自行查看)。 - 在使用
JsonReader
时,我们必须遵循先读字段名,后读字段数据的顺序进行,而使用JsonWriter
同样需要注意这类问题,只是变成了写操作。 例如,这里我们摘一小段"从使用到源码—Gson(上)"中定制TypeAdapter
部分的代码, 可以看出,跟JsonReader
一样,我们在写操作时,同样需要通过诸如beginObject
、endObject
这类方法去标记我们当前写入的是Json
对象还是其他什么类型,此时JsonWriter
内部会将这个标识压栈或者出栈, 原理跟JsonReader
是一致的,只是换了个方向而已。
static class MyTypeAdapter extends TypeAdapter<Data> {
@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();
}
// ...
}
static class Data {
private String data;
private User result;
// 省略set/get/toString
}
static class User {
private String name;
private int age;
// 省略set/get/toString
}
总结
- 在本篇文章中,我们依然是从一个实际使用着手,比较详细地分析了
JsonReader
内部原理,从中我们可以了解到Gson
如何分解Json
数据、用什么方式解析、以及作为调用者应当遵循什么样的操作规则等。当然还有些细致的问题由于自身菜没有分析透彻的,比如buffer
的操作过程等,不过这并不影响我们理解JsonReader
的解析原理。 - 而对于
JsonWriter
,可以看作是JsonReader
的逆向操作过程,两者内部结构几乎一致,调用者都需要遵顼一定的原则才能正常解析,不同的是,JsonWriter
作为写操作,并不需要像JsonReader
那样去记录字段名称和key:value
数,因此显得更为简洁易懂。 - OK, 这篇文章可以说是拖了非常久才完结出稿,懒啊,得改得改!!
网友评论