2009年3月28日 星期六

[Struts2] 宣告與設定 Interceptors

我們已經知道 interceptor 是如何的運作以及 Struts2 framework 中已經幫我們建立了哪些的 interceptors。現在我們就要深入探討關於設定已經宣告我們的 interceptors!
在 Struts2 framework 中已經提供了 struts-default.xml 幫助我們設定了一些內建的 interceptors 以及 interceptor stack,這讓我們減輕了不少的工作,不過我們在開發大型的系統時,這些預設的設定檔可能不太適用,所以我們還是要知道怎麼樣去自己設定 interceptors。

宣告與設定 interceptors
基本上來說,interceptor 的設定包含了兩個部份:interceptor 的宣告與 interceptor stack 的設定。我們就參考 struts-default.xml 中的 interceptor:
<package name="struts-default">

...

<interceptors>
<interceptor name="alias" class="com.opensymphony.xwork2.interceptor.AliasInterceptor"/>
<interceptor name="autowiring" class="com.opensymphony.xwork2.spring.interceptor.ActionAutowiringInterceptor"/>
<interceptor name="chain" class="com.opensymphony.xwork2.interceptor.ChainingInterceptor"/>
<interceptor name="conversionError" class="org.apache.struts2.interceptor.StrutsConversionErrorInterceptor"/>
<interceptor name="createSession" class="org.apache.struts2.interceptor.CreateSessionInterceptor" />
<interceptor name="debugging" class="org.apache.struts2.interceptor.debugging.DebuggingInterceptor" />
<interceptor name="externalRef" class="com.opensymphony.xwork2.interceptor.ExternalReferencesInterceptor"/>
<interceptor name="execAndWait" class="org.apache.struts2.interceptor.ExecuteAndWaitInterceptor"/>
<interceptor name="exception" class="com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor"/>
<interceptor name="fileUpload" class="org.apache.struts2.interceptor.FileUploadInterceptor"/>
<interceptor name="i18n" class="com.opensymphony.xwork2.interceptor.I18nInterceptor"/>
<interceptor name="logger" class="com.opensymphony.xwork2.interceptor.LoggingInterceptor"/>
<interceptor name="modelDriven" class="com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor"/>
<interceptor name="scopedModelDriven" class="com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor"/>
<interceptor name="params" class="com.opensymphony.xwork2.interceptor.ParametersInterceptor"/>
<interceptor name="prepare" class="com.opensymphony.xwork2.interceptor.PrepareInterceptor"/>
<interceptor name="staticParams" class="com.opensymphony.xwork2.interceptor.StaticParametersInterceptor"/>
<interceptor name="scope" class="org.apache.struts2.interceptor.ScopeInterceptor"/>
<interceptor name="servletConfig" class="org.apache.struts2.interceptor.ServletConfigInterceptor"/>
<interceptor name="sessionAutowiring" class="org.apache.struts2.spring.interceptor.SessionContextAutowiringInterceptor"/>
<interceptor name="timer" class="com.opensymphony.xwork2.interceptor.TimerInterceptor"/>
<interceptor name="token" class="org.apache.struts2.interceptor.TokenInterceptor"/>
<interceptor name="tokenSession" class="org.apache.struts2.interceptor.TokenSessionStoreInterceptor"/>
<interceptor name="validation" class="org.apache.struts2.interceptor.validation.AnnotationValidationInterceptor"/>
<interceptor name="workflow" class="com.opensymphony.xwork2.interceptor.DefaultWorkflowInterceptor"/>
<interceptor name="store" class="org.apache.struts2.interceptor.MessageStoreInterceptor" />
<interceptor name="checkbox" class="org.apache.struts2.interceptor.CheckboxInterceptor" />
<interceptor name="profiling" class="org.apache.struts2.interceptor.ProfilingActivationInterceptor" />
<interceptor name="roles" class="org.apache.struts2.interceptor.RolesInterceptor" />
<interceptor name="jsonValidation" class="org.apache.struts2.interceptor.validation.JSONValidationInterceptor" />
<interceptor name="annotationWorkflow" class="com.opensymphony.xwork2.interceptor.annotations.AnnotationWorkflowInterceptor" />

...

<interceptor-stack name="defaultStack">
<interceptor-ref name="exception"/>
<interceptor-ref name="alias"/>
<interceptor-ref name="servletConfig"/>
<interceptor-ref name="prepare"/>
<interceptor-ref name="i18n"/>
<interceptor-ref name="chain"/>
<interceptor-ref name="debugging"/>
<interceptor-ref name="profiling"/>
<interceptor-ref name="scopedModelDriven"/>
<interceptor-ref name="modelDriven"/>
<interceptor-ref name="fileUpload"/>
<interceptor-ref name="checkbox"/>
<interceptor-ref name="staticParams"/>
<interceptor-ref name="params">
<param name="excludeParams">dojo\..*</param>
</interceptor-ref>
<interceptor-ref name="conversionError"/>
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="workflow">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
</interceptor-stack>

...

</interceptors>
<default-interceptor-ref name="defaultStack"/>

...

</package>

