问题现象
线上有一段代码使用fastjson中的JSONObject.parse
方法来完成数据格式化,刚上线时没有任何问题,但过了一段时间后发现该方法偶尔有JSON解析异常的现象发生,分析日志后发现问题原因大致如下示例所示:
java
public class TestJson {
public static void main(String[] args) {
// 不报错
Object parse = JSONObject.parse("12345678");
System.out.println(parse + ", classType: " + parse.getClass());
// 报错
Object parse1 = JSONObject.parse("A12345678");
System.out.println(parse1 + ", classType: " + parse1.getClass());
}
}
输出结果如下:
java
12345678, classType: class java.lang.Integer
Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, pos 1, json : A12345678
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1436)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1322)
at com.alibaba.fastjson.JSON.parse(JSON.java:152)
at com.alibaba.fastjson.JSON.parse(JSON.java:162)
at com.alibaba.fastjson.JSON.parse(JSON.java:131)
at com.ykc.TestJson.main(TestJson.java:12)
Process finished with exit code 1
问题分析
其实我本以为上面两个方法是都应该报错的,因为正常理解JSONObject.parse
方法应该只能解析Json格式的字符串才对。
当然,为了搞清楚原因,于是就查看了一下相关源码,发现原来解析时还兼容了一些其他类型的参数,以下是关键源码的处理逻辑。
java
public final void nextToken() {
sp = 0;
for (;;) {
pos = bp;
if (ch == '/') {
skipComment();
continue;
}
if (ch == '"') {
scanString();
return;
}
if (ch == ',') {
next();
token = COMMA;
return;
}
// 支持数值
if (ch >= '0' && ch <= '9') {
scanNumber();
return;
}
// 这个是负号的意思。。。所以还支持负数
if (ch == '-') {
scanNumber();
return;
}
switch (ch) {
case '\'':
if (!isEnabled(Feature.AllowSingleQuotes)) {
throw new JSONException("Feature.AllowSingleQuotes is false");
}
scanStringSingleQuote();
return;
case ' ':
case '\t':
case '\b':
case '\f':
case '\n':
case '\r':
next();
break;
// 还有一些true,false,null这样的
case 't': // true
scanTrue();
return;
case 'f': // false
scanFalse();
return;
case 'n': // new,null
scanNullOrNew();
return;
case 'T':
case 'N': // NULL
case 'S':
case 'u': // undefined
scanIdent();
return;
case '(':
next();
token = LPAREN;
return;
case ')':
next();
token = RPAREN;
return;
case '[':
next();
token = LBRACKET;
return;
case ']':
next();
token = RBRACKET;
return;
case '{':
next();
token = LBRACE;
return;
case '}':
next();
token = RBRACE;
return;
case ':':
next();
token = COLON;
return;
case ';':
next();
token = SEMI;
return;
case '.':
next();
token = DOT;
return;
case '+':
next();
scanNumber();
return;
case 'x':
scanHex();
return;
default:
if (isEOF()) { // JLS
if (token == EOF) {
throw new JSONException("EOF error");
}
token = EOF;
pos = bp = eofPos;
} else {
if (ch <= 31 || ch == 127) {
next();
break;
}
lexError("illegal.char", String.valueOf((int) ch));
next();
}
return;
}
}
}
根据源码处理逻辑,于是进行如下实验:
java
public class TestJson {
public static void main(String[] args) {
Object parse1 = JSONObject.parse("true");
System.out.println(parse1 + ", classType: " + parse1.getClass());
Object parse2 = JSONObject.parse("1");
System.out.println(parse2 + ", classType: " + parse2.getClass());
Object parse3 = JSONObject.parse("-1");
System.out.println(parse3 + ", classType: " + parse3.getClass());
Object parse4 = JSONObject.parse("1.1");
System.out.println(parse4 + ", classType: " + parse4.getClass());
Object parse5 = JSONObject.parse("null");
System.out.println(parse5);
}
}
java
true, classType: class java.lang.Boolean
1, classType: class java.lang.Integer
-1, classType: class java.lang.Integer
1.1, classType: class java.math.BigDecimal
null
Process finished with exit code 0
所以,实际上JSONObject.parse
隐藏了不少数据类型的解析逻辑,要小心使用。
解决方式
这种问题根本原因在于协议约定方面,如果约定了数据交互格式为JSON,那就应该严格按照JSON格式来。
延伸思考
首先 ,找找自身的问题,我认为确实是使用方式有点非主流了,JSONObject.parse
就应该只用来解析JSON格式,哪怕你已经查阅源码知道它也可以兼容其他格式,例如:"12345678"
,那也请不要这样做,千万不要在这种事情上炫技(显摆)
。
其次 ,可以吐槽一下JSONObject.parse
这个方法,如此隐蔽的逻辑,在方法上居然找不到任何注释说明,这对于使用者来说非常不友好。