在web开发过程中离不开数据的交互,这就需要规定交互数据的相关格式,以便数据在客户端与服务器之间进行传递。数据的格式通常有2种:1、xml;2、json。通常来说都是使用json来传递数据。本文正是介绍在java中json与对象之间互相转换时遇到的几个问题以及相关的建议。
首先明确对于json有两个概念:
json对象(javascript object notation,javascript对象表示法)。这看似只存是位javascript所定制的,但它作为一种语法是独立于语言以及平台的。只是说通常情况下我们在客户端(浏览器)向服务器端传递数据时,使用的是json格式,而这个格式是用于表示javascript对象。它是由一系列的“key-value”组成,如 {“id”: 1, “name”: “kevin”},这有点类似map键值对的存储方式。在java中所述的json对象,实际是指的jsonobject类,这在各个第三方的jsonjar包中通常都以这个名字命名,不同jar包对其内部实现略有不同。
json字符串。json对象和json字符串之间的转换是序列化与反序列化的过程,这就是好比java对象的序列化与反序列化。在网络中数据的传递是通过字符串,或者是二进制流等等进行的,也就是说在客户端(浏览器)需要将数据以json格式传递时,此时在网络中传递的是字符串,而服务器端在接收到数据后当然也是字符串(string类型),有时就需要将json字符串转换为json对象再做下一步操作(string类型转换为jsonobject类型)。
以上两个概念的明确就基本明确了json这种数据格式,或者也称之为json语法。java中对于json的jar包有许多,最最“常用”的是“net.sf.json”提供的jar包了,本文要着重说的就是这个坑包,虽然坑,却有着广泛的应用。其实还有其他优秀的json包供我们使用,例如阿里号称最快的json包——fastjson,还有谷歌的gson,还有jackson。尽量,或者千万不要使用“net.sf.json”包,不仅有坑,而且已经很老了,老到都没法在idea里下载到源码,maven仓库里显示它2010年在2.4版本就停止更新了。下面就谈我已知的“net.sf.json”的2个bug(我认为这是bug),以及这2个bug是如何产生的。
java中的json坑包——net.sf.json
1. 在java对象转换json对象时,get开头的所有方法会被转换
这是什么意思呢,例如现有以下java对象。
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
|
package sfjson; import java.util.list; /** * created by kevin on 2017/12/1. */ public class student { private int id; private list< long > courseids; public int getid() { return id; } public void setid( int id) { this .id = id; } public list< long > getcourseids() { return courseids; } public void setcourseids(list< long > courseids) { this .courseids = courseids; } public string getsql() { //此类中获取sql语句的方法,并没有对应的属性字段 return "this is sql." ; } } |
在我们将student对象转换成json对象的时候,希望转换后的json格式应该是:
1
2
3
4
|
{ "id" : 1 , "courseids" : [ 1 , 2 , 3 ] } |
然而在使用“net.sf.json”包的jsonobject json = jsonobject.fromobject(student); api转换后的结果却是:
也就是说可以猜测到的是,“net.sf.json”获取java对象中public修饰符get开头的方法,并将其后缀定义为json对象的“key”,而将get开头方法的返回值定义为对应key的“value”,注意是public修饰符get开头的方法,且有返回值。
我认为这是不合理的转换规则。如果我在java对象中定义了一个方法,仅仅因为这个方法是“get”开头,且有返回值就将其作为转换后json对象的“key-value”,那岂不是暴露出来了?或者在返回给客户端(浏览器)时候就直接暴露给了前端的console控制台?作者规定了这种转换规则,我想的大概原因是:既然你定义为了public方法,且命名为get,那就是有意将此方法暴露出来让调用它的客户端有权获取。但我仍然认为这不合理,甚至我定义它是一个bug。我这么定义也许也不合理,因为据我实测发现,不仅是“net.sf.json”包会按照这个规则进行转换,fastjson和jackson同样也是照此规则,唯独谷歌的gson并没有按照这个规则进行对象向json转换。
通过jsonobject json = jsonobject.fromobject(student);将构造好的student对象转换为json对象,student如上文所述。 进入此方法后会继续调用fromobject(object, jsonconfig)的重载方法,在此重载方法中会通过instanceof判断待转换的object对象是否是枚举、注解等类型,这些特殊类型会有特别的判断方法。在这里是一个普通的java pojo对象,所以会进入到_fromobject(object, jsonconfig),在这个方法中会有一些判断,而最后则通过调用defaultbeanprocessing创建json对象。这个方法是关键,在里面还继续会通过propertyutils.getpropertydescriptors(bean)方法获取“属性描述符”,实际上就是获取带get的方法,它在这里封装成了propertydescriptor。这student这个类中会获取4个,分别是:getclass、getid、getcourseids、getsql。
其实propertydescriptor封装得已经很详细了,什么读写方法都已经赋值了。
例如这个getsql方法已经被解析成了上图的propertydescriptor。之后的通过这个类将一些方法过滤掉,例如getclass方法不是pojo中的方法,所以并不需要将它转换成json对象。而propertydescriptor的获取是通过beaninfo#getpropertydescriptors,而beaninfo的获取则又是通过new introspector(beanclass, null, use_all_beaninfo).getbeaninfo();不断深入最后就会到达如下方法。
1
2
3
4
5
|
private beaninfo getbeaninfo() throws introspectionexception { … methoddescriptor mds[] = gettargetmethodinfo(); //这个方法中会调用getpublicdeclaredmethods,可以看到确实是查找public方法,而且是所有public方法,包括wait等 propertydescriptor pds[] = gettargetpropertyinfo(); //按照一定的规则进行过滤,过滤规则全在这个方法里了,就是选择public修饰符带有get前缀和返回值的方法 … |
对net.sf.json的源码简要分析了一下,发现确实如猜想的那样,具体的源码比较多篇幅有限需自行查看跟踪。
2. 在json对象转换java对象时,list<long>会出现转换错误
标题一句话解释不清楚,这个问题,我很确定地认为它是一个bug。
现在有{"id": 1, "courseids": [1,2,3]}的json字符串,需要将它转换为上文中提到的student对象,在student对象中有int和list<long>类型的两个属性字段,也就是说这个json字符串应该转换为对应的数据类型。
1
2
3
|
string json = "{\"id\": 1, \"courseids\": [1,2,3]}" ; student student = (student) jsonobject.tobean(jsonobject.fromobject(json), student. class ); system.out.println(student.getcourseids().get( 0 ) instanceof long ); |
上面的输出结果应该是true,然而遗憾的是却是false。准确来说在编译时是long型,而在运行时却是integer。这不得不说就是一个坑了,另外三个json包都未出现这种错误。所以我确定它是一个bug。来看看这个bug在net.sf.json是怎么发生的,同样需要自行对比源码进行查看。我在打断点debug不断深入的时候发现了net.sf.json对于整型数据的处理时,发现了这个方法numberutils#createnumber,这个类是从字符串中取出数据时判断它的数据类型,本意是想如果数字后面带有“l”或“l”则将其处理为long型,从这里来看最后的结果应该是对的啊。
1
2
3
4
5
6
7
8
9
10
11
|
case 'l' : case 'l' : if (dec == null && exp == null && (numeric.charat( 0 ) == '-' && isdigits(numeric.substring( 1 )) || isdigits(numeric))) { try { return createlong(numeric); } catch (numberformatexception var11) { return createbiginteger(numeric); } } else { throw new numberformatexception(str + " is not a valid number." ); } |
的确到目前为止net.sf.json通过数字后的标识符准确地判断了数据类型,问题出就出在获得了这个值以及它的数据类型后需要将它存入jsonobject中,而存入的过程中有jsonutils#transformnumber这个方法的存在,这个方法的存在,至少在目前看来纯属画蛇添足。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public static number transformnumber(number input) { if (input instanceof float ) { return new double (input.tostring()); } else if (input instanceof short ) { return new integer(input.intvalue()); } else if (input instanceof byte ) { return new integer(input.intvalue()); } else { if (input instanceof long ) { long max = new long (2147483647l); if (input.longvalue() <= max.longvalue() && input.longvalue() >= -2147483648l) { //就算原类型是long型,但是只要它在integer范围,那么就最终还是会转换为integer。 return new integer(input.intvalue()); } } return input; } } |
上面的这段代码很清晰的显示了元凶所在,不论是long型(integer范围内的long型),包括byte、short都会转换为integer。尚不明白这段代码的意义在哪里。前面又要根据数字后的字母确定准确的数据类型,后面又要将准确的数据类型转换一次,这就导致了开头提到的那个bug。这个问题几乎是无法回避,所以最好的办法就是不要用。
这两个坑是偶然间发现,建议还是不要使用早已没有维护的net.sf.json的json包,另外有一点,net.sf.json包对json格式的校验并不那么严格,如果这样的格式“{"id": 1, "courseids": "[1,2,3]"}”,在其他三个包是会抛出异常的,但net.sf.json则不会。
以上这篇详谈java中net.sf.json包关于json与对象互转的坑就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持服务器之家。
原文链接:http://www.cnblogs.com/yulinfeng/archive/2017/12/03/7967603.html