由上面的設定中我們可以看到,整個 interceptors 是宣告在 interceptor tag 之中,而且如同其他 framework components 一樣,這些設定都是屬於 package tag 之下!
在 interceptors tag 之中包含了兩組 tags:interceptor 與 interceptor-stack,我們可以從 tag 的名稱很容易瞭解其功能。
首先,我們透過 interceptor tag 來宣告我們擁有的 interceptors,當然,interceptors 要先被宣告才能使用,這就跟 programming language 一樣!而且宣告 interceptor 是很簡單的,我們透過 name attribute 給予該 interceptor 一個宣告的名稱,就像程式中宣告變數的名稱一樣;再來我們就是透過 class attribute 告知 framework,該 interceptor 所屬的 class 位置,這個位置一定要包含完整的 package name 與 class name(這裡說得是 class 位置,而不是檔案位置喔!所以最後面不需要加上 .java)!
宣告了 interceptors 之後,我們就可以設定我們的 interceptor stack,透過 interceptor-stack tag 來宣告一組 stack。這裡由於原始的 struts-default.xml 中的設定太過於龐大,所以我們只有顯示 defaultStack 的 interceptor stack。每一組 interceptor stack 我們都要給予一個唯一的 name attribute,而一個 interceptor stack 中都包含了一系列有順序性的 interceptor-ref tag,這個 tag 也是很容易瞭解的,interceptor-ref tag 就是用來參考(reference) 我們所宣告的 interceptors,所以我們在 interceptor-ref tag 中設定 name attribute 來參考到我們所宣告的 interceptors。
請注意剛才我所說得"有順序性",因為我們所宣告在 interceptor-stack tag 中的 interceptor ref 順序是在將來系統執行時由上而下的來執行!所以如果有某些順序被更動,可能會造成整個 interceptor stack 的不同。如果我們將 param interceptor 移到 workflow interceptor 之後,那我們就無法在 workflow interceptor 中使用param tag 來給予設定參數,因為 framework 對於 param tag 的執行是透過 param interceptor 來進行的,所以 param interceptor 要先被呼叫後,才能在之後的 interceptors 使用 param tag 進行參數設定!
當我們宣告完成 interceptors 以及設定好 interceptor stack 後,我們就要設定 framework 中預設使用的 interceptor stack 了!這個可以透過 default-interceptor-ref tag 來設定,我們透過給予 default-interceptor-ref tag 中的 name attribute 來告知 framework,我們要使用的 default interceptor stack!

針對每個 Action 設定特定的 interceptors
許多時候我們的 Action 都是繼承自其所屬 package 中對於 interceptors 的設定,不過也有時候我們可能會需要將某些 interceptors 用在某個 Action 呼叫之前,在 Struts2 framework 中提供了一個很方便的設定方式,讓我們可以為某一個 Action 給予特定的 interceptors,讓我們看看下面的設定:
<action name="MyAction" class="silver8250.test.MyAction">
<interceptor-ref name="timer" />
<interceptor-ref name="logger" />
<result>/WEB-INF/pages/finish.jsp</result>
</action>

這個設定中告知 Struts2 framework,我們有一個 MyAction 的 Action,要使用 timer 與 logger interceptor,而且 interceptor 呼叫的順序是依照我們的排列,不過我們的 MyAction 是不會有其他的 interceptors 了!這點要注意,如果我們採用 Action 中自己定義的 interceptors,那我們就會失去原先在設定檔中設定的 interceptor stack 了!不過為了讓我們可以很方便的 reuse 原先我們設定的 interceptor stack,我們可以直接在 interceptor-ref tag 中設定某個已經存在的 interceptor stack,如下:
這種設定方式讓我們可以很方便的 reuse 某個 interceptor stack,當然,我們可以安排不一樣的順序!
<action name="MyAction" class="silver8250.test.MyAction">
<interceptor-ref name="timer" />
<interceptor-ref name="logger" />
<interceptor-ref name="defaultStack" />
<result>/WEB-INF/pages/finish.jsp</result>
</action>

Action 中 Overriding interceptor 的參數
上面的方法是為某個 Action 設定不同的 interceptor stack,現在我們則是要修改 Action 的 interceptor stack 中的某個參數,先看看下面的設定:

在這裡的 MyAction 我們一樣給予一個我們自己設定的 interceptor stack,只不過是 reuse defaultStack,這樣的方式讓我們可以調整我們 reuse 的 interceptor stack,就如同上面所設定的:透過 param tag 告知 framework,目前 MyAction 是要使用 defaultStack 中定義的 interceptor stack,不過對於 workflow interceptor,我們要設定 excludeMethods 改為 someMethod 來取代原先設定的參數!
這種方式讓我們可以為某個 Action 調整特定的 interceptor stack,當然,我們也可以將這種方法結合上面的方法來完成更多不同的 interceptor stack。
<action name="MyAction" class="silver8250.test.MyAction">
<interceptor-ref name="defaultStack">
<param name="workflow.excludeMethods">someMethod</param>
</interceptor-ref>
<result>/WEB-INF/pages/finish.jsp</result>
</action>

在這篇中我們深入了描述了 interceptor 的設定與宣告,我們可以為一整個 package 中的 Actions 設定共用的 interceptor stack,也可以特定位某個 Action 設定特別的 interceptor stack,也可以 reuse 某個 interceptor stack 並且調整不同的設定參數,甚至我們可以結合這兩種方式來提供更好用的 interceptor stack!

2009年3月19日 星期四

[Struts2] 內建的 Interceptors

在 Struts2 framework 中,擁有許多的內建 Interceptors,這讓 programmers 可以很方便的 reuse,甚至我們根本就不需要開發自己的 interceptors,因為內建的 interceptors 已經做了大部分的工作!這種好處不僅僅在 interceptor 中才有,其他的 Struts2 components 也有這樣的特性:framework 已經內建好很多很好用的 components,讓我們很專心的在開發我們的 business logic。

