【問題】如題所示,在我們使用hibernate框架而又需要將對象轉化為json的時候,如果配置了雙向的關聯關系,就會出現這個死循環問題

  異常信息:

XML/HTML代碼
  1. Method public java.lang.String org.apache.commons.lang.exception.NestableRuntimeException.getMessage(int) threw an exception when invoked on net.sf.json.JSONException: There is a cycle in the hierarchy!  
  2. The problematic instruction:  
  3. ----------  
  4. ==> ${msgs[0][0]} [on line 76, column 25 in org/apache/struts2/dispatcher/error.ftl]  
  5. ----------  
  6.   
  7. Java backtrace for programmers:  
  8. ----------  
  9. freemarker.template.TemplateModelException: Method public java.lang.String org.apache.commons.lang.exception.NestableRuntimeException.getMessage(int) threw an exception when invoked on net.sf.json.JSONException: There is a cycle in the hierarchy!  
  10.     at freemarker.ext.beans.SimpleMethodModel.exec(SimpleMethodModel.java:130)  
  11.     at freemarker.ext.beans.SimpleMethodModel.get(SimpleMethodModel.java:138)  
  12.     at freemarker.core.DynamicKeyName.dealWithNumericalKey(DynamicKeyName.java:111)  
  13.     at freemarker.core.DynamicKeyName._getAsTemplateModel(DynamicKeyName.java:90)  
  14.     at freemarker.core.Expression.getAsTemplateModel(Expression.java:89)  
  15.     at freemarker.core.Expression.getStringValue(Expression.java:93)  
  16.     at freemarker.core.DollarVariable.accept(DollarVariable.java:76)  
  17.     at freemarker.core.Environment.visit(Environment.java:209)  
  18.     at freemarker.core.MixedContent.accept(MixedContent.java:92)  
  19.     at freemarker.core.Environment.visit(Environment.java:209)  
  20.     at freemarker.core.IfBlock.accept(IfBlock.java:82)  
  21.     at freemarker.core.Environment.visit(Environment.java:209)  
  22.     at freemarker.core.IfBlock.accept(IfBlock.java:82)  
  23.     at freemarker.core.Environment.visit(Environment.java:209)  
  24.     at freemarker.core.MixedContent.accept(MixedContent.java:92)  
  25.     at freemarker.core.Environment.visit(Environment.java:209)  
  26.     at freemarker.core.Environment.process(Environment.java:189)  
  27.     at freemarker.template.Template.process(Template.java:237)  
  28.     at org.apache.struts2.dispatcher.Dispatcher.sendError(Dispatcher.java:748)  
  29.     at org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:505)  
  30.     at org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:77)  
  31.     at org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:91)  
  32.     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)  
  33.     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)  
  34.     at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)  
  35.     at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)  
  36.     at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)  
  37.     at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)  
  38.     at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)  
  39.     at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:293)  
  40.     at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:861)  
  41.     at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:606)  
  42.     at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:489)  
  43.     at java.lang.Thread.run(Thread.java:619)  
  44. Caused by: java.lang.NullPointerException  
  45.     at freemarker.ext.beans.SimpleMemberModel.unwrapArguments(SimpleMemberModel.java:85)  
  46.     at freemarker.ext.beans.SimpleMethodModel.exec(SimpleMethodModel.java:106)  
  47.     ... 33 more  

  關鍵字是net.sf.json.JSONException: There is a cycle in the hierarchy!,意思是在層次關系里有一個循環

  【原因】為什么會這樣呢?原因在于你要轉化的對象里配置了對另外一個對象的關聯,而那個對象里又配置了對你這個對象的關聯。比如我的兩個類叫做Shop(商店)和Staff(員工),一個商店可以有多個員工,所以我給這兩個對象配置了雙向的一對多和多對一的關聯關系。這時候問題就出現了,JSON lib在把shop對象轉化為json字符串的時候,發現shop里有個Set<Staff>,它就會去級聯的把Set<Staff>轉化為json字符串,在它遍歷Set的時候,發現Staff里又有一個Shop對象,這時候它又會去嘗試把shop轉化為json字符串,然后就發現shop里又有Set<Staff>,如此周而復始,就形成了死循環。

  【解決一】如果你的業務單向關聯就可以滿足,比如查shop里面的staff員工信息,那么我們在類對象建立的時候在,Staff類里面的shop屬性上面使用注解@JsonIgnore即可,在后臺的controller我們可以查到staff對象中的shop屬性,但是在頁面json轉化的時候會被忽略掉,從而切斷遞歸,避免死循環;

  【解決二】如果你的業務必須使用雙向關聯,如何解決呢?我百度了一下,也找到了數十條的資料,但大都只是都說明了要用jsonConfig.setJsonPropertyFilter(new PropertyFilter(){}),而對于其中的參數和if語句該如何寫,并沒有一個說的很明白的。為此我查看了json-lib的源碼,并進行了嘗試,總結如下:

  1,思路是在JSONObject把Shop對象轉化為json字符串的時候,在中間加一道過濾,如果當前要轉化的屬性是Set并且屬性名是staffs,那么就進行過濾

  2,代碼如下:

