從源代碼角度看Struts2返回JSON數據的原理

前面一篇文章其實只是介紹瞭如何在Struts2中返回JSON數據到客戶端的具體範例而無關其原理,內容與標題不符惹來標題黨嫌疑確實是筆者發文不夠嚴謹,目前已修改標題,與內容匹配。本文將從struts2-json插件的源碼角度出發,結合之前的應用範例來說明struts2-json插件返回JSON數據的原理。

 

用winrar打開struts2-json-plugin-xx.jar(筆者使用版本爲2.1.8.1),根目錄下有一個struts-plugin.xml,這個文件想必大家都很瞭解,不做過多介紹了。打開該文件,內容非常簡答,如下:

 

Xml代碼
  收藏代碼
  1. <?xml version="1.0" encoding="UTF-8" ?>  
  2.   
  3. <!DOCTYPE struts PUBLIC  
  4.         "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"  
  5.         "http://struts.apache.org/dtds/struts-2.0.dtd">  
  6.   
  7. <struts>  
  8.     <package name="json-default" extends="struts-default">  
  9.         <result-types>  
  10.             <result-type name="json" class="org.apache.struts2.json.JSONResult"/>  
  11.         </result-types>  
  12.         <interceptors>  
  13.             <interceptor name="json" class="org.apache.struts2.json.JSONInterceptor"/>  
  14.         </interceptors>  
  15.     </package>  
  16. </struts>  

 

 

前文提到,如果要使用Struts2返回JSON數據到客戶端,那麼action所在的package必須繼承自json-default包,原因就在上邊的配置文件中:這裏的配置文件指定了該插件的包名爲json-default,所以要使用該插件的功能,就必須繼承自該包——json-default。

 

上面的配置文件中,配置了兩個類:org.apache.struts2.json.JSONResult和org.apache.struts2.json.JSONInterceptor,前者是結果類型,後者是一個攔截器。簡單說一下,org.apache.struts2.json.JSONResult負責將action中的“某些”(通過相關參數可以指定,前文已有詳述)或action中所有"可獲取"(有getter方法的屬性或一個有返回值的getter方法的返回值)數據序列化成JSON字符串,然後發送給客戶端;org.apache.struts2.json.JSONInterceptor負責攔截客戶端到json-default包下的所有請求,並檢查客戶端提交的數據是否是JSON類型,如果是則根據指定配置來反序列化JSON數據到action中的bean中(說的有點簡單,其實該攔截器內部對數據做了很多判斷),攔截器不是本文的重點,介紹到此爲止。看一張圖,或許能夠更加清晰明瞭的說明JSON插件執行的流程:

 

 

JSON插件執行時序圖

 

 

下面重點說說org.apache.struts2.json.JSONResult。

 

首先看一下org.apache.struts2.json.JSONResult源碼的核心部分:

 

部分屬性


 

Java代碼
  收藏代碼
  1. private String defaultEncoding = "ISO-8859-1";//默認的編碼  
  2. private List<Pattern> includeProperties;//被包含的屬性的正則表達式,這些屬性的值將被序列化爲JSON字符串,傳送到客戶端  
  3. private List<Pattern> excludeProperties;//被排除的屬性的正則表達式,這些屬性的值在對象序列化時將被忽略  
  4. private String root;//根對象,即要被序列化的對象,如不指定,將序列化action中所有可被序列化的數據  
  5. private boolean wrapWithComments;//是否包裝成註釋  
  6. private boolean prefix;//前綴  
  7. private boolean enableGZIP = false;//是否壓縮  
  8. private boolean ignoreHierarchy = true;//是否忽略層次關係,即是否序列化對象父類中的屬性  
  9. private boolean ignoreInterfaces = true;//是否忽略接口  
  10. private boolean enumAsBean = false;//是否將枚舉類型作爲一個bean處理  
  11. private boolean excludeNullProperties = false;//是否排除空的屬性,即是否不序列化空值屬性  
  12. private int statusCode;//HTTP狀態碼  
  13. private int errorCode;//HTTP錯誤碼  
  14. private String contentType;//內容類型,通常爲application/json,在IE瀏覽器中會提示下載,可以通過參數配置<param name="contentType">text/html</param>,則不提示下載  
  15. private String wrapPrefix;//包裝前綴  
  16. private String wrapSuffix;//包裝後綴  

 

 