要知道 Struts2 中內建有哪些的 interceptors,我們可以從 struts-default.xml 中得到資訊!以下我只會簡單的介紹幾個比較重要的 interceptors,剩下的就留給有興趣的你來 survey。

1. Utility Interceptors

首先,我們先看看一些工具類的 interceptors,這類的 interceptors 主要是幫助 programmer 來開發、調校與找 bug 所使用的。

這個 interceptor 很直觀就是用來測時間的!這個 interceptor 可以幫助 programmer 測量每一次 request 執行所花費的時間,而且很有趣的是,如果我們將這個 interceptor 放在 stack 的最上層,我們測量的時間就是整個 request 執行的時間;如果我們將 timer interceptor 放在 stack 的最下層,那我們量測的時間就會是 Action 執行的時間!

所以我們可以藉由調整這個 interceptor 的位置來測量 request 的時間或是 Action 執行時間。

這個 interceptor 提供一個很簡單的 logging 機制,此 interceptor 會在 preprocessing 與 postprocessing 進行 logging 的動作。這樣可以很容易的 debug,因為我可以追蹤使用者的使用紀錄,進而找出問題的所在。

2. Data Transfer Interceptors

資料轉換是一個從 client 到 server 端很重要的角色,因為使用者填寫完畫面上的表單後,接著就會透過 interceptors 將資料 mapping 到 server 端的 java 物件。也就是將 request parameter 中的資料移動到 Action 之中。

這是一個很重要的 interceptor,這個 interceptor 會主要將 request parameters 轉換到 ValueStack 中相對映的 properties。這個 interceptor 也會與 OGNL 一起合作將 parameter 作轉換,並且可以幫助我們將 parameter 的值轉換到 ModelDriven 的 Action 中。不過,param interceptor 並不會知道資料確實是要存放在哪,他只會將 parameter 的值轉換到第一個相符的 property 中。這點一開始會令人感到很詭異,不過因為現在還不會對 ValueStack 作很深入的探討,所以你就先記著這個特性,之後我們在深入的討論 ValueStack。

這個 interceptor 跟上面的 param interceptor 功能很像,都是將 parameter 中的值轉換到 ValueStack 中,不過不一樣的是,這個是將 configuration file 中設定的 parameter 轉換到 ValueStack 中:

<action name="myAction" class="silver8250.my.MyAction">
<param name="firstName">Silver</param>
<param name="lastName">Su</param>
</action>
從上面的 configuration 中可以得知,我們可以在 configuration 中先設定預設 parameter 的值,static-param interceptor 會幫我們將這些設定在 configuration 中的 parameter 轉換到 Action 中。有一點值得注意的是,因為 static-param interceptor 在 stack 中是排在 param interceptor 之前,那就代表著如果使用者的表單中也有 firstName 與 lastName 的話,當表單送交後,就會覆蓋原先在 configuration 中設定的值。這樣可以讓使用者沒有填的欄位具有 default value。

這個 interceptor 也是相當重要的一個,因為他提供了 Dependency Injection(DI) 的功能。DI 簡單來說就是我們不要直接的去要求某些物件,反而是讓 framework 自己給我某些物件!如果你有熟悉 Spring framework 的話,DI 對你來說應該就是家常便飯了!

如果你有寫過 Struts 1.x,我想以下的例子你就可以更深刻的體會了!在 Struts 1.x 中,execute() method 都會與 HttpServletRequest 等物件有相依性,這樣就造成了測試上的不方便,因為我們要將這些 Container 物件傳遞給 execute() method,我們才能進行測試的工作!也就是我們要使用 Mock Object 來測試,這樣讓測試的工作不單純。不過請你回想一下在 Strut2 中我們的 Action execute() method 是很 pure 的,我們無須讓呼叫者傳遞任何的 parameters 給我們!但是你應該會跟我一樣很 confuse!我們要怎要拿到 HttpServletRequest 物件呢?別急!接下來就是 Servlet-config interceptor 的能力要發揮了~

Servelt-config interceptor 提供了以下的 interfaces 讓 programmer 可以 implement:

  1. ServletContextAware - 設定 ServletContext 物件給 Action
  2. ServletRequestAware - 設定 HttpServletRequest 物件給 Action
  3. ServletResponseAware - 設定 HttpServletResponse 物件給 Action
  4. ParameterAware - 設定 request parameter 的 Map 物件給 Action
  5. RequestAware - 設定 request 的 Map 物件給 Action
  6. SessionAware - 設定 session 的 Map 物件給 Action
  7. ApplicationAware - 設定 application 的 Map 物件給 Action
  8. PrincipalAware - 設定 Principal 物件給 Action,用於 security

在上面所列出的 8 項中,最常用的應該就是前 7 個,而且這每一個 interface 都只有一個 setter method(如:SessionAware 的 setSession(Map<String, Object> session) method)。第 1~3 的 interfaces 屬於將原始的 JSP 物件設定給 Action,而 4~7 是 Struts2 framework 將各個在 JSP 的隱含物件(Implicit Object)提供 Map 方式存取!

只要我們的 Action implement SessionAware 並且實做出 setSession() method,我們就可以在 Action 中存取 Session Map 物件,如下:




public MyAction implements SessionAware
{
private Map<String, Object> session;
public setSession(Map<String, Object> session)
{
this.session = session;
}
public String execute()
{
//對 session 物件進行存取!
}
}

