Java中net.sf.json包關於JSON與對象互轉的坑

  在Web開發過程當中離不開數據的交互,這就須要規定交互數據的相關格式,以便數據在客戶端與服務器之間進行傳遞。數據的格式一般有2種:一、xml;二、JSON。一般來講都是使用JSON來傳遞數據。本文正是介紹在Java中JSON與對象之間互相轉換時遇到的幾個問題以及相關的建議。 首先明確對於JSON有兩個概念:前端

  1. JSON對象(JavaScript Object Notation,JavaScript對象表示法)。這看似只存是位JavaScript所定製的,但它做爲一種語法是獨立於語言以及平臺的。只是說一般狀況下咱們在客戶端(瀏覽器)向服務器端傳遞數據時,使用的是JSON格式,而這個格式是用於表示JavaScript對象。它是由一系列的「key-value」組成,如 {「id」: 1, 「name」: 「kevin」},這有點相似Map鍵值對的存儲方式。在Java中所述的JSON對象,實際是指的JSONObject類,這在各個第三方的JSONjar包中一般都以這個名字命名,不一樣jar包對其內部實現略有不一樣。java

  2. 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中的JSON坑包——net.sf.json

1. 在Java對象轉換JSON對象時,get開頭的全部方法會被轉換

  這是什麼意思呢,例如現有如下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的源碼簡要分析了一下,發現確實如猜測的那樣,具體的源碼比較多篇幅有限需自行查看跟蹤。

2. 在JSON對象轉換Java對象時,List<Long>會出現轉換錯誤

  標題一句話解釋不清楚,這個問題,我很肯定地認爲它是一個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的公衆號 

相關文章
相關標籤/搜索