看一下上一篇文章中的相關參數配置:

 

Xml代碼
  收藏代碼
  1. <package name="json" extends="json-default" namespace="/test">  
  2.     <action name="testByAction"  
  3.             class="cn.ysh.studio.struts2.json.demo.action.UserAction" method="testByAction">  
  4.         <result type="json">  
  5.                 <!-- 這裏指定將被Struts2序列化的屬性,該屬性在action中必須有對應的getter方法 -->  
  6.                 <!-- 默認將會序列所有有返回值的getter方法的值,而無論該方法是否有對應屬性 -->  
  7.                 <param name="root">dataMap</param>  
  8.                 <!-- 指定是否序列化空的屬性 -->  
  9.                 <param name="excludeNullProperties">true</param>  
  10.                 <!-- 這裏指定將序列化dataMap中的那些屬性 -->  
  11.                 <param name="includeProperties">  
  12.                     user.*  
  13.                 </param>  
  14.                 <!-- 指定內容類型,默認爲application/json,IE瀏覽器會提示下載 -->  
  15.                 <param name="contentType">text/html</param>  
  16.                 <!-- 這裏指定將要從dataMap中排除那些屬性,這些排除的屬性將不被序列化,一半不與上邊的參數配置同時出現 -->  
  17.                 <param name="excludeProperties">  
  18.                     SUCCESS  
  19.                 </param>  
  20.         </result>  
  21.     </action>  
  22. </package>  

 

配置中出現了JSONResult的部分屬性名,是的,JSONResult中的屬性都可以根據需要在struts.xml中配置對應參數以改變默認值來滿足我們的需要。

 

 

接下來看看它的兩個核心方法:

 

 

Java代碼
  收藏代碼
  1. public void execute(ActionInvocation invocation) throws Exception {  
  2.         ActionContext actionContext = invocation.getInvocationContext();  
  3.         HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);  
  4.         HttpServletResponse response = (HttpServletResponse) actionContext.get(StrutsStatics.HTTP_RESPONSE);  
  5.   
  6.         try {  
  7.             String json;  
  8.             Object rootObject;  
  9.             //查找指定的需要序列化的對象,否則序列化整個action(上文包括前一篇文章中一提到過多次)  
  10.             if (this.enableSMD) {  
  11.                 // generate SMD  
  12.                 rootObject = this.writeSMD(invocation);  
  13.             } else {  
  14.                 // generate JSON  
  15.                 if (this.root != null) {  
  16.                     ValueStack stack = invocation.getStack();  
  17.                     rootObject = stack.findValue(this.root);  
  18.                 } else {  
  19.                     rootObject = invocation.getAction();  
  20.                 }  
  21.             }  
  22.             //這是最核心的一行代碼,包括瞭如何從rootObject抽取"可以"被序列化的屬性的值,然後包裝稱JSON字符串並返回  
  23.             json = JSONUtil.serialize(rootObject, excludeProperties, includeProperties, ignoreHierarchy,  
  24.                     enumAsBean, excludeNullProperties);  
  25.             //針對JSONP的一個成員方法  
  26.             json = addCallbackIfApplicable(request, json);  
  27.   
  28.             boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request);  
  29.   
  30.             //該方法是org.apache.struts2.json.JSONResult的一個成員方法,用於將JSON字符串根據指定參數包裝後發送到客戶端  
  31.             writeToResponse(response, json, writeGzip);  
  32.   
  33.         } catch (IOException exception) {  
  34.             LOG.error(exception.getMessage(), exception);  
  35.             throw exception;  
  36.         }  
  37.     }  
  38.   
  39.     /** 
  40.      * 負責根據相關參數配置,將制定JSON字符串發送到客戶端 
  41.      * @param response 
  42.      * @param json 
  43.      * @param gzip 
  44.      * @throws IOException 
  45.      */  
  46.     protected void writeToResponse(HttpServletResponse response, String json, boolean gzip)  
  47.             throws IOException {  
  48.         JSONUtil.writeJSONToResponse(new SerializationParams(response, getEncoding(), isWrapWithComments(),  
  49.                 json, false, gzip, noCache, statusCode, errorCode, prefix, contentType, wrapPrefix,  
  50.                 wrapSuffix));  
  51.     }  

 