上面的程式中我們先宣告一個 session 的 Map 物件,這是為了方便我們在 execute() method 中存取 session,所以我們透過 setSession() method 將 framework 給我們的 session Map 物件設定到我們的 session 變數。其他的 interface 用法也如同 SessionAware 的使用方式。

這個 interceptor 幫我們將 client 端 form 的 multipart request 轉換成 Action 中的物件,不過這轉換的機制跟 param interceptor 有點不一樣,因為在 defaultStack 中,Fileupload interceptor 是被安排在 param interceptor 之前,所以,Fileupload 是將 parameter 的值轉換成像是一般的 parameter 一樣(有興趣的你可以自行研究一下 API 的內容~),然後在交給 param interceptor 去處理,而這樣的處理就變得更單純了!

3. Workflow Interceptors

Workflow 類型的 interceptors 提供了許多在執行 Action 之前的最後作業,所以 workflow 類型的 interceptors 通常是在 stack 中的最後面幾個,因為當所有的 data 都完成轉換後,workflow 就會開始進行一連串的流程作業。

而這裡所謂的 workflow 是指:一系列的流程,從 interceptor 開始到 Action 執行,產生 Result 最後再回到 interceptor 的過程。而 workflow interceptors 扮演著檢驗流程中處理的狀態,如果流程中有任何狀態是有問題的,就會進行非正常的流程處理。

  • Workflow interceptor(default stack)
    別懷疑,workflow 確實是一個 interceptor!這個 interceptor 會負責 data validation,並且會將驗證錯誤的資訊傳送到 client 端的頁面。這裡所謂的 data validation 就是我們之前在深入實做 Action 中提到的 basic validation,也就是我們在 Action 中撰寫的 validate() method。下面的程式碼就是簡化過得 workflow interceptor:(雖然簡化,不過功能很齊全!)


public String intercept(ActionInvocation invocation) throw Exception
{
Action action = invocation.getAction();
if (action instanceof Validateable)
{
Validateable validateable = (Validateable) action;
validateable.validate();
}
if (action instanceof ValidationAware)
{
ValidationAware validationAwareAction =
(ValidationAware) action;
if (validationAwareAction.hasErrors())
{
return Action.INPUT;
}
}
return invocation.invoke();
}

如果我們的 Action 是 extends ActionSupport 的話,ActionSupport 就會幫我們 implement Validateable 與 ValidationAware interfaces。第一個 if statement 用來判斷此 Action 是否為一個 Validateable 物件,並且執行 validate() method。第二個 if statement 則是在判斷是否有 Error message 被儲存在 ValidationAware 中。

另一個有趣的功能就是 workflow 可以透過 param interceptor 被調整,怎麼說呢!在 struts-default.xml 中我們可以看到 workflow interceptor 已經被調整成如下:


<interceptor-ref name="workflow">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>

上面的設定中我們告知 workflow interceptor:當執行 validate 時,請忽略 input, back, cancel, browse 為 Action 進入點的 methods。關於目前我們只有使用最簡單的 Action,也就是單一的 execute() method 為進入點的 Action。所以目前你可能會沒有感覺,但是我想表達的是關於 workflow interceptor 是可以被這樣用來調整成適合我們的!以下就列出一些 workflow 准許我們調整的 parameters。

參數 預設值 說明
alwaysInvokeValidate true / false true

設定是否總是呼叫 validate() method

inputResultName String Action.INPUT 設定如果驗證失敗則要產生的頁面
excludeMethods String N/A 設定不執行驗證的 Action 進入點 method

上面的 workflow interceptor 已經提供了驗證的功能了!那 Validation interceptor 要用來做什麼呢?你應該會很困惑~雖然 workflow interceptor 已經做了最基本的驗證功能,不過對於一個大型的 web application 來說,要將類似的驗證邏輯都重複寫在不同的 Action validate() method 中其實很不方便!所以 Validation interceptor 就是用來解決這樣的問題,在 Struts2 中提供了另一種 Validation framework,讓 programmer 可以將驗證邏輯撰寫一次,然後藉由設定檔在不同的 Action 進行相同的驗證!這個機制的好處對於大型的 web application 來說是比較好得!畢竟複製一堆相同程式碼在不同的地方,對於日後的維護是相當累的!

我們在這裡並不會詳談關於 Validation framework 的部份,不過可以簡單的介紹一下。當 programmer 將各個 Action 的驗證規則定義好後,validation interceptor 就會根據設定檔進行驗證的工作,當有發現驗證失敗的地方,會將錯誤訊息儲存在 ValidationAware 中,所以在 workflow interceptor 中還是需要將這些錯誤訊息顯示在 client 端頁面上!不過我們還是可以撰寫 validation() method,這個 method 的內容還是會被執行的!

另外,在 interceptor stack 中,validation interceptor 一定要在 workflow 之前,否則驗證有錯誤的訊息就不會被顯示了!

Prepare interceptor 提供了一種方式讓我們可以介入整個 request 流程,也就是如果我們的 Action implements Preparable interface 就可以擁有這樣的好處(P.S. ActionSupport 沒有 implement Preparable),並且實做 prepare() method 就可以!

當 Prepare interceptor 執行時會先檢查該 Action 是否為一個 Preparable,如果是,就會呼叫 prepare() method。所以我們可以在 Action 被執行之前(更嚴格的說是在 Prepare interceptor 之後的動作執行前),將前置動作寫在 prepare() method 中。

Prepare interceptor 也提供一個 parameter 可以調校:

alwaysInvokePrepare - 是否總是呼叫 prepare() method,預設是 true。

