在Web开发过程当中离不开数据的交互,这就须要规定交互数据的相关格式,以便数据在客户端与服务器之间进行传递。数据的格式一般有2种:一、xml;二、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包对其内部实现略有不一样。java
JSON字符串。JSON对象和JSON字符串之间的转换是序列化与反序列化的过程,这就是比如Java对象的序列化与反序列化。在网络中数据的传递是经过字符串,或者是二进制流等等进行的,也就是说在客户端(浏览器)须要将数据以JSON格式传递时,此时在网络中传递的是字符串,而服务器端在接收到数据后固然也是字符串(String类型),有时就须要将JSON字符串转换为JSON对象再作下一步操做(String类型转换为JSONObject类型)。git
以上两个概念的明确就基本明确了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对象。sql
1 package sfjson; 2 3 import java.util.List; 4 5 /** 6 * Created by Kevin on 2017/12/1. 7 */ 8 public class Student { 9 private int id; 10 private List<Long> courseIds; 11 12 public int getId() { 13 return id; 14 } 15 16 public void setId(int id) { 17 this.id = id; 18 } 19 20 public List<Long> getCourseIds() { 21 return courseIds; 22 } 23 24 public void setCourseIds(List<Long> courseIds) { 25 this.courseIds = courseIds; 26 } 27 28 public String getSql() { //此类中获取sql语句的方法,并无对应的属性字段 29 return "this is sql."; 30 } 31 }
在咱们将Student对象转换成JSON对象的时候,但愿转换后的JSON格式应该是:json
1 { 2 "id": 1, 3 "courseIds": [1, 2, 3] 4 }
然而在使用“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。this
其实PropertyDescriptor封装得已经很详细了,什么读写方法都已经赋值了。
例如这个getSql方法已经被解析成了上图的PropertyDescriptor。以后的经过这个类将一些方法过滤掉,例如getClass方法不是POJO中的方法,因此并不须要将它转换成JSON对象。而PropertyDescriptor的获取是经过BeanInfo#getPropertyDescriptors,而BeanInfo的获取则又是经过new Introspector(beanClass, null, USE_ALL_BEANINFO).getBeanInfo();不断深刻最后就会到达以下方法。
private BeanInfo getBeanInfo() throws IntrospectionException { … MethodDescriptor mds[] = getTargetMethodInfo(); //这个方法中会调用getPublicDeclaredMethods,能够看到确实是查找public方法,并且是全部public方法,包括wait等 PropertyDescriptor pds[] = getTargetPropertyInfo(); //按照必定的规则进行过滤,过滤规则全在这个方法里了,就是选择public修饰符带有get前缀和返回值的方法 …
对net.sf.json的源码简要分析了一下,发现确实如猜测的那样,具体的源码比较多篇幅有限需自行查看跟踪。
标题一句话解释不清楚,这个问题,我很肯定地认为它是一个bug。
如今有{"id": 1, "courseIds": [1,2,3]}的JSON字符串,须要将它转换为上文中提到的Student对象,在Student对象中有int和List<Long>类型的两个属性字段,也就是说这个JSON字符串应该转换为对应的数据类型。
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型,从这里来看最后的结果应该是对的啊。
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 public static Number transformNumber(Number input) { 2 if (input instanceof Float) { 3 return new Double(input.toString()); 4 } else if (input instanceof Short) { 5 return new Integer(input.intValue()); 6 } else if (input instanceof Byte) { 7 return new Integer(input.intValue()); 8 } else { 9 if (input instanceof Long) { 10 Long max = new Long(2147483647L); 11 if (input.longValue() <= max.longValue() && input.longValue() >= -2147483648L) { //就算原类型是Long型,可是只要它在Integer范围,那么就最终仍是会转换为Integer。 12 return new Integer(input.intValue()); 13 } 14 } 15 16 return input; 17 } 18 }
上面的这段代码很清晰的显示了元凶所在,不管是Long型(Integer范围内的Long型),包括Byte、Short都会转换为Integer。尚不明白这段代码的意义在哪里。前面又要根据数字后的字母肯定准确的数据类型,后面又要将准确的数据类型转换一次,这就致使了开头提到的那个bug。这个问题几乎是没法回避,因此最好的办法就是不要用。
这两个坑是偶然间发现,建议仍是不要使用早已没有维护的net.sf.json的JSON包,另外有一点,net.sf.json包对JSON格式的校验并不那么严格,若是这样的格式“{"id": 1, "courseIds": "[1,2,3]"}”,在其余三个包是会抛出异常的,但net.sf.json则不会。
这是一个能给程序员加buff的公众号