恕筆者愚鈍,找了好多資料,始終不明白這裏的"SMD"是個什麼意思,所在這裏包括下文,都將忽略"SMD"。

 

 

可以看到,Struts2序列化對象爲JSON字符串的整個過程都被JSONUtil的serialize方法包辦了,所以有必要跟入這個方法一探究竟:

 

Java代碼
  收藏代碼
  1. /** 
  2.  * Serializes an object into JSON, excluding any properties matching any of 
  3.  * the regular expressions in the given collection. 
  4.  *  
  5.  * @param object 
  6.  *            to be serialized 
  7.  * @param excludeProperties 
  8.  *            Patterns matching properties to exclude 
  9.  * @param ignoreHierarchy 
  10.  *            whether to ignore properties defined on base classes of the 
  11.  *            root object 
  12.  * @param enumAsBean 
  13.  *            whether to serialized enums a Bean or name=value pair 
  14.  * @return JSON string 
  15.  * @throws JSONException 
  16.  */  
  17. public static String serialize(Object object, Collection<Pattern> excludeProperties,  
  18.         Collection<Pattern> includeProperties, boolean ignoreHierarchy, boolean enumAsBean,  
  19.         boolean excludeNullProperties) throws JSONException {  
  20.     JSONWriter writer = new JSONWriter();  
  21.     writer.setIgnoreHierarchy(ignoreHierarchy);  
  22.     writer.setEnumAsBean(enumAsBean);  
  23.     return writer.write(object, excludeProperties, includeProperties, excludeNullProperties);  
  24. }  

 

 

該方法還有一個重載的兄弟方法,只是少了boolean enumAsBean這個參數,我們並不關心它,這裏不討論它。可以看到,這個方法更簡單:構建一個JSONWriter實例,注入兩個參數,然後調用該實例的write方法。我們進入JSONWriter,查看write方法的源碼:

 

 

Java代碼
  收藏代碼
  1. /** 
  2.  * @param object 
  3.  *            Object to be serialized into JSON 
  4.  * @return JSON string for object 
  5.  * @throws JSONException 
  6.  */  
  7. public String write(Object object, Collection<Pattern> excludeProperties,  
  8.         Collection<Pattern> includeProperties, boolean excludeNullProperties) throws JSONException {  
  9.     this.excludeNullProperties = excludeNullProperties;  
  10.     this.buf.setLength(0);  
  11.     this.root = object;  
  12.     this.exprStack = "";  
  13.     this.buildExpr = ((excludeProperties != null) && !excludeProperties.isEmpty())  
  14.             || ((includeProperties != null) && !includeProperties.isEmpty());  
  15.     this.excludeProperties = excludeProperties;  
  16.     this.includeProperties = includeProperties;  
  17.     this.value(object, null);  
  18.   
  19.     return this.buf.toString();  
  20. }  

 

 

它同樣有一個重載的方法,我們同樣不關心,瀏覽整個方法,不難發現,它只是所做了一些賦值操作,然後將對象的序列化工作交給了value成員方法,那麼我們進入value方法看一看: 

 

 

Java代碼
  收藏代碼
  1. /** 
  2.  * Detect cyclic references 
  3.  */  
  4. private void value(Object object, Method method) throws JSONException {  
  5.     if (object == null) {  
  6.         this.add("null");  
  7.   
  8.         return;  
  9.     }  
  10.   
  11.     if (this.stack.contains(object)) {  
  12.         Class clazz = object.getClass();  
  13.   
  14.         // cyclic reference  
  15.         if (clazz.isPrimitive() || clazz.equals(String.class)) {  
  16.             this.process(object, method);  
  17.         } else {  
  18.             if (LOG.isDebugEnabled()) {  
  19.                 LOG.debug("Cyclic reference detected on " + object);  
  20.             }  
  21.   
  22.             this.add("null");  
  23.         }  
  24.   
  25.         return;  
  26.     }  
  27.   
  28.     this.process(object, method);  
  29. }  

 

 

