Hibernate的POJO到JSON序列化過程

原文:http://www.iteye.com/topic/296467
 一年前的代碼,最近寫了點文檔share大家,有點亂,這個亂主要指夾雜了些我們公司開發框架的一些東西。 
    如果看到錯誤或者莫名其妙的地方還請指出,包括錯別字。 
    此文純屬個人觀點,歡迎討論。 

論壇關聯的地址: 
http://www.iteye.com/topic/296467 

我認爲的“智能”的JSON序列化方案主要包含如下特點: 
1.要像JSON-lib那樣能夠利用反射和遞歸,自動遍歷對象圖,同時並序列化,這個要求JSON-lib已經滿足。 
2.默認情況下,所有的簡單對象(非Hibernate代理對象)都能夠自動序列化,利用JSON-lib就能實現。 
3.所有的Hibernate代理對象自動屏蔽。 
4.提供足夠靈活和足夠簡單的參數方式,有選擇的序列化指定的Hibernate代理對象。 
5.對Hibernate多對一的代理對象提供足夠的支持,能夠直接抓取外鍵ID而不觸發多餘的sql,這個多對一的性能問題可以參考自我的另外一帖的分析: 
【Hibernate 3.2中annotation註釋的位置對性能的巨大影響】 
http://www.iteye.com/topic/212236 

    有興趣的人蔘看附件中的源代碼。 

    先介紹JSON的基本概念,至於序列化成JSON的目的後問會詳細敘述。JSON:JavaScript Object Notation,是一種輕量級的數據交換格式。易於人閱讀和編寫。同時也易於機器解析和生成。它基於JavaScript(Standard ECMA-262 3rd Edition - December 1999)的一個子集。 
    來看一個NodeUser的部分屬性的最簡單的例子: 
Js代碼  收藏代碼
  1. {"nusId":"402892891b209674011b209681530009","nusCreateTime":"2008-05-06 20:21:34"}  

    可以理解爲一個map結構的數據結構,一個key(屬性名),一個value(屬性值),只不過都是文本類型。 
看一個數組或者Collection轉換成JSON後的例子: 
Js代碼  收藏代碼
  1. [  
  2.     {"nusEmail":"[email protected]","nusStatus":{"code":"1040-1010","name":"身份證"}},  
  3.     {"nusEmail":"[email protected]","nusStatus":{"code":"1040-1020","name":"軍人證"}}  
  4. ]  

    就是用中括號來表示多個對象的集合。在這例子中還出現了對象的嵌套,nusStatus是一個枚舉對象,之前已經介紹過,只有code和name兩個屬性,從數據庫讀取數據以後,只有code有值,name的值一開始是空的,隨意JSON序列化的過程除了把Java對象轉換成JSON文本以外,還要把所有的枚舉類中的name的值填上,最後才把序列化好的JSON字符串傳遞到客戶端瀏覽器,JSON通訊策略後文會分析。 

    分析一個性能問題:JSON序列化和Hibernate放在一起使用其實就是一個數據抓取的過程,因爲JSON序列化的時候遍歷的的是Hibernate生成的實體類的每一個屬性,表面上看訪問的是屬性,但是實際上是Hibernate生成的代理對象,調用屬性的get方法的過程某種意義上來說就是一個數據抓取的過程。Hibernate的性能問題絕大部分都是出現在數據抓取的粒度和深度問題。所以JSON-lib的工具類net.sf.json.JSONObject和net.sf.json.JSONArray是不能滿足本框架的需求的,或者說是不能直接和Hibernate結合使用的。 
    一般大多數的JSON序列化的做法是在每一個實體類中實現一個toJSON()的方法來直接提供序列化的支持,我認爲這種做法是非常的有問題的,實體類的結構是要充分利用的,也就意味着一個實體類的數據抓取粒度和深度在不同場景下是完全不一樣的,不可能用一個toJSON()方法就能完全搞定。再有就是JSON序列化屬於和Ajax客戶端通訊是才使用的機制,把Ajax通訊相關的代碼寫在實體類中,從軟件分層角度上來說也是非常不合適的。 

    舉一個抓取粒度和深度不同的例子:先參考前文描述的NodeEducation和NodeUser的多對一的關係。第一個場景是,需要列出所有教育背景的基本信息。第二個場景是,列出所有教育背景的基本信息,外加所屬用戶的姓名,這個需求下,數據抓取一定會牽涉到NodeUser所對應的表,這兩個場景的數據抓取策略肯定是不一樣的。 

    介紹JSONConvert類中的三個方法: 
    modelCollect2JSONArray方法,把Collection轉換爲JSONArray對象,實現方式就是遍歷需要轉換的Collection,針對每一個model再次調用另一個專門轉換model方法,最後返回JSONArray對象。 

    model2JSON是最重要的方法,把model轉換爲JSON對象,凡是實現JSONNotAware的不序列化,按照get方法得到屬性信息,默認對所有可能引起的Hibernate查詢數據的sql的操作都會跳過,目的是減少不必要的延遲加載,如果一定要要抓取的話,則把需要序列化的名字通過jsonAwareArray傳入。 

    getAwareSubList方法是用來處理每一級的遞歸需要的字符串處理,從原始的jsonAwareList中查找前綴爲name的,去除前綴後,放到新的List中返回。 

    結合一個具體的例子詳細分析如下,先不考慮分頁: 

    場景一:需要列出所有教育背景的基本信息。 

    NodeEducationService中代碼如下,因爲利用的BaseManager,所以NodeEducationManager針對這個查詢中沒有特別的代碼: 
