解析Json文本——如何将Json文本转化为Java对象

Json是一种简单小巧的数据交换格式,在Web开发中获得了广泛应用。网络上有很多Json库,光用Java编写的就不下二十个之多。无论哪一个Json库都必须具有一个基本功能,就是把Json文本转换为用本语言表示的数据结构,本文就是介绍如何把Json文本一字符一字符的解析成Java对象。

      如果要问解析Json需要哪些基础知识的话,计算机科班出身的读者立马就能想到大学时学过的编译原理这门课程。解析Json就是需要利用编译原理的知识,不过Json非常简单,解析它不必使用所有的编译技术,只要了解词法分析就可以了。不了解词法分析也不要紧,Json非常简单,不用词法分析也能解析。本文根据bantouyan-json库中解析Json文本的方法编写,不需要词法分析的基础。

 

      在介绍怎样解析Json文本之前,我们先回顾一下Json的定义。如果要了解Json的详细定义可以查看RFC4627文档,或者www.json.org网站。本文只作简要的介绍。


Json对象


Json数组

 

Json Value的定义

 

Json String

 

Json Number

      上面的五幅图像分别定义了Json的对象、数组、Value、字符串与数字。Json文本由一个Json对象或Json数组构成,无论是Json对象或者Json数组,还是Json字符串或数字,都是一个Json Value。

      Json文本由构成Json的必要字符和额外的空白字符构成,解析它就要忽略额外的空白字符并将剩余部分转换为Java对象。本文按照bantouyan-json库提供的方法解析Json文本,介绍时做了一些适当的简化。

 

      bantouyan-json库的解析功能由内部类JsonTextParser提供,JsonTextParser类的定义如下:

 1 class JsonTextParser  
2 {
3 private Reader reader;
4 private int ch = -1;
5
6 public JsonTextParser(String text)throws IOException
7 {
8 StringReader strReader = new StringReader(text);
9 this.reader = strReader;
10 next();
11 }
12
13 private void next() throws IOException
14 {
15 ch = reader.read();
16 }
17 }

JsonTextParser类的核心成员有reader和ch,reader存储要解析的字符串,ch表示当前被扫描的字符,方法next()每执行一次扫描一个字符,当ch的值是-1时表明字符串扫描结束。

      在详细介绍解析算法之前,先介绍JsonTextParser类的几个关键方法,

boolean isBlankCharacter(int ch),判断字符ch是不是空白字符;void parseTailBlank(int endChar) 与void parseTailBlank(int endChar1, int endChar2),扫描并Json文本,在遇到终止字符endChar之前如果已经扫描完Json文本则抛出异常,扫描到第一个终止字符时退出该方法。

 

      Json文本的解析从方法parse()开始(一个JsonTextParser对象方法parse只能被执行一次,这样设计也许并不合理,但它是内部类,不影响使用),其代码如下:

 1 public Json parse() throws IOException, JsonException  
2 {
3 Json json = null;
4
5 while(ch != -1)
6 {
7 if(ch == '{')
8 {
9 json = parseObject();
10 parseTailBlank(-1);
11 }
12 else if(ch == '[')
13 {
14 json = parseArray();
15 parseTailBlank(-1);
16 }
17 else if(! isBlankCharacter(ch))
18 {
19 // throw exception
20 }
21
22 next();
23 }
24
25 return json;
26 }