很簡潔,進入process方法

 

 

Java代碼
  收藏代碼
  1. /** 
  2.  * Serialize object into json 
  3.  */  
  4. private void process(Object object, Method method) throws JSONException {  
  5.     this.stack.push(object);  
  6.   
  7.     if (object instanceof Class) {  
  8.         this.string(object);  
  9.     } else if (object instanceof Boolean) {  
  10.         this.bool(((Boolean) object).booleanValue());  
  11.     } else if (object instanceof Number) {  
  12.         this.add(object);  
  13.     } else if (object instanceof String) {  
  14.         this.string(object);  
  15.     } else if (object instanceof Character) {  
  16.         this.string(object);  
  17.     } else if (object instanceof Map) {  
  18.         this.map((Map) object, method);  
  19.     } else if (object.getClass().isArray()) {  
  20.         this.array(object, method);  
  21.     } else if (object instanceof Iterable) {  
  22.         this.array(((Iterable) object).iterator(), method);  
  23.     } else if (object instanceof Date) {  
  24.         this.date((Date) object, method);  
  25.     } else if (object instanceof Calendar) {  
  26.         this.date(((Calendar) object).getTime(), method);  
  27.     } else if (object instanceof Locale) {  
  28.         this.string(object);  
  29.     } else if (object instanceof Enum) {  
  30.         this.enumeration((Enum) object);  
  31.     } else {  
  32.         this.bean(object);  
  33.     }  
  34.   
  35.     this.stack.pop();  
  36. }  

 

 

發現它做了很多判斷,並結合不同的方法來支持不同的數據類型,那麼從這裏我們可以知道Struts-json-plugin支持哪些數據類型了。對於每一種支持的數據類型,Struts-json-plugin都有相應的方法來從從對象中抽取數據並封裝成JSON字符串,以Map爲例,我們看一下map方法的源碼:

 

 

Java代碼
  收藏代碼
  1.  /** 
  2.   * Add map to buffer 
  3.   */  
  4.  private void map(Map map, Method method) throws JSONException {  
  5.      //這是一個對象,按照JSON語法,應該以"{}"括起來  
  6. his.add("{");  
  7.   
  8.      Iterator it = map.entrySet().iterator();  
  9.   
  10.      boolean warnedNonString = false// one report per map  
  11.      boolean hasData = false;  
  12.      while (it.hasNext()) {  
  13.          Map.Entry entry = (Map.Entry) it.next();  
  14. //如果key不是String類型,將發出警告  
  15.          Object key = entry.getKey();  
  16. //當前屬性的OGNL表達式  
  17.          String expr = null;  
  18.          if (this.buildExpr) {  
  19.              if (key == null) {  
  20.                  LOG.error("Cannot build expression for null key in " + this.exprStack);  
  21.                  continue;  
  22.              } else {  
  23.         //獲取完整的OGNL表達式  
  24.                  expr = this.expandExpr(key.toString());  
  25.         //是否是被排除的屬性  
  26.         //如果你對上邊生成的OGNL表達式的格式有所瞭解,那麼includeProperties和excludeProperties的正則配置絕對不是問題  
  27.                  if (this.shouldExcludeProperty(expr)) {  
  28.                      continue;  
  29.                  }  
  30.         //如果不被排除,則將當前屬性名壓入表達式棧(其實就是一個String而非傳統意義上的棧,此處是模擬,非常精巧的算法)  
  31.         //該方法返回原來的表達式,稍後還將恢復該表達式到"棧"中  
  32.                  expr = this.setExprStack(expr);  
  33.              }  
  34.          }  
  35. //如果還有數據,則以","風格,這是JSON的語法格式  
  36.          if (hasData) {  
  37.              this.add(',');  
  38.          }  
  39.          hasData = true;  
  40. //如果key不是String類型,將發出警告,且只警告一次  
  41.          if (!warnedNonString && !(key instanceof String)) {  
  42.              LOG.warn("JavaScript doesn't support non-String keys, using toString() on "  
  43.                      + key.getClass().getName());  
  44.              warnedNonString = true;  
  45.          }  
  46.          this.value(key.toString(), method);  
  47.          this.add(":");  
  48. //遞歸抽取數據  
  49.          this.value(entry.getValue(), method);  
  50. //下一層的數據遞歸完成後,恢復表達式棧值爲當前層的屬性名  
  51.          if (this.buildExpr) {  
  52.              this.setExprStack(expr);  
  53.          }  
  54.      }  
  55.   
  56.      this.add("}");  
  57.  }  

 