Java代碼  收藏代碼
  1. public List findList() {  
  2.     return nodeEducationManager.findList();  
  3. }  

    Ctrl類最終會間接的調用modelCollect2JSONArray(nodeEducationList),Ctrl的代碼規範和運行過程後文會詳細介紹,這裏的list就是List,modelCollect2JSONArray遍歷每一個元素,調用model2JSON(nodeEducation),這個方法會根據反射的結果遍歷所有的nodeEducation的所有method。爲什麼是遍歷方法,而不是遍歷屬性是有重要原因的,例如一個文件類,只保存了完整的文件名的這一個屬性,但是提供了很多get方法,有獲得文件名前綴、後綴的等等,雖然頁面上編程也能達到同樣的效果,但是實體類中既然已經提供了這樣便利的方法,就應該充分利用,就應該根據get方法造兩個屬性“文件名前綴”、“文件名後綴”出來,這樣能充分減少頁面上的編程工作量。 

    繼續分析序列化單實例的過程,在遍歷所有的方法時,會判斷當前方法是否符合getMethod的特徵,符合的才序列化這個getMethod對應的虛擬屬性,因爲這個屬性未必真實存在。此時還會過濾一些明顯不需要序列化的get方法,例如getClass方法、或者是getMethod返回類型是Document.class(XML Document對象)、byte[].class(二進制文件對象)、Logger.class(系統日誌對象)、LazyInitializer.class(Hibernate代理對象中的延遲加載屬性),這些都是明顯不需要序列化的對象。 

    然後繼續判斷,如果getMethod返回格式是Calendar和Date格式,則調用相應的轉換方法,轉換成統一的字符串格式,如果是枚舉類格式,則調用dictionaryFactory中的setName方法,根據枚舉類的code找到對應的name,並賦值到給這個枚舉類的name。、 

    如果getMethod返回格式是JSONNotAware,說明遇到了多對一的屬性,如果繼續訪問這個這個返回對象的其他值就有可能要觸發sql,所以跳過。 

    如果getMethod返回格式是org.hibernate.collection.AbstractPersistentCollection,說明遇到了一對多的屬性,如果繼續訪問這個這個返回對象的其他值就有可能要觸發sql,必須跳過。 

    對於場景一序列化大致就是這個過程,但是對於多對一的跳過的說法其實是不完全的,先分析一下,假設頁面上有這樣的需求,每一個教育背景信息佔一行,每一行有一個鏈接“查看所屬用戶”,指向一個新頁面,那這個url裏的get參數肯定是要帶上所屬用戶id的,到了顯示用戶的頁面再根據次id查詢相關用戶信息,也就是說序列化的內容中需要多對一屬性nodeUser的主鍵nusId,在表node_education中就是普通的外鍵,這項信息本來就是應該存在的,不需要再次觸發什麼sql來查詢node_user表。而且Hibernate也確實提供了這樣的機制來獲取這個外鍵id,而不會觸發新的查詢sql,一旦觸發就是著名的N+1查詢問題,有10個教育背景可能就產生11條sql語句。 

    現在詳細說明獲取這個外鍵id的過程,首先判斷這個返回對象是不是HibernateProxy的類型,不是的話說明不是Hibernate多對一的代理對象。然後利用ModelUtils.getIdFieldName((Modelable) getObj)獲取idName,利用(((HibernateProxy) getObj).getHibernateLazyInitializer()).getIdentifier()獲取id的值,構造一個只有id的JSONObject對象進行序列化。也就是說,序列化的時候,默認總是會序列化多對一的那個對象的主鍵,也就是主表的外鍵,因爲這些數據應經抓取到了,不會觸發額外的sql影響性能。最後得到的序列化的結果示例如下: 