Java代碼
  1. Map<String, Object> map=new HashMap<String, Object>();  
  2. map.put("shops", list);  
  3. map.put("total", total);  
  4.           
  5. JsonConfig jsonConfig = new JsonConfig();  
  6. jsonConfig.setJsonPropertyFilter(new PropertyFilter() {  
  7.     public boolean apply(Object obj, String name, Object value) {  
  8.     if(obj instanceof Set||name.equals("staffs")){  
  9.         return true;  
  10.     }else{  
  11.         return false;  
  12.     }  
  13.    }  
  14. });  
  15.           
  16. return JSONObject.fromObject(map,jsonConfig);  

  如上,PropertyFilter是json-lib提供的進行屬性過濾的一個接口,具體的實現是由apply方法做的,那么我們就需要重寫apply方法。

  此方法有三個參數,第一個是Object類型的,是你要過濾的屬性的類型;第二個參數是String類型,是你要過濾的屬性的名稱;第三個參數是Object類型的,是你要過濾的屬性的值(值可能是String或其它類型的,所以用Object)。 返回值是boolean類型的,返回true;就是進行過濾,返回false,就是不進行過濾。

  if語句的寫法就要根據實際的需要了,比如說我這里要解決死循環,就要實現把Shop里的Set<Staff> staffs屬性過濾掉,那我的if語句就應該如上面那樣寫??偠灾褪荍SON-lib在轉化的時候,會對每個屬性都調用這個apply方法,這樣我們就要根據實際的業務需要,如果當前屬性符合你的if條件,那你就要返回true,進行過濾。是使用||還是&&也要根據實際而定,比如你的Shop里有兩個Set,那你就要使用&&。

  這樣配置后,再測試,就發現獲取Shop的時候死循環問題已經不再出現了。

  3,同理,Staff端也應該進行類似的配置

