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

来源:http://www.cnblogs.com/yulinfeng/archive/2017/12/03/7967603.html
-Advertisement-
Play Games

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


  在Web開發過程中離不開數據的交互,這就需要規定交互數據的相關格式,以便數據在客戶端與伺服器之間進行傳遞。數據的格式通常有2種:1、xml;2、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包對其內部實現略有不同。

  2. 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<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格式應該是:

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

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的公眾號 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在python進行像b = a這樣的賦值時,只會創建一個對a的新引用,使a的引用計數加1,而不會創建新的對象: 這樣,當引用的對象是可變對象的時候(列表,字典,可變集合等),會產生意料之外的行為: 因為a和b引用的是同一對象,改變其中一個,另外一個也會隨之改變。當我們想建立一個副本而不是引用時,可以 ...
  • 前言 上篇SSM框架環境搭建篇,演示了我們進行web開發必不可少的一些配置和準備工作,如果這方面還有疑問的地方,可以先參考上一篇“SSM框架開發web項目系列(一) 環境搭建篇”。本文主要介紹MyBatis的基礎內容,包括基本概念、開發步驟、使用實例等。說起MyBatis,工作中做過SSH/SSM相 ...
  • 簡介: 割邊和割點的定義僅限於無向圖中。我們可以通過定義以蠻力方式求解出無向圖的所有割點和割邊,但這樣的求解方式效率低。Tarjan提出了一種快速求解的方式,通過一次DFS就求解出圖中所有的割點和割邊。 歡迎探討,如有錯誤敬請指正 如需轉載,請註明出處 http://www.cnblogs.com/ ...
  • 本文涉及到的知識:Struts2超鏈接的使用、Struts遍歷List、${pageContext.request.contextPath} 不能出現在Struts2標簽中。 ...
  • 前言 為何使用Python Python 是一種效率極高的語言。與其他眾多的語言相比,實現相同功能,使用Python編寫的程式包含的代碼更少。Python的語法簡單,易上手,使用Python編寫的代碼更容易閱讀、調試和擴展。 Python使用領域也比較多。Python應用領域有:雲計算、人工智慧、大 ...
  • 隨筆之java匿名內部類 從今天起開始每日一篇技術博客,當然這隻是我當天所學的一些隨筆,裡面或多或少會有理解不當的地方,希望大家多多指教,一起進步! 在講匿名內部類之前,先講講內部類的一些概念。 1.內部類:顧名思義,內部類就是寫在一個類裡面的類(廢話),這裡大家可能會問了,為什麼我們不直接寫一個c ...
  • 三、tornado.web.Application 1、Application:tornado.web.Aplication新建一個應用,可通過直接實例化這個類或實例化它的子類來新建應用; 2、handlers:實例化時至少需要傳入參數handlers,handlers為元素為元組的列表,元組中第一 ...
  • 周末真的是懶到心慌。。。。。。 本文是在完整s2sh項目基礎上添加的,不太瞭解s2sh項目構建的朋友可以先參考一下這幾篇文章: eclipse環境下基於tomcat-7.0.82構建struts2項目 eclipse環境下基於已構建struts2項目整合spring+hibernate 基於已構建S ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...