這個 interceptor 對我們來說並不陌生,很直觀的,他就是用來處理 ModelDriven 的 Action。ModelDriven interceptor 執行時,會先判斷此 Action 是否有 implements ModelDriven interface,接著就會將 getModel() method 中的 model 儲存到 ValueStack 中。

需要注意的是,ModelDriven interceptor 一定要安排在 static-parameter 與 parameter interceptor 之前,否則將不會發揮功能!

4. Miscellaneous Interceptors

這類的 interceptors 是屬於一些輔助型的,而且這些 interceptors 對整個 stack 的其他 interceptors 進行點綴。

這個 interceptor 很重要,因為他會負責處理整個 request 流程中的 exception。在 defaultStack 中 exception interceptor 是第一個被呼叫的 interceptor,並且會擷取使用者定義的 exception,將訊息顯示在使用者定義的 exception page。也因為最上層的 interceptor 所以他會擷取所有的 exception,若是放在最下層,則只會擷取到 Action 所產生的 exception。

<global-results>
<result name="error">/WEB-INF/pages/error.jsp</result>
</global-results>
<global-exception-mappings>
<exception-mapping exception="java.lang.Exception" result="error" />
</global-exception-mappings>

上面的設定是屬於 configuration file中,global-results tag 表示當某個 Action 所回傳的控制字串不再 Action 的定義內,framework 會去搜尋 global-results。也就是說 global-results 是在這個 configuration file 中可以共用的!另外,global-exception-mapping tag 就是定義 exception interceptor 要幫我們處理哪些 exception,並且要將錯誤的訊息顯示在哪個頁面!

這個 interceptor 是可以讓我們將 ModelDriven 所使用的 model object 儲存在 session 等等的 scope,不過目前我不會詳細的介紹!

在這裡我們介紹了很多有用的 interceptors,有興趣的你可以各自深入的瞭解,在這裡我只能很簡略的討論各個 interceptors 的功能而已!

2009年3月15日 星期日

[Struts2] 深入探討 Interceptor

先前我們已經簡單的介紹了 interceptor 的功能以及整個 interceptor stack 與 Action 呼叫之間的順序!現在我們要將我們的焦點往 interceptor 拉近一些,也就是我們要看看每一個 interceptor 內部的實做面,不過在這裡我們並不會開發我們自己的 interceptor!

Introduce Interceptor 中我們有提到 ActionInvocation 物件,這個物件扮演著整個 interceptor stack 最重要的核心角色!接下來我將會分成兩個 section 來探討我們的主題:1) ActionInvocation guy 與 2) Interceptor 的流程。

ActionInvocation guy

我們之前說過,ActionInvocation 是整個 interceptor stack 中最重要的核心角色,不過當我們瞭解 ActionInvocation 物件時,並不代表我們就會瞭解整個 Struts2 framework 對於處理 request 的流程!當我們在開發 interceptor 時,我們並不會直接的與 ActionInvocation 物件互動,而是間接的與 ActionInvocation 有關係!

ActionInvocation 是一個包裝了所有與 Action 執行有關的 detail information 的物件,當我們的 framework 接收到一個使用者的 request 時,Struts2 framework 會根據使用者呼叫的 URL 來 mapping 所對應的 Action,並且會將此 Action 的相關資訊加入到 ActionInvocation 物件中,接著 Struts2 framework 就會搜尋所有 configuration file 中的所有 interceptors 並且加到 ActionInvocation 中,幫助 ActionInvocation 走訪所有的 interceptors。所以,ActionInvocation 會紀錄整個 request processing 的重要資訊,並且當整個 Struts2 framework 在運作時,ActionInvocation 物件會決定哪一個 component 將會是下一個被呼叫,以及該 component 被呼叫時要給予哪些 data 等等的工作!

Interceptor 的流程

介紹完整個 interceptor stack 中的核心角色後,我們接著就要來看看單一 interceptor 內部執行的流程!當系統中的 ActionInvocation 物件被 setup 完成後,接著就會依照 configuration file 中設定的 interceptor 順序來執行所有的 interceptors。然而在 ActionInvocation 中最重要的 operator 就是 invoke() method,這個 method 會在 ActionInvocation 被 Struts2 framework 設定完成後第一個呼叫的 method,因為 invoke() method 中會具備有執行 interceptor stack 中所有 interceptors 的功能。所以當 invoke() method 第一次被呼叫時,ActionInvocation 就會執行 stack 中第一個 interceptor。值得注意的是,invoke() method 第一的執行並不匯總是執行第一個 interceptor,執行哪一個 interceptor 與傳送哪些資料最終還是由 ActionInvocation 決定,當我們的 interceptor 要被執行時,ActionInvocation 就會呼叫 interceptor 中的 intercept() method

當每一次 invoke() method 被呼叫時,ActionInvocation 會決定接下來是哪一個 interceptor 要被執行,然後呼叫該 interceptor 的 intercept() method。這樣的流程會被執行道沒有下一個 interceptor 要被執行時,ActionInvocation 就會改呼叫 Action 的 execute() method 來執行真正的 business logic,再來 ActionInvocation 就會依照相反地順序呼叫剛剛的 interceptors!

不知道你是否有看出來?這樣的 interceptor stack 的呼叫是一種 recursive 的方是在執行。當前一個 interceptor 呼叫 invoke() method 後,就會執行當前 interceptor 的 preprocessing 程式,接著就要再呼叫 invoke() method,讓 ActionInvocation 執行下一個 interceptor 的 preprocessing,當所有 interceptors 的 proprocessing 完成後才是 Action 的執行,最後在反向的執行每個 interceptor 的 postprocessing。由圖一我們可以看出一個 interceptor 的執行週期。