Java代碼
  1. Map<String,Object> map=new HashMap<String, Object>();  
  2. map.put("staffs", list);  
  3. map.put("total", total);  
  4.           
  5. JsonConfig jsonConfig = new JsonConfig();    
  6. jsonConfig.setExcludes(new String[]{"handler","hibernateLazyInitializer"});  
  7. jsonConfig.setJsonPropertyFilter(new PropertyFilter() {  
  8.     public boolean apply(Object obj, String name, Object value) {  
  9.         if(obj instanceof Shop&&name.equals("shop")){  
  10.             return true;  
  11.         }else{  
  12.             return false;  
  13.         }  
  14.     }  
  15. });  
  16.           
  17. return JSONObject.fromObject(map,jsonConfig);  

  經過測試,也是沒有問題的。

  這里大家也可以看見jsonConfig.setExcludes(new String[]{"handler","hibernateLazyInitializer"});,這一行是為了防止hibernate延遲加載造成的異常而設置的。

  4,到這里大功告成了嗎?不,我在測試的時候發現了一個更嚴重的問題,如果按照上面做這樣配置,那我獲取shop的時候,生成的json字符串里staffs的Set不見了;獲取Staff的時候,它的屬性shop在json字符串里也不見了!稍加分析就可以知道這是上面配置造成的。按上面的配置,Shop里的Set<Staff>被過濾掉了,“過濾掉”的含義不是不級聯的轉化Staff里的Shop了,而是直接連Set<Staff>都不轉化了。這可壞了,我配置雙向關聯關系就是為了關聯顯示,你把我的屬性過濾掉了,那我還配置雙向關聯干嘛?我還這么大費周章的來解決死循環干嘛?

  那么這個問題該如何解決呢?其實仔細一想,也不難,大家看我把Shop的配置改成下面這樣

Java代碼
  1. Map<String, Object> map=new HashMap<String, Object>();  
  2. map.put("shops", list);  
  3. map.put("total", total);  
  4.           
  5. JsonConfig jsonConfig = new JsonConfig();  
  6. jsonConfig.setJsonPropertyFilter(new PropertyFilter() {  
  7.     public boolean apply(Object obj, String name, Object value) {  
  8.     if(obj instanceof Staff||name.equals("shop")){  
  9.         return true;  
  10.     }else{  
  11.         return false;  
  12.     }  
  13.    }  
  14. });  
  15.           
  16. return JSONObject.fromObject(map,jsonConfig);  

  這樣就可以獲取到了,為什么呢?因為這樣配置的話,在將Shop里的Set<Staff> staffs轉化的時候,我們不過濾;而在將staffs里的每個Staff里的shop轉化的時候,我們進行過濾,這樣就既解決了死循環問題,又避免了Shop里的staffs被過濾掉的問題。

  同理,Staff要這樣配置

Java代碼
  1. Map<String,Object> map=new HashMap<String, Object>();  
  2. map.put("staffs", list);  
  3. map.put("total", total);  
  4.           
  5. JsonConfig jsonConfig = new JsonConfig();    
  6. jsonConfig.setExcludes(new String[]{"handler","hibernateLazyInitializer"});  
  7. jsonConfig.setJsonPropertyFilter(new PropertyFilter() {  
  8.     public boolean apply(Object obj, String name, Object value) {  
  9.         if(obj instanceof Set||name.equals("staffs")){  
  10.             return true;  
  11.         }else{  
  12.             return false;  
  13.         }  
  14.     }  
  15. });  
  16.           
  17. return JSONObject.fromObject(map,jsonConfig);  

  可是我測試的時候卻發現得到的字符串里只有total,staffs沒有了?大家可能已經明白了,不僅Shop里的staffs被過濾掉了,map里的staffs也被過濾掉了。解決也很簡單,把map.put("staffs",list);改成map.put("list",list);就行了,就是換個名字。

  【擴展】到這里,應該能解決大家的問題了。另外還有一種方法也要提一下,

Java代碼
  1. JsonConfig jsonConfig = new JsonConfig();  
  2. jsonConfig.setIgnoreDefaultExcludes(false); //設置默認忽略   
  3. jsonConfig.setCycleDetectionStrategy(CycleDetectionStrategy.LENIENT);//設置循環策略為忽略    解決json最頭疼的問題 死循環  
  4. jsonConfig.setExcludes(new String[] {"staffs"});//此處是亮點,只要將所需忽略字段加到數組中即可  

  這種配置也是比較好理解的,但也要注意屬性過濾問題,Shop過濾shop,Staff過濾staffs。

除非特別注明,雞啄米文章均為原創
轉載請標明本文地址:http://www.028keji.com/software/711.html
2017年3月24日
作者:雞啄米 分類:軟件開發 瀏覽: 評論:0