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

文將從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插件執行的流程:算法

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表達式是如何獲取的部分代碼,就會明白正則該如何寫了。

 

純屬我的理解,若有錯誤,煩請指正,不勝榮幸!

相關文章
相關標籤/搜索