文將從struts2-json插件的源碼角度出發,結合以前的應用範例來講明struts2-json插件返回JSON數據的原理。html
用winrar打開struts2-json-plugin-xx.jar(筆者使用版本爲2.1.8.1),根目錄下有一個struts-plugin.xml,這個文件想必你們都很瞭解,不作過多介紹了。打開該文件,內容很是簡答,以下:java
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.0.dtd"> <struts> <package name="json-default" extends="struts-default"> <result-types> <result-type name="json" class="org.apache.struts2.json.JSONResult"/> </result-types> <interceptors> <interceptor name="json" class="org.apache.struts2.json.JSONInterceptor"/> </interceptors> </package> </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插件執行的流程:算法
下面重點說說org.apache.struts2.json.JSONResult。express
首先看一下org.apache.struts2.json.JSONResult源碼的核心部分:apache
部分屬性編程
private String defaultEncoding = "ISO-8859-1";//默認的編碼 private List<Pattern> includeProperties;//被包含的屬性的正則表達式,這些屬性的值將被序列化爲JSON字符串,傳送到客戶端 private List<Pattern> excludeProperties;//被排除的屬性的正則表達式,這些屬性的值在對象序列化時將被忽略 private String root;//根對象,即要被序列化的對象,如不指定,將序列化action中全部可被序列化的數據 private boolean wrapWithComments;//是否包裝成註釋 private boolean prefix;//前綴 private boolean enableGZIP = false;//是否壓縮 private boolean ignoreHierarchy = true;//是否忽略層次關係,便是否序列化對象父類中的屬性 private boolean ignoreInterfaces = true;//是否忽略接口 private boolean enumAsBean = false;//是否將枚舉類型做爲一個bean處理 private boolean excludeNullProperties = false;//是否排除空的屬性,便是否不序列化空值屬性 private int statusCode;//HTTP狀態碼 private int errorCode;//HTTP錯誤碼 private String contentType;//內容類型,一般爲application/json,在IE瀏覽器中會提示下載,能夠經過參數配置<param name="contentType">text/html</param>,則不提示下載 private String wrapPrefix;//包裝前綴 private String wrapSuffix;//包裝後綴
看一下上一篇文章中的相關參數配置:json
<package name="json" extends="json-default" namespace="/test"> <action name="testByAction" class="cn.ysh.studio.struts2.json.demo.action.UserAction" method="testByAction"> <result type="json"> <!-- 這裏指定將被Struts2序列化的屬性,該屬性在action中必須有對應的getter方法 --> <!-- 默認將會序列全部有返回值的getter方法的值,而不管該方法是否有對應屬性 --> <param name="root">dataMap</param> <!-- 指定是否序列化空的屬性 --> <param name="excludeNullProperties">true</param> <!-- 這裏指定將序列化dataMap中的那些屬性 --> <param name="includeProperties"> user.* </param> <!-- 指定內容類型,默認爲application/json,IE瀏覽器會提示下載 --> <param name="contentType">text/html</param> <!-- 這裏指定將要從dataMap中排除那些屬性,這些排除的屬性將不被序列化,一半不與上邊的參數配置同時出現 --> <param name="excludeProperties"> SUCCESS </param> </result> </action> </package>
配置中出現了JSONResult的部分屬性名,是的,JSONResult中的屬性均可以根據須要在struts.xml中配置對應參數以改變默認值來知足咱們的須要。數組
接下來看看它的兩個核心方法:瀏覽器
public void execute(ActionInvocation invocation) throws Exception { ActionContext actionContext = invocation.getInvocationContext(); HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST); HttpServletResponse response = (HttpServletResponse) actionContext.get(StrutsStatics.HTTP_RESPONSE); try { String json; Object rootObject; //查找指定的須要序列化的對象,不然序列化整個action(上文包括前一篇文章中一提到過屢次) if (this.enableSMD) { // generate SMD rootObject = this.writeSMD(invocation); } else { // generate JSON if (this.root != null) { ValueStack stack = invocation.getStack(); rootObject = stack.findValue(this.root); } else { rootObject = invocation.getAction(); } } //這是最核心的一行代碼,包括瞭如何從rootObject抽取"能夠"被序列化的屬性的值,而後包裝稱JSON字符串並返回 json = JSONUtil.serialize(rootObject, excludeProperties, includeProperties, ignoreHierarchy, enumAsBean, excludeNullProperties); //針對JSONP的一個成員方法 json = addCallbackIfApplicable(request, json); boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request); //該方法是org.apache.struts2.json.JSONResult的一個成員方法,用於將JSON字符串根據指定參數包裝後發送到客戶端 writeToResponse(response, json, writeGzip); } catch (IOException exception) { LOG.error(exception.getMessage(), exception); throw exception; } } /** * 負責根據相關參數配置,將制定JSON字符串發送到客戶端 * @param response * @param json * @param gzip * @throws IOException */ protected void writeToResponse(HttpServletResponse response, String json, boolean gzip) throws IOException { JSONUtil.writeJSONToResponse(new SerializationParams(response, getEncoding(), isWrapWithComments(), json, false, gzip, noCache, statusCode, errorCode, prefix, contentType, wrapPrefix, wrapSuffix)); }
恕筆者愚鈍,找了好多資料,始終不明白這裏的"SMD"是個什麼意思,所在這裏包括下文,都將忽略"SMD"。
能夠看到,Struts2序列化對象爲JSON字符串的整個過程都被JSONUtil的serialize方法包辦了,因此有必要跟入這個方法一探究竟:
/** * Serializes an object into JSON, excluding any properties matching any of * the regular expressions in the given collection. * * @param object * to be serialized * @param excludeProperties * Patterns matching properties to exclude * @param ignoreHierarchy * whether to ignore properties defined on base classes of the * root object * @param enumAsBean * whether to serialized enums a Bean or name=value pair * @return JSON string * @throws JSONException */ public static String serialize(Object object, Collection<Pattern> excludeProperties, Collection<Pattern> includeProperties, boolean ignoreHierarchy, boolean enumAsBean, boolean excludeNullProperties) throws JSONException { JSONWriter writer = new JSONWriter(); writer.setIgnoreHierarchy(ignoreHierarchy); writer.setEnumAsBean(enumAsBean); return writer.write(object, excludeProperties, includeProperties, excludeNullProperties); }
該方法還有一個重載的兄弟方法,只是少了boolean enumAsBean這個參數,咱們並不關心它,這裏不討論它。能夠看到,這個方法更簡單:構建一個JSONWriter實例,注入兩個參數,而後調用該實例的write方法。咱們進入JSONWriter,查看write方法的源碼:
/** * @param object * Object to be serialized into JSON * @return JSON string for object * @throws JSONException */ public String write(Object object, Collection<Pattern> excludeProperties, Collection<Pattern> includeProperties, boolean excludeNullProperties) throws JSONException { this.excludeNullProperties = excludeNullProperties; this.buf.setLength(0); this.root = object; this.exprStack = ""; this.buildExpr = ((excludeProperties != null) && !excludeProperties.isEmpty()) || ((includeProperties != null) && !includeProperties.isEmpty()); this.excludeProperties = excludeProperties; this.includeProperties = includeProperties; this.value(object, null); return this.buf.toString(); }
它一樣有一個重載的方法,咱們一樣不關心,瀏覽整個方法,不難發現,它只是所作了一些賦值操做,而後將對象的序列化工做交給了value成員方法,那麼咱們進入value方法看一看:
/** * Detect cyclic references */ private void value(Object object, Method method) throws JSONException { if (object == null) { this.add("null"); return; } if (this.stack.contains(object)) { Class clazz = object.getClass(); // cyclic reference if (clazz.isPrimitive() || clazz.equals(String.class)) { this.process(object, method); } else { if (LOG.isDebugEnabled()) { LOG.debug("Cyclic reference detected on " + object); } this.add("null"); } return; } this.process(object, method); }
很簡潔,進入process方法:
/** * Serialize object into json */ private void process(Object object, Method method) throws JSONException { this.stack.push(object); if (object instanceof Class) { this.string(object); } else if (object instanceof Boolean) { this.bool(((Boolean) object).booleanValue()); } else if (object instanceof Number) { this.add(object); } else if (object instanceof String) { this.string(object); } else if (object instanceof Character) { this.string(object); } else if (object instanceof Map) { this.map((Map) object, method); } else if (object.getClass().isArray()) { this.array(object, method); } else if (object instanceof Iterable) { this.array(((Iterable) object).iterator(), method); } else if (object instanceof Date) { this.date((Date) object, method); } else if (object instanceof Calendar) { this.date(((Calendar) object).getTime(), method); } else if (object instanceof Locale) { this.string(object); } else if (object instanceof Enum) { this.enumeration((Enum) object); } else { this.bean(object); } this.stack.pop(); }
發現它作了不少判斷,並結合不一樣的方法來支持不一樣的數據類型,那麼從這裏咱們能夠知道Struts-json-plugin支持哪些數據類型了。對於每一種支持的數據類型,Struts-json-plugin都有相應的方法來從從對象中抽取數據並封裝成JSON字符串,以Map爲例,咱們看一下map方法的源碼:
/** * Add map to buffer */ private void map(Map map, Method method) throws JSONException { //這是一個對象,按照JSON語法,應該以"{}"括起來 this.add("{"); Iterator it = map.entrySet().iterator(); boolean warnedNonString = false; // one report per map boolean hasData = false; while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); //若是key不是String類型,將發出警告 Object key = entry.getKey(); //當前屬性的OGNL表達式 String expr = null; if (this.buildExpr) { if (key == null) { LOG.error("Cannot build expression for null key in " + this.exprStack); continue; } else { //獲取完整的OGNL表達式 expr = this.expandExpr(key.toString()); //是不是被排除的屬性 //若是你對上邊生成的OGNL表達式的格式有所瞭解,那麼includeProperties和excludeProperties的正則配置絕對不是問題 if (this.shouldExcludeProperty(expr)) { continue; } //若是不被排除,則將當前屬性名壓入表達式棧(其實就是一個String而非傳統意義上的棧,此處是模擬,很是精巧的算法) //該方法返回原來的表達式,稍後還將恢復該表達式到"棧"中 expr = this.setExprStack(expr); } } //若是還有數據,則以","風格,這是JSON的語法格式 if (hasData) { this.add(','); } hasData = true; //若是key不是String類型,將發出警告,且只警告一次 if (!warnedNonString && !(key instanceof String)) { LOG.warn("JavaScript doesn't support non-String keys, using toString() on " + key.getClass().getName()); warnedNonString = true; } this.value(key.toString(), method); this.add(":"); //遞歸抽取數據 this.value(entry.getValue(), method); //下一層的數據遞歸完成後,恢復表達式棧值爲當前層的屬性名 if (this.buildExpr) { this.setExprStack(expr); } } this.add("}"); }
這個方法中比較重要的幾行代碼都作了註釋,再也不贅述。過濾某些屬性,以使其不被序列化時struts2-JSON應用中很是常見的,好比在序列化一個用戶對象的時候,密碼信息時不該該被傳送到客戶端的,因此要排除掉。瞭解shouldExcludeProperty方法的過濾規則,能夠幫助咱們更好的使用此功能。源碼以下:
private boolean shouldExcludeProperty(String expr) { if (this.excludeProperties != null) { for (Pattern pattern : this.excludeProperties) { if (pattern.matcher(expr).matches()) { if (LOG.isDebugEnabled()) LOG.debug("Ignoring property because of exclude rule: " + expr); return true; } } } if (this.includeProperties != null) { for (Pattern pattern : this.includeProperties) { if (pattern.matcher(expr).matches()) { return false; } } if (LOG.isDebugEnabled()) LOG.debug("Ignoring property because of include rule: " + expr); return true; } return false; }
很是簡單,就是簡單的正則匹配,若是有排除配置,則先判斷當前屬性是否被排除,若是沒有被排除,且有包含配置則檢查是否被包含,若是沒有被包含,則不序列化該屬性,若是沒有被排除且沒有包含配置,則將序列化該屬性。
源碼跟蹤到這裏,已經沒有繼續下去的必要了,由於咱們已經很清楚Struts2是如何將一個對象轉換成JSON字符串並返回客戶端的:
一、收集用戶配置;
二、JSONWriter經過判斷對象的類型來有針對性的抽取其中的屬性值,對於嵌套的對象則採用遞歸的方式來抽取,抽取的同時,包裝成符合JSON語法規範的字符串;
三、JSONUtil.writeJSONToResponse將序列化的JSON字符串按照相關配置發送到客戶端;
不難看出,代碼邏輯清晰,簡單,樸素,沒有半點花巧和賣弄,但確實是很是的精巧,表現出做者紮實的編程功底和過人的邏輯思惟能力。尤爲是遞歸抽取嵌套對象的屬性值和獲取當前屬性的OGNL表達式的算法,堪稱經典!
經過以上的源碼跟蹤,咱們很清楚的瞭解Struts2序列化對象的原理和過程,並對相關參數的配置有了深入的體會。只是使人感到奇怪的是,他並無使用json-lib.xx.jar中的API接口,而是以字符串拼接的方式手動構建JSON字符串,我想緣由多是由於它要用正則表達式包含或排除某些屬性的緣由吧,僅做猜想,還望高人指點。
有不少人說不知道includeProperties和excludeProperties的正則表達式該怎麼配置,我想說其實很簡單,除了正則知識外,就是"對象名.屬性名",數組稍微不一樣,覺得它有下標,因此是"數組對象名\[\d+\]\.屬性名"。若是這裏以爲說的不清楚,能夠閱讀如下JSONWriter中關於OGNL表達式是如何獲取的部分代碼,就會明白正則該如何寫了。
純屬我的理解,若有錯誤,煩請指正,不勝榮幸!