圖一 Interceptor 的執行週期

讓我們整理一下單一的 interceptor 在撰寫程式時的結構:

  1. 執行 Preprocessing,在這個階段我們可以準備一些在 Action 執行前的作業,我們可以 filter data, alert message 或是處理重要的 data。
  2. 將控制權轉交給下一個 interceptor,而最終是交給 Action 的 execute() method,所以我們呼叫 ActionInvocation.invoke()。在呼叫後,invoke() method 會回傳一個 control string,這個 control string 會由下一個 interceptor 傳遞過來,如果整個執行流程中沒有任何的失誤,control string 將會是我們之前在 Action 的 execute() method 所回傳的那個 string。
  3. 執行 Postprocessing,當我們接收到 control string 後,我們就可以開始執行 postprocessing 的動作,在這裡可以做一些資料的後置處理如:logging 等。不過必須注意的是:當我們獲得 control string 後,代表著回傳給使用者的頁面已經被確定了!所以我們更改 control string 的內容並不會造成頁面的不同!也就是說,當我們需要對 control string 做改變時,我們必須在 preprocessing 階段就動手,這樣才會算數!

在這裡我們比較深入的討論到 interceptor 內部的動作,接下來我們才會著手進行我們自己的 interceptor。

[Struts2] Introduce Interceptor

之前我們在 How Struts2 works 中有稍微的談到 Interceptor。在 Struts2 中 Interceptor 算是與 Struts1 不同處的最大突破!因為 Interceptor 幫助我們可以很容易的 reuse components 的功能。而且 Interceptor 在 Struts2 framework 中扮演著很重要的關鍵角色,因為 Interceptor 的導入,讓 Struts2 framework 更達到 Separation of concerns。(也就是專門的 component 就做自己專門的工作,無須與其他 components 有太過複雜的互動等)

Separation of concerns 讓系統可以更清楚的劃分各個區塊的工作,在 Struts2 framework 中主要 focus 的 concern 就是 MVC,所以 Separation of concerns 讓 Struts2 的 MVC 架構更乾淨明瞭!而且從架構的觀點來說,interceptor 讓我們可以更專注的在開發我們的 web application,讓一些每個部份都需要用到的功能(例如:Logging 等)放在 interceptor 中給大家共用,而我們的 business logic 並不會受到任何的影響。換句話說,當我們在開發我們的 Actions 時,我們不用自己在 Action 中加入 Logging 的程式碼,透過 interceptor 幫我們做 logging 的工作,並且所有的 Actions 都可以享有這樣的功能!這種就是 cross-cutting concern

How Struts2 works 中我們有提到:Interceptor stack 是在整個流程中第一個被執行,也是最後一個被執行的!這就表示說 interceptor stack 中蒐集了很多的 interceptors,而這些 interceptors 都可以在執行我們的 Action 之前做一些 preprocessing 與 postprocessing 的工作!一個最直觀的 preprocessing 工作就是 data transfer,還記得使用者在 JSP 頁面中可以輸入資料,而這些資料最後都會被 mapping 到 Action 的 properties,這樣的工作就是交由 param interceptor 幫我們完成的!而這工作是必須在我們的 Action 執行前就要先被系統完成的前置工作,因為我們的 Action 在執行 execute() method 時,通常都需要使用者輸入的資料。而 postprocessing 就以 Logging 為例,我們可能希望系統執行過哪些 Actions 的紀錄要做 Log 以方便日後追查用等等,所以就需要在 Action 執行完之後進行 Logging 的作業。

圖一 ActionInvocation

圖一中表示,當 Struts2 framework 呼叫使用者所 request 的 Action 前,會先呼叫該 Action 所屬的 configuration file(.xml) 中 interceptor stack,這裡會定義一些再呼叫 Action 之前的動作,這些動作與 Action 是沒有直接關聯,但是卻是讓撰寫 Action 的 programmer 可以不用煩惱 detail 的動作。從 interceptor stack 為 stack 就知道,這樣的呼叫過程式 Layered process 呼叫方式,而 stack 的精神就是 FILO(First-In-Last-Out),所以在 interceptor stack 中的第一個 interceptor 會被第一個呼叫,也會是最後一個呼叫。如圖一中的第一個 interceptor 為 exception interceptor,在 Action 執行會是第一個被執行的 interceptor,當 Action 執行完後,Struts2 安排好顯示給使用者的Result 畫面,接著才會在回到 interceptor stack 中依照剛剛相反地順序執行 interceptors,所以這時候 exception interceptor 就會變成整個流程中最後一個被執行的 interceptor。如果我們的 interceptor 在 stack 中是最後一個的話,我們不必直接的呼叫 Action 的 execute() method,因為 Struts2 framework 會產生一個 ActionInvocation object 物件負責協調各個 interceptors 之間以及與 Action 之間的互動,甚至我們無須知道我們的 interceptor 是處在 stack 中的哪一個位置以及我們前後的 interceptor 又是哪些,我們一樣可以很簡單的去完成 interceptor 需要執行的功能!

這種 Layered 結構讓我們的 web application 保持簡單乾淨,甚至提昇了 interceptor 的 readabililty, testing 與 flexibility 特性。