parseTailBlank(-1)表示扫描完整个字符串前不允许出现非空白字符。Json文本只允许表示一个对象或数组,所以在字符“[”、“{”之前不允许出现非空白字符,扫描完第一个对象或数组(正常情况下只有一个)后也不允许再出现非空白字符。

 

      方法parseJsonObject负责扫描并解析一个Json对象,调用该方法时正好扫描到Json对象的开始字符'{',得到Json对象后,扫描到对象的结束字符'}'的下一个字符时退出,parseJsonObject的代码如下:

private JsonObject parseObject() throws IOException, JsonException  
{
JsonObject json = new JsonObject();
boolean needNextElement = false;

next(); //skip character '{'

while(ch != -1)
{
//还没有遇到JsonObject的子元素,扫描到'}'可直接结束
if(needNextElement == false && ch == '}') break;

if(isBlankCharacter(ch))
{
next(); //skip blank character
}
else
{
String name = parseName();
Json value = parseValue('}');
parseTailBlank(',', '}');
json.set(name, value);

if (ch == '}') //子元素后是'}',JsonObject结束
{
break;
}
else //子元素后是',',需解析下一个子元素(Name Value对)
{
next(); // skip character ','
needNextElement = true;
}
}
}

if(ch == '}')
{
next(); // skip character '}'
}
else
{
// throw exception
}

return json;
}

方法parseName 负责解析JsonObject子元素的Name部分(包括分界符':'),parseValue负责解析JsonObject子元素的Value部分。

 

      方法parseJsonArray负责扫描并解析一个Json数组,调用该方法时正好扫描到Json数组的开始字符'[',得到Json数组后,扫描到数组的结束字符']'的下一个字符时退出,parseJsonArray的代码如下:

 1 private JsonArray parseArray() throws IOException, JsonException  
2 {
3 JsonArray json = new JsonArray();
4 boolean needNextElement = false;
5
6 next(); // skip character '['
7
8 while(ch != -1)
9 {
10 //还没有遇到JsonArray的子元素,扫描到']'可直接结束
11 if(needNextElement == false && ch == ']') break;
12
13 if (isBlankCharacter(ch))
14 {
15 next(); //skip blank character
16 }
17 else
18 {
19 Json value = parseValue(']');
20 json.append(value);
21 parseTailBlank(',', ']');
22 if (ch == ']') //子元素后是']',数组结束
23 {
24 break;
25 }
26 else //子元素后是',',需解析下一个子元素
27 {
28 next(); // skip character ','
29 needNextElement = true;
30 }
31 }
32 }
33
34 if(ch == ']')
35 {
36 next(); // skip character ']'
37 }
38 else
39 {
40 // throw exception
41 }
42
43 return json;
44 }

方法parseValue负责解析Json数组的子元素。

 

     方法parseName负责扫描并解析Json对象子元素的Name部分,调用该方法时正好扫描到Name部分的第一个字符(非空白字符),得到Name后,扫描到Name部分的结束字符':'的下一个字符时退出,parseName的代码如下:

 1 private String parseName() throws IOException, JsonException  
2 {
3 String name = null;
4 name = parseString(ch);
5 parseTailBlank(':');
6
7 if(ch == ':') //Name部分正常结束
8 {
9 next(); //skip character ':'
10 }
11 else
12 {
13 // throw exception
14 }
15
16 return name;
17 }

JsonObject子元素的Name部分就是一个字符串,方法parseName扫描这个字符串和随后的结束符':' 。

 

      parseJsonObject与parseJsonArray都调用到方法parseValue, 方法parseValue负责解析一个Json Value。在Json中,Value可以是Json对象、Json数组、Json字符串、Json数字、true、false或null,它出现在Json对象子元素的Value部分和Json数组的子元素处,把这两处用到的代码归纳在一起,就是方法parseValue。调用parseValue时正好扫描到Value部分的第一个字符(可以是Value部分的第一个前导空白字符),扫描完Value部分(不包括后缀空白字符)后遇到下一个字符时退出,parseValue的代码如下:

 1 private Json parseValue(int endChar) throws IOException, JsonException  
2 {
3 Json json = null;
4
5 while(ch != -1)
6 {
7 if(isBlankCharacter(ch))
8 {
9 next(); // skip blank character
10 }
11 else if(ch == '{')
12 {
13 json = parseObject();
14 break;
15 }
16 else if(ch == '[')
17 {
18 json = parseArray();
19 break;
20 }
21 else if(ch == 't') // parse true
22 {
23 json = parseJsonConstant("true", trueAry, endChar);
24 //trueAry是一个私有静态数组,内容是{'t', 'r', 'u', 'e'}
25 break;
26 }
27 else if(ch == 'f') //parse false
28 {
29 json = parseJsonConstant("false", falseAry, endChar);
30 //falseAry是一个私有静态数组,内容是{'f', 'a', 'l', 's', 'e'}
31 break;
32 }
33 else if(ch == 'n') //parse null
34 {
35 json = parseJsonConstant("null", nullAry, endChar);
36 //nullAry是一个私有静态数组,内容是{'n', 'u', 'l', 'l'}
37 break;
38 }
39 else if(ch == '\"')
40 {
41 String str = parseString();
42 json = new JsonPrimitive(str);
43 break;
44 }
45 else if(ch == '-' || (ch >= '0' && ch<= '9'))
46 {
47 Number num = parseNumber(endChar);
48 json = new JsonPrimitive(num);
49 break;
50 }
51 else
52 {
53 // throw exception
54 }
55 }
56
57 return json;
58 }

Value部分第一个非空白字符如果是'{'则Value是一个Json对象,如果是字符'[' 则Value是一个Json数组,如果是字符'"'则肯定是Json字符串,如果是字符'-'或字符'0'到'9'则肯定是Json数字,如果是字符't'则肯定是Json常量true,如果是字符'f'则肯定是常量false,如果是字符'n'则肯定是常量null,否则肯定Json文本格式有误,因而无法解析。

 

      parseJsonObject与parseJsonArray前面都已经给予了介绍,下面介绍剩余的方法。

 

      方法parseString负责扫描并解析一个Json字符串,调用该方法时正好扫描到Json字符串的开始字符'"',扫描完Json字符串后,扫描到字符串的结束字符'"'的下一个字符时退出,parseString的代码如下:

 1 private String parseString() throws IOException, JsonException  
2 {
3 StringBuilder build = new StringBuilder();
4 next(); // skip quatorChar "
5
6 while(ch != -1)
7 {
8 if(ch == '"') break;
9
10 if(ch < 0x0020)
11 {
12 // throw exception
13 }
14 if(ch == '\\')
15 {
16 next();
17 switch(ch)
18 {
19 case '\"':
20 ch = '\"';
21 break;
22 case '\'':
23 ch = '\'';
24 break;
25 case '\\':
26 ch = '\\';
27 break;
28 case '/':
29 ch = '/';
30 break;
31 case 'b':
32 ch = '\b';
33 break;
34 case 'f':
35 ch = '\f';
36 break;
37 case 'n':
38 ch = '\n';
39 break;
40 case 'r':
41 ch = '\r';
42 break;
43 case 't':
44 ch = '\t';
45 break;
46 case 'u':
47 for(int i=0; i<4; i++)
48 {
49 next();
50 if((ch >='0' && ch <='9') || (ch >= 'a' && ch <='f')
51 || (ch>= 'A' && ch <= 'F'))
52 {
53 unicodeChar[i] = (char)ch;
54 // unicodeChar是一个私有字符数组,长度为4
55 }
56 else
57 {
58 // throw exception
59 }
60
61 }
62 ch = Integer.parseInt(new String(unicodeChar), 16);
63 break;
64 default:
65 // throw exception
66 }
67 }
68 build.append((char)ch);
69
70 next();
71 }
72
73 if(ch == '"')
74 {
75 next(); // skip quator char
76 }
77 else
78 {
79 // throw exception
80 }
81
82 return build.toString();
83 }

解析Json字符串的关键在于处理转义字符序列,要能够恰当的将转义字符序列转换为对应的字符,遇到非法转义字符时要报告错误。

       Json中的数字与通常意义上的数字字面量有点不同,它不允许有不必要的前导零,也不允许有前导符号'+'。方法parseNumber负责扫描并解析一个Json数子,调用该方法时正好扫描到Json数字的开始字符,扫描完Json数字后,扫描到数字的下一个字符时退出,parseString的代码如下:

  1 private Number parseNumber(int endChar) throws IOException, JsonException  
2 {
3 StringBuilder build = new StringBuilder();
4 boolean isInt = true;
5
6 // parse minus sign
7 if(ch == '-')
8 {
9 build.append((char)ch);
10 next();
11 }
12
13 //parse integer part
14 if(ch == '0') //begin with 0
15 {
16 build.append((char)ch);
17 next();
18 if(ch >= '0' && ch <= '9')
19 {
20 // throw exception
21 }
22 }
23 else if(ch > '0' && ch <= '9') //begin with 1..9
24 {
25 build.append((char)ch);
26 next();
27
28 while(ch != -1)
29 {
30 if(ch >= '0' && ch <= '9')
31 {
32 build.append((char)ch);
33 }
34 else
35 {
36 break;
37 }
38 next();
39 }
40 }
41 else
42 {
43 // throw exception
44 }
45
46 //parse fraction
47 if(ch == '.')
48 {
49 build.append((char)ch);
50 isInt = false;
51 next(); //skip character '.'
52
53 if(ch>='0' && ch<='9')
54 {
55 while(ch != -1)
56 {
57 if(ch>='0' && ch<='9')
58 {
59 build.append((char)ch);
60 }
61 else
62 {
63 break;
64 }
65 next();
66 }
67 }
68 else
69 {
70 // throw exception
71 }
72 }
73
74 // parse exponent
75 if(ch == 'e' || ch == 'E')
76 {
77 build.append((char)ch);
78 isInt = false;
79 next(); //skip character e
80
81 //parse plus or minus sign
82 if(ch == '+' || ch == '-')
83 {
84 build.append((char)ch);
85 next();
86 }
87
88 if(ch>='0' && ch<='9')
89 {
90 while(ch != -1)
91 {
92 if(ch>='0' && ch<='9')
93 {
94 build.append((char)ch);
95 }
96 else
97 {
98 break;
99 }
100 next();
101 }
102 }
103 else
104 {
105 // throw exception
106 }
107 }
108 if(ch != ',' && ch != endChar && !isBlankCharacter(ch))
109 {
110 // throw exception
111 }
112
113 String numStr = build.toString();
114 try
115 {
116 if(isInt)
117 {
118 return Long.parseLong(numStr);
119 }
120 else
121 {
122 return Double.parseDouble(numStr);
123 }
124 }
125 catch (NumberFormatException e)
126 {
127 // throw exception
128 }
129 }

解析Json数字时使用一个StringBuilder存储表示数字的字符串,一边扫描一遍检查数字格式,最后根据扫描到的字符串能否转换为整数决定是用Long.parseLong(String)解析还是用Double.parseDouble(String)解析。

 

      方法parseJsonConstant()负责解析Json常量true、false和null,调用时正好扫描到常量的第一个字符,退出时扫描到常量的下一个字符时退出,如果常量的下一个字符既不是分界符也不是是空白字符,那么认为所扫描到的不是一个Json常量处理。parseJsonConstant()的代码如下:

 1 private JsonPrimitive parseJsonConstant(String constName, int[] constAry, int endChar)  
2 throws IOException, JsonException
3 {
4 JsonPrimitive json = null;
5
6 for(int i=0; i<constAry.length; i++)
7 {
8 if(ch != constAry[i])
9 {
10 // throw exception
11 }
12
13 next();
14 }
15 if(ch != ',' && ch != endChar && !isBlankCharacter(ch))
16 {
17 // throw exception
18 }
19
20 json = (constAry == trueAry)? Json.trueJson:
21 (constAry == falseAry)? Json.falseJson: Json.nullJson;
22
23 return json;
24 }

参数endChar表示Json常量后除','外允许跟的另外一个分界符,参数constAry用于比较所扫描的字符,参数constName用来报告更直观的异常。

 

      本文介绍的算法对字符串只进行一次扫描,不需要回溯,因此时间复杂度为O(n)。一遍扫描的好处是不用回溯,算法简单,坏处是如果被解析的字符串格式不正确,那么只能报告所发现的第一个错误。好在大多数情况下本解析的Json文本都是正确的,而且代码一般也不负责修复Json文本的错误,所以采用一次扫描的策略是可行的。

 

转自:http://bantouyan.iteye.com/blog/968079

posted @ 2011-11-16 16:45 >3< 阅读(...) 评论(...) 编辑 收藏