Js代碼  收藏代碼
  1. {  
  2.     "nedId":"4028928d1c744a03011c744a03770000",  
  3.     "nedCreateTime":"2008-10-06 20:12:21",  
  4.     "nodeUser":{"nusId":"4028928d1c744a03011c744a03770000"}  
  5. }  

  省略了部分屬性,注意這裏的nodeUser嵌套對象只有nusId這一個屬性。 

  場景二,如果需求變化,在這個教育背景列表中的每一行都要顯示所屬用戶的姓名,從數據庫的角度來分析,這是一定要查詢第二張表才能獲取到數據的。NodeUserManager中新增一個方法: 
Java代碼  收藏代碼
  1. public List findListWithNodeUser() {  
  2.     String hql = "from NodeEducation ned join ned.nodeUser";  
  3.     return getQuery(hql).list();  
  4. }  

  雖然這是一個跨表的查詢,但是不是一個跨表的業務操作,所以可以放在Manager中。NodeUserService也增加一個同名方法:
Java代碼  收藏代碼
  1. public List findListWithNodeUser() {  
  2.     return nodeEducationManager.findListWithNodeUser();  
  3. }  

    之後和場景一類似,不同之處在於Ctrl類最終會的調用代碼類似下面的例子: 
Java代碼  收藏代碼
  1. List jsonAwareCollect = new LinkedList();  
  2. jsonAwareCollect.add("nodeUser");  
  3. modelCollect2JSONArray(nodeEducationList, jsonAwareCollect);  

    jsonAwareCollect是第二個參數,其中指定的屬性名字會強制序列化,在判斷多對一和一對多的場景前,會先判斷jsonAwareCollect參數中是否含有當前屬性的名稱,有的話會強制序列化,所以最終的序列化結果示例如下: 
Java代碼  收藏代碼
  1. {  
  2.     "nedId":"4028928d1c744a03011c744a03770000",  
  3.     "nedCreateTime":"2008-10-06 20:12:21",  
  4.     "nodeUser":{"nusId":"4028928d1c744a03011c744a03770000","nusEmail":"[email protected]"}  
  5. }  

    序列化過程會經由一個遞歸過程完整遍歷nodeUser對象,getAwareSubList方法就是每次遞歸之前處理下一次遞歸所需的jsonAwareCollect用的,例如,如果jsonAwareCollect的代碼如下,假設nodeUser中還有個多對一對象group: 
Java代碼  收藏代碼
  1. jsonAwareCollect.add("nodeUser");  
  2. jsonAwareCollect.add("nodeUser.group");  

    表示繼續序列化實體類group,實際上上面的例子只需第二行就行了,因爲第二行肯定會序列化nodeUser對象了,第一行就不需要了。至此說明了jsonAwareCollect就是可定製序列化的核心,通過簡單明瞭的參數傳遞就能夠很方便的進行序列化的定製,達到既能只獲取必須數據,又不會過多的抓取數據的效果。在場景二的例子中,因爲Manager中的實現已經做了兩表的關聯查詢,相關的nodeUser數據早已抓取完畢,所以序列化nodeUser的時候不會觸發新的sql,也就是說通過配套的查詢方法,能夠和JSON序列化工具很好的配合,最終只產生一句sql,就能抓取並序列化所有需要數據。 

    但是這種配合在一對多下是不成立,本質上因爲,一對多很難用一句sql搞定所有數據,此時要考慮UI交互問題,而不是強行的一次性抓取數據。例,場景三,羅列當前數據庫中所有NodeUser用戶的基本信息,同時還要顯示每個用戶的所有教育背景資料,初看是一個必須要抓取每個nodeUser的一對多屬性nodeEducationList的例子,但是實際上這個需求本身就不合理,這麼多信息在一個頁面本來就難以顯示,實際一點的做法往往是,給一個名爲“查看所有教育背景”的鏈接,點擊以後,傳遞一個所在行nodeUser的id到新的頁面,服務端重新根據傳遞的id來查詢次用戶下的所有教育背景資料,第二次查詢用Hibernate來做也是很簡單的,花不了多少工作量,但是邏輯非常清楚。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章