前面一篇文章其實只是介紹瞭如何在Struts2中返回JSON數據到客戶端的具體範例而無關其原理,內容與標題不符惹來標題黨嫌疑確實是筆者發文不夠嚴謹,目前已修改標題,與內容匹配。本文將從struts2-json插件的源碼角度出發,結合以前的應用範例來講明struts2-json插件返回JSON數據的原理。 html
用winrar打開struts2-json-plugin-xx.jar(筆者使用版本爲2.1.8.1),根目錄下有一個struts-plugin.xml,這個文件想必你們都很瞭解,不作過多介紹了。打開該文件,內容很是簡答,以下: java
Xml代碼
- <?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
部分屬性 編程
Java代碼
- 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
Xml代碼
- <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中配置對應參數以改變默認值來知足咱們的須要。 數組
接下來看看它的兩個核心方法: 瀏覽器
Java代碼
- 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方法包辦了,因此有必要跟入這個方法一探究竟:
Java代碼
- /**
- * 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方法的源碼:
Java代碼
- /**
- * @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方法看一看:
Java代碼
- /**
- * 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方法
Java代碼
- /**
- * 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方法的源碼:
Java代碼
- /**
- * Add map to buffer
- */
- private void map(Map map, Method method) throws JSONException {
- //這是一個對象,按照JSON語法,應該以"{}"括起來
- his.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方法的過濾規則,能夠幫助咱們更好的使用此功能。源碼以下:
Java代碼
- 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表達式是如何獲取的部分代碼,就會明白正則該如何寫了。
純屬我的理解,若有錯誤,煩請指正,不勝榮幸!
轉載自 : http://www.yshjava.cn/post/329.html