不推荐使用Json-lib的n个理由

Json-lib, 包名以net.sf.json开头,是一个Java中常用的用于将JSON[JSON]和Java中的对象进行相互转换的库。出于以下几点原因,我不推荐使用这个库,排名不分先后。

Json-lib已经过时了

Json-lib的上一个版本是在2010年[Json-lib::Changelog]发布的,距今已经有8年之久。有的人可能会觉得这样的库一定会久经考验,非常稳定,不会出错。事实恰恰相反。在下文中我会阐述这个库目前我已经发现的bug。此外旧版本的库,或是软件会比较稳定而不易出错一直是一个错误的想法。新版本的库不仅能修复旧版本中已知的bug,还能引入新的功能,解决旧版本库所不能解决的问题。在选择开源库的时候,尽可能选择在持续维护和更新的库,并使用它的最新版本。

使用Json-lib,你的程序可能意外中断

1
2
3
4
5
6
7
8
9
public class App {
public static void main(String[] args) {
String s = "{";
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject);
jsonObject = JSONObject.fromObject("{\"a\":\"1\"}");
System.out.println(jsonObject);
}
}

如上程序试图将两个JSON转换为JSONObject对象。由于“{”不是一个合法的JSON对象,转换自然而然就失败了。因此在JSONObject jsonObject = JSONObject.fromObject(s);这一行,程序会抛出一个异常,信息如下:

1
2
3
4
5
6
7
Exception in thread "main" net.sf.json.JSONException: Found starting '{' but missing '}' at the end. at character 0 of null
at net.sf.json.util.JSONTokener.syntaxError(JSONTokener.java:499)
at net.sf.json.util.JSONTokener.<init>(JSONTokener.java:85)
at net.sf.json.JSONObject._fromString(JSONObject.java:1201)
at net.sf.json.JSONObject.fromObject(JSONObject.java:165)
at net.sf.json.JSONObject.fromObject(JSONObject.java:134)
at me.deeloves.App.main(App.java:9)

如果我们查看源代码的话,会发现net.sf.json.JSONException继承于org.apache.commons.lang.exception.NestableRuntimeException,而org.apache.commons.lang.exception.NestableRuntimeException又继承于java.lang.RuntimeException。RuntimeException不需要被捕获,因此当它被抛出而又没有捕获的时候,程序就会中断。这就导致了我们的第二个字符串,一个合法的JSON没办法被正常转换,除非我们修复程序,让它重新运行。在实际的工作中,我们常常会忽视了传入JSONObject.fromObject(Object)方法的对象是否为合法的参数,因此无意识地传入了不合法的参数,并导致了中断的发生。

如果我们仔细阅读了Json-lib API,就会发现JSONObject.fromObject(Object)方法抛出了一个非检查型异常。IDE不会帮助我们捕获这个异常,但是我们手动操作一下就可以修复这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class App {
public static void main(String[] args) {
try {
String s = "{";
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
try {
JSONObject jsonObject = JSONObject.fromObject("{\"a\":\"1\"}");
System.out.println(jsonObject);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

修复后的程序输出如下:

1
2
Found starting '{' but missing '}' at the end. at character 0 of null
{"a":"1"}

虽说在方法传入参数违背要求时可以抛出运行时异常,比如NullPointerException会在要求参数为非空但传入值为null时抛出。但是在Json-lib中,有些方法,如JSONObject.element(String, Map, JsonConfig)也会抛出JSONException,且文档[JSONObject.element(String, Map, JsonConfig)]中即没有告诉我们对参数的要求是什么,也没有告诉我们什么时候会抛出这个异常。这样的异常定义方式是令人困惑的。

Json-lib没有使用泛型

正如前文所提到的,Json-lib发布于2010年,而且最后一个版本的Json-lib是可以兼容JDK 1.5的。JDK 1.5是具有跨时代意义的一个Java版本。在这个版本中,Java语言第一次引入了泛型[New Features and Enhancements J2SE 5.0]。泛型增强了程序中的类型安全,使得许多过往在运行时类型不匹配而产生的异常在编译时就可以被发现并被修复。[Lesson: Generics (Updated)]遗憾的是,即便Json-lib的最后一个版本是在JDK 1.5发布了6年之后发布的,它依然没有使用泛型。

1
2
3
4
5
6
7
8
9
10
11
12
public class App {
public static void main(String[] args) {
try {
String s = "{\"a\":\"123\", \"b\":456}";
JSONObject jsonObject = JSONObject.fromObject(s);
char four = ((String) jsonObject.get("b")).charAt(0);
System.out.println(four);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

你可能以为这段程序会输出4。然而它运行结果如下:

1
2
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at me.deeloves.App.main(App.java:14)

很显然我们能看出问题在哪了:程序试图在运行时将Integer类型的对象转换成String对象,于是发生了这个错误。[JLS 5.1.6 Narrowing Reference Conversion] 在进行转换的时候,Json-lib会将JSON中的String类型转换成Java中的String类型。对于JSON中Number类型,Json-lib会将它转换成Integer类型,或是别的可以表示数字的类型。对于JSONObject,一个合适的泛型可能会是JSONObject<String, Object>。但很遗憾Json-lib没有这么做,于是Bug就很容易产生。它也不允许客户端指定要转换的JSONObject当中value的类型。如果你希望把所有的value都当成String来处理,一个比较可行的解决方案如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class App {
public static void main(String[] args) {
try {
String s = "{\"a\":\"123\", \"b\":456}";
JSONObject jsonObject = JSONObject.fromObject(s);
Map<String, String> theTrueConvertedMap = new HashMap<>();
for (Object e : jsonObject.entrySet()) {
if (e instanceof Map.Entry) {
Map.Entry entry = (Map.Entry) e;
theTrueConvertedMap.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
}
}
char four = theTrueConvertedMap.get("b").charAt(0);
System.out.println(four);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

这段程序将JSON转换成了类型安全的Map,确保了后续的操作都是类型安全的。但如果我们期望转换后得到的Map的泛型是别的类型的,恐怕就有些复杂了。

Json-lib对浮点数进行转换的时候会丢失精度[Json-lib Bug #116]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class App {
public static void main(String[] args) {
try {
String s = "{\"pi\":3.141592653589793}";
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject.get("pi"));
System.out.println(jsonObject.get("pi").getClass());
Double pi = Math.PI;
System.out.println(pi);
System.out.println(pi.equals(jsonObject.get("pi")));
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

你可能以为这段程序的输出是:

1
2
3
4
3.141592653589793
class java.lang.Double
3.141592653589793
true

但实际上它输出的是:

1
2
3
4
3.1415927
class java.lang.Double
3.141592653589793
false

Json-lib缺少更加详细定义的异常

Json-lib只定义了一个异常,而且很可悲地是一个运行时异常JSONException。所有可能会抛出异常的地方,抛出的异常都是JSONException。假定我们具备了一定的对错误输入的处理能力,并写下了如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class App {
public static void main(String[] args) {
print("{");
print("}");
}

private static void print(String s) {
try {
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject);
} catch (JSONException e) {
if (e.getMessage().startsWith("Found starting '{' but missing '}' at the end.")) {
s = s + "}";
try {
System.out.println(JSONObject.fromObject(s));
} catch (JSONException e1) {
System.out.println("oh we lost");
}
} else if (e.getMessage().startsWith("A JSONObject text must begin with '{'")) {
s = "{" + s;
try {
System.out.println(JSONObject.fromObject(s));
} catch (JSONException e1) {
System.out.println("oh we lost");
}
} else {
System.out.println(e.getMessage());
}
}
}
}

运行后的输出是:

1
2
{}
{}

但如果我们有能力对所有的不同异常情况进行处理呢?我们就需要写一长串的if-else语句,来判断异常信息的内容是什么,然后根据e.getMessage()得到的返回值的内容来进行各种各样的处理。如此长的if-else语句会使代码变得不可读和难以维护。Json-lib应当给不同类型的异常定义不同类型的,名字上带有具体含义的异常。比如在这里所处理的两种不同情况,所对应的异常可以是MissingFirstBraceExceptionMissingLastBraceException。很遗憾,Json-lib没有这么做。

不仅如此,在一些不必使用JSONException的地方,Json-lib还是不恰当地抛出了JSONException。比如JSONObject.element(String, boolean)[JSONObject.element(String, boolean)]会在第一个参数为null时抛出JSONException,但显然在这里NullPointerException要比JSONException更适合被抛出。

Json-lib不符合JSON的格式规范[ECMA-404]

使用Json-lib的初衷是为了能在JSON和Java对象之间进行转换,但如果你操作的根本就不是标准的JSON呢?

1
2
3
4
5
6
7
8
9
10
public class App {
public static void main(String[] args) {
try {
JSONObject jsonObject = JSONObject.fromObject("{'a':1}");
System.out.println(jsonObject);
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

这段程序能正常运行,但是{'a':1}真的是合乎规范的JSON Text吗?

由于版权的问题,我无法将规范内的图片截取到我这篇文章来。但上面这个程序的问题是显而易见的:规范中规定了JSON当中的字符串必须是双引号所包括的,内部是字符。而Json-lib会把单引号所括起来的,我不知道称之为什么的东西也当成JSON中合法的字符串。显然违背了JSON的格式规范。

更糟糕的是,JSON规范中规定了Object中的name必须为字符串,而Json-lib里却可以不是字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class App {
public static void main(String[] args) {
try {
String s = "{a:2}";
JSONObject jsonObject = JSONObject.fromObject(s);
System.out.println(jsonObject);
s = "{'a':2}";
System.out.println(JSONObject.fromObject(s));
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}

上面这段程序的输出结果如下:

1
2
{"a":2}
{"a":2}

这是两个一模一样的字符串。从第一行我们可以看到,当name不为字符串的时候,Json-lib会把整个“串”都当成字符串处理。根据这样的推断,第二行的输出应当是{"'a'":2}。而实际结果却不是这样。Json-lib自身对”JSON”格式的定义是怎么样的呢?我们无从得知,于是使用Json-lib所写出来的程序也就自然令人费解。

如果是前面所讲到的问题是可以被克服的(实际上我也尝试给出了解决的方案代码),那么这一小节所讲的是根本性问题,是我不推荐使用Json-lib的根本性原因。

总结

Json-lib的bug还有不少,如果你在搜索引擎里搜索一下,就能找到很多。[json-lib bug at DuckDuckGo]时间关系不再一一阐述。一句话总结:不要使用Json-lib。