對於 readability 來說,因為每一個 interceptor 可以是獨立的,也可以與其他 interceptors 合作,但是對於我們的程式來說並不用考慮這樣的問題,我們只需要專心的撰寫我們 interceptor 所要提供的功能就好,剩下的是就交給 Struts2 framework 來幫我們完成!這樣就可以增加整個 interceptor 程式碼的可讀性,而且與 framework 之間的 coupling 降低,而 cohesion 提昇。

同樣的,testing 也會變得相對的容易的多,因為我們不必知道太多其他 detail 而可以很容易的去測試我們的程式碼。

最後就是 flexibility,就如同之前所說的,因為我們的 interceptor 不必知道其他 interceptors 之間的資訊,但是彼此間又可以合作,所以我們就可以讓我們的 interceptor 既可以獨立運作又可以互相合作,並且我們的 interceptor 可以根據 configuration file 的設定來改變 interceptor 之間的位置來達到不同的效果,這樣就是增加了 flexibility。

對於初次接觸 Struts2 framework 的使用者來說,對於 interceptor 的設定並不是哪麼的容易,因為有些 interceptors 是 Struts2 原先就提供給 programmer 很方便的工具,如果我們忘記將這些 interceptors 加入 stack 中,那我們的 web application 就會跑不起來。雖然我們之前有說過我們可已將 interceptors 之間的位製作調整,但是有些 interceptors 要先被放置在 stack 的上面(第一次先被執行),之後的 interceptors 才能夠發揮其作用,所以彼此之間是有那麼一點關聯性存在。幸好 Struts2 framework 提供了 defaultStack 讓初學的人對於設定 interceptor stack 不用在煩心,而且對於初學者來說,defaultStack 中所安排的 interceptors 幾乎都可以滿足需求!那你可能會問,我們在初啼試聲 HelloStruts2 中好像沒有設定我們的 defaultStack 耶!的確,我們並沒有很明確的設定我們的 defaultStack,不過當我們對 <package> tag 設定 extends attribute 時就已經間接的設定我們的 defaultStack!因為 defaultStack 是被設定在 struts-default 的 package 中,當我們 package extends struts-default 就繼承了 struts-default 中的所有設定!所以對於初學的 programmer 來說,我們就使用 defaultStack 作為我們的 interceptor stack!

2009年3月7日 星期六

[Agile Method] Pair-Programming 與成本

那天去關貿網路面試時,技術長有問道:企業老闆怎麼可能採用 pair-programming,怎麼可能讓兩個同時只做一件事情?有沒有數據研究可以證明兩個人工作至少等於兩個人甚至大於兩個人的成效?

這個問題其實見怪不怪,當我還是個準碩士時,我的指導教授開了一場敏捷方法研討會,內容就是提倡敏捷方法!在會議後半段的提問時間,許多企業也對都於 pair-programming 實行上產生質疑!沒錯~依照台灣對於成本 cost down 的觀念來說,要執行這種不符合成本的作法實在不合邏輯~雖然我在實驗室有採用過這種方法來開發軟體,不過因為我們的軟體規模實在是很小,不能說是一種 product 只是計畫中的一個 project 而已,所以實際的成效很難信服,但是兩個人的討論確實可以減輕很多壓力!

後來我就找我的老師討論,老師應該也已經想過這問題如何在台灣實行的問題,所以他就給我這樣的回答:做軟體的品質一定要第一,成本才是其次;沒有品質的軟體賣不出去,那成本壓的在低也是不符合成本!但是如果老闆真的有成本上的壓力,我們可以部份採用 pair-programming,其實 Agile method 只是一套方法,而方法的內容可以根據實際上得情形加以調整,以便因應不同環境!所以我們可以在軟體的核心元件採用 pair-programming 而其餘的部份像是 UI 就採用傳統的 single role 方式來開發,畢竟軟體的核心元件是整個系統中最重要的,重要的部份就必須要提高其品質!

聽完老師這樣的回答後,對於 Agile method 的可行性又更往前邁了一步!而且我也很贊同老師所說得:品質第一!

[Interview] 關貿網路的兩次面試

其實已經事隔幾天了,現在才分享一下!話說,那天是 2/20,是我第一次到關貿網路面試,因為之前已經面試過英丰寶了,所以面試時已經不會太緊張!第一次面試是跟課長談,不過一開始 HR 就先進會議室跟我聊聊,拿了一張公司的簡介單給我,並且很仔細的跟我介紹公司的狀況、對於研發替代役的資訊、過去國防役學長的經驗分享以及公司的福利等等。接下來就是課長近來跟我面試了!正式的面試就開始~

一開始因為課長才剛拿到我的履歷,所以就先簡短的自我簡介一下~接下來就聊聊關於學校的學習狀況、論文的內容啦~其餘的時間都是 focus 在我的實務經驗,因為我自己為了家計必須要打工賺錢,所以上了碩士後就是以技術為導向的打工經驗,接一些學校的 cases 等,這樣就聊了快一個小時,其實過程很愉快~課長不會給你很大的壓力~也希望大家像是朋友一樣的聊天模式,所以過程很輕鬆~後來我就開始提問,課長也很仔細的分析給我聽,所以面試的後半段就幾乎是課長在講給我聽~因為自己還沒有出社會,對於軟體業並不是很瞭解,只是因為自己喜歡寫程事兒想進入這行業罷了!課長就先分析了軟體業在市場上的各種領域等。最後課長從手中的資料中發現了一張考卷!接著他就說:時間不多了!我們就完成一下例行公事好了!接著就要我寫考卷中最後一個題目,題目很簡單,就是寫個 class 描述一下某個 entity 就這樣!之後就是 HR 進來跟我談薪資的部份(因為是我問課長,他說他也不知道XD 所以就請 HR 跟我說明)。第一次的面試感覺很輕鬆愉悅,課長並不會刁難你,大家在聊天中度過了進兩個小時的面試~