這個方法中比較重要的幾行代碼都做了註釋,不再贅述。過濾某些屬性,以使其不被序列化時struts2-JSON應用中非常常見的,比如在序列化一個用戶對象的時候,密碼信息時不應該被傳送到客戶端的,所以要排除掉。瞭解shouldExcludeProperty方法的過濾規則,可以幫助我們更好的使用此功能。源碼如下:

 

Java代碼
  收藏代碼
  1. private boolean shouldExcludeProperty(String expr) {  
  2.         if (this.excludeProperties != null) {  
  3.             for (Pattern pattern : this.excludeProperties) {  
  4.                 if (pattern.matcher(expr).matches()) {  
  5.                     if (LOG.isDebugEnabled())  
  6.                         LOG.debug("Ignoring property because of exclude rule: " + expr);  
  7.                     return true;  
  8.                 }  
  9.             }  
  10.         }  
  11.   
  12.         if (this.includeProperties != null) {  
  13.             for (Pattern pattern : this.includeProperties) {  
  14.                 if (pattern.matcher(expr).matches()) {  
  15.                     return false;  
  16.                 }  
  17.             }  
  18.   
  19.             if (LOG.isDebugEnabled())  
  20.                 LOG.debug("Ignoring property because of include rule:  " + expr);  
  21.             return true;  
  22.         }  
  23.   
  24.         return false;  
  25.     }  

 

非常簡單,就是簡單的正則匹配,如果有排除配置,則先判斷當前屬性是否被排除,如果沒有被排除,且有包含配置則檢查是否被包含,如果沒有被包含,則不序列化該屬性,如果沒有被排除且沒有包含配置,則將序列化該屬性。

源碼跟蹤到這裏,已經沒有繼續下去的必要了,因爲我們已經很清楚Struts2是如何將一個對象轉換成JSON字符串並返回客戶端的:


1、收集用戶配置;
2、JSONWriter通過判斷對象的類型來有針對性的抽取其中的屬性值,對於嵌套的對象則採用遞歸的方式來抽取,抽取的同時,包裝成符合JSON語法規範的字符串;
3、JSONUtil.writeJSONToResponse將序列化的JSON字符串按照相關配置發送到客戶端;
 
  
不難看出,代碼邏輯清晰,簡單,樸素,沒有半點花巧和賣弄,但確實是非常的精巧,表現出作者紮實的編程功底和過人的邏輯思維能力。尤其是遞歸抽取嵌套對象的屬性值和獲取當前屬性的OGNL表達式的算法,堪稱經典!

 

通過以上的源碼跟蹤,我們很清楚的瞭解Struts2序列化對象的原理和過程,並對相關參數的配置有了深刻的體會。只是令人感到奇怪的是,他並沒有使用json-lib.xx.jar中的API接口,而是以字符串拼接的方式手動構建JSON字符串,我想原因可能是因爲它要用正則表達式包含或排除某些屬性的原因吧,僅作猜測,還望高人指點。

 

有很多人說不知道includeProperties和excludeProperties的正則表達式該怎麼配置,我想說其實很簡單,除了正則知識外,就是"對象名.屬性名",數組稍微不同,以爲它有下標,所以是"數組對象名\[\d+\]\.屬性名"。如果這裏覺得說的不清楚,可以閱讀以下JSONWriter中關於OGNL表達式是如何獲取的部分代碼,就會明白正則該如何寫了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章