1. 程式人生 > >Java中net.sf.json包關於JSON與物件互轉的坑

Java中net.sf.json包關於JSON與物件互轉的坑

在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 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 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 getCourseIds() {
    21 return courseIds;
    22 }
    23
    24 public void setCourseIds(List courseIds) {
    25 this.courseIds = courseIds;
    26 }
    27
    28 public String getSql() { //此類中獲取sql語句的方法,並沒有對應的屬性欄位
    29 return “this is sql.”;
    30 }
    31 }

      在我們將Student物件轉換成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。

  其實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的原始碼簡要分析了一下,發現確實如猜想的那樣,具體的原始碼比較多篇幅有限需自行檢視跟蹤。

  1. 在JSON物件轉換Java物件時,List會出現轉換錯誤

      標題一句話解釋不清楚,這個問題,我很確定地認為它是一個bug。

      現在有{“id”: 1, “courseIds”: [1,2,3]}的JSON字串,需要將它轉換為上文中提到的Student物件,在Student物件中有int和List型別的兩個屬性欄位,也就是說這個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格式的校驗並不嚴格