關貿網路公司很大,因為第一次坐錯電梯,坐到只有到 E 棟 8 樓以上的那部XD,因為我是要到 6 樓,所以想說去 8 樓換電梯,門打開才發現,這裡也是關貿XD,他們好像是 6~8 樓都是的樣子XD

後來因為等待面試結果很痛苦,所以我就主動打電話給 HR,HR 也說正想要通知我,所以我就準備要去第二次面試了~

3/3 是第二次面試,對象是跟技術長以及一位也是很資深的技術部門長官,這次的面試過程只有短短的 30 分鐘!一開始也是自我簡介,接著兩位面試官就根據我的打工經驗裡問問關於遇到的困難,要怎樣解決等等的問題,問題也不會刁難,只是從中發現我的人格特質(跟去英丰寶的總經理面試一樣),因為我在實驗室學的是 Agile method,他們也有問一些問題:CMMI 如果要你寫很多文件,以你做 agile method 的人來看你有怎樣的看法?其實我的回答不是很八股,因為我不希望去捧長官,雖然說講話要甜一點,可是我就很真實的去回答我想要說得,不過還是態度有比較微婉~技術長其實也不會不高興,因為我們整個面試中也算是有說有笑,面試也不會有太大的壓力~就這樣,結束了短短 30 分鐘的面試過程~

現在的我又進入等面試結果的狀態~等待是很痛苦的~

2009年3月5日 星期四

[Agile Method] Communication: The key to Agile Method

在 Agile method 中有很多現有的方法被採用,像是 Test-Driven Development(TDD)、Pair programming 等,也有一些新的規範被納入,如:stand-up meeting、on-site customer 等!不過這些方法最重要的基礎就在於 Communication(溝通)!Agile method 中很講究溝通,就像 pair programming 就是最明顯也是強需要溝通的一項方法,溝通必須是基於自己肯分享肯討論的個性為主,不然 pair programming 也會流於形式!

Pair programming 在之前也有討論過,就是兩個人綿密的溝通,一個人的思考畢竟有限,想法也會有缺陷,所以透過 pair programming 的方式可以降低這樣的缺陷,不過這只有降低,並不能完全的消除,畢竟還是會有機率發生在兩個人都剛好沒有想到某個缺陷的存在!因為我在實行 pair programming 時真的是有發生這樣的情況~在 pair programming 中溝通就變得很重要,溝通是基於表達方式,如果表達方式不佳,溝通就容易造成失敗,因為對方聽不懂你在說什麼。

TDD 方法中也是有溝通,很難想像吧!當初我也是沒有想到,不過在某一堂課程中,我們老師點出了這個觀念我就懂了!TDD 就是先將我們的 test code 根據 scenario 完成,當我們完成這部份的功能時,我們就交給電腦去幫我們驗證邏輯,這種就是跟電腦的溝通,因為當 scenario 很豐富時,我們可能要測試的 test case 就會很多,這當然不能由人自己慢慢的測試,我們只能測其中幾個比較重要的,剩下的完整測試就交由電腦幫我們去跑,所以這也是一種溝通,只是不是面對人,是面對機器!

Stand-up meeting 就像是現在有很多公司會每週開會一次,不過在 Agile method 中是希望可以每天早上開會,開會中決定今天的工作,開會中討論昨天可能碰到的問題或是開發中可能遇到的狀況等等,也因為一個開發團隊的人員很少(6~10人),所以 stand-up meeting 是相對的容易,當團隊擴大時,溝通也會是一個很大的問題!目前有些國外的 conference 也在徵求類似的經驗分享 papers,希望業界中有人可以分享他是如何解決團隊擴大時溝通的問題!

On-site customer 就是希望我們的客戶能夠跟著開發團隊一起工作,因為只有 on-site customer 可以真正的瞭解 customers 的需求,如果開發團隊在開發中遇到問題,就可以馬上請教 on-site customer 回答問題~這也是一種溝通,不過這種溝通是希望可以讓 customer 提早進入軟體開發的生命週期,以往的開發方法中,customer 只會參與到整個生命週期的最早與最晚,也就是 requirement 與驗收,若開發團隊無法與 customer 時時的溝通,產品往往在完成後變成一堆價值很高的垃圾,畢竟 customer 無法在一次會議或一段時間中想到所有想要的 requirement,所以如何讓需求變成 live requirements 是現行開發方法中最重要的 issue。

從這些 Agile method 中很重要的實行方法中,我們可以發現溝通是最重要的元素,不過也會有很多人誤會 Agile method 就是靠溝通而不需要撰寫文件,溝通固然是必須的,但是沒有文件紀錄溝通中重要的資訊,那溝通也就枉然了~人類的腦袋無法記得所有溝通的內容,所以還是需要文件的輔助,不過 Agile method 中又希望文件是有必要才產生,因為當文件的數量一龐大,系統有修正時,修改文件就會變成很粗重的工作,若文件不跟著同步,那文件也是流於形式而成為昂貴的垃圾了~所以如何在文件的數量上取捨,其實也是一種學問跟經驗。

以上是目前當了自己老師的 Agile method 課程助教時有感而發~與大家分享!