Action的單元測試
理解了ServletDispatcher,我們就明白了整個框架調用執行的順序。Action雖然是與Web無關,可是它的創建、參數設置、執行與我們的WebWork、XWork緊密關聯在一起,有我們的控制器ServletDispatcher去統一調度,那我們如何去對Action進行獨立的單元測試呢?
請看下面的例子:使用單元測試框架JUnit對register.User. RegisterAction做單元測試
見example.register. RegisterActionTest類testExecuteWithProxyFactory()方法:
代碼 |
public void testExecuteWithProxyFactory() throws Exception{ Map params = new HashMap(); params.put("user.username","Moxie"); params.put("user.password","mypassword"); params.put("user.email","[email protected]"); params.put("user.age",new Integer(23)); Map extraContext = new HashMap(); extraContext.put(ActionContext.PARAMETERS,params); ActionProxy proxy = ActionProxyFactory.getFactory().createActionProxy("example", "register", extraContext); proxy.setExecuteResult(false); assertEquals(proxy.execute(),"success"); RegisterAction action = (RegisterAction) proxy.getAction(); assertEquals(action.getUser().getUsername(),"Moxie"); assertEquals(action.getUser().getAge(),23); } |
下面解說這個方法:
1、 對象params表示請求參數的Map,在它裏面設置了註冊用戶的信息。extraContext當然就是我們ActionContext上下文的容器,它裏面保存了放置請求參數的對象params
2、 創建我們的ActionProxy,它傳入的參數有:“example”-這個Action的命名空間,“register”-Action對應的名字,extraContext-存放Actin上下文裏的對象,,執行並將它返回的值與“success”比較,測試Action是否能正確執行完成。注意:proxy.setExecuteResult(false);,因爲我們是單元測試,所以Action執行完成就可以了,不用再去調用結果響應的操作,故將是否執行結果設置爲“false”。
3、 Action正確執行完成之後,我們也可以測試現在Action的字段裏的數據是否按照我們預期的要求正確設置。從ActionProxy對象裏取得執行的Action,即RegisterAction對象,再取得它的User模型,將其數據與前面設置參數的數據進行比較,判斷它是否等於我們預期設置的數值。
Result Type
前面我們學習了ServletDispatcher,它是WebWork框架機制的核心。它和Action在我們MVC模式中,扮演着控制器的角色,MVC模式通過控制器實現了我們模型和視圖的分離。WebWork提供了多種活靈活視圖展現方式。
我們先看看前面用戶註冊例子的展現方式:我們使用的是Jsp和WebWork自帶的標籤庫,Action對應的視圖當然是在xwork.xml配置文件裏設置:
代碼 |
<action name="register" class="example.register.RegisterAction" > <result name="success" type="dispatcher"> <param name="location">register-result.jsp</param> </result> <interceptor-ref name="params"/> </action> |
Result是Action執行完返回的一個字符串常量,它表示Action執行完成的狀態,比如:執行成功、執行失敗等。在我們前面Action的介紹中,詳細介紹了它默認的標準Result,當然Result我們也可以自己定義,只要是一個字符串常量就可以了。
Result的值在xwork.xml配置文件裏就是result標籤裏“name”的值,name="success"表示Action執行成功,返回“success”就對應此標籤的配置,進行視圖輸出。
“type”就是我們的Result Type,Result Type是一個類,它在Action執行完成並返回Result之後,決定採用哪一種視圖技術,將執行結果展現給用戶。我們輸出的類型是:type="dispatcher",它對應com.opensymphony.webwork.dispatcher.ServletDispatcherResult這個類,它將執行結果通過javax.servlet.RequestDispatcher的forward()或include()方法調度到Jsp頁面展現。
我們可以自己開發Result Type,實現我們需要的視圖展現方式。Result Type必需要實現com.opensymphony.xwork..Result接口。在WebWork中,它已經爲我們提供了很多Result Type,實現了視圖部分對JSP, Velocity, FreeMarker, JasperReports,XML等的支持,具體如下表格:
Result Type
Nname
Class
Dispatcher
dispatcher
com.opensymphony.webwork.dispatcher.ServletDispatcherResult
Redirect
redirect
com.opensymphony.webwork.dispatcher.ServletRedirectResult
Action Chaining
chain
com.opensymphony.xwork.ActionChainResult
Velocity
velocity
com.opensymphony.webwork.dispatcher.VelocityResult
FreeMarker
freemarker
com.opensymphony.webwork.views.freemarker.FreemarkerResult
JasperReports
jasper
com.opensymphony.webwork.views.jasperreports.JasperReportsResult
XML/XSL
xslt
com.opensymphony.webwork.views.xslt.XSLTResult
HttpHeader
com.opensymphony.webwork.dispatcher.HttpHeaderResult
Dispatcher:通過javax.servlet.RequestDispatcher的forward()或include()方法調度到頁面展現,這樣的頁面一般是Jsp頁面。
參數(Parameters)
是否必需
描 述
location
是
執行完成之後轉向的位置
parse
否
默認的是“true”,如果設置爲“false”,location參數將不會被OGNL表達式語言解析
例子:
代碼 |
<result name="success" type="dispatcher"> <param name="location">register-result.jsp</param> </result> |
也可以簡單寫成這樣:
代碼 |
<result name="success" type="dispatcher">register-result.jsp</result> |
Redirect:將響應重定向到瀏覽器指定的位置,它將會導致Action執行完成的數據丟失或不再可用。它在程序裏是通過調用javax.servlet.http.HttpServletResponse.sendRedirect(String location)方法,將響應定向到參數location指定的、新的url中。
參數(Parameters)
是否必需
描 述
location
是
執行完成之後轉向的位置
parse
否
默認的是“true”,如果設置爲“false”,location參數將不會被OGNL表達式語言解析
例子
代碼 |
<result name="success" type="redirect"> <param name="location">foo.jsp</param> <param name="parse">false</param> </result> |
Action Chaining:一種特殊的視圖結果,將Action執行完之後鏈接到另一個Action中繼續執行。新的Action使用上一個Action的上下文(ActionContext)。
參數(Parameters)
是否必需
描 述
actionName
是
將要被鏈接的Action名字
namespace
否
被鏈接的Action的命名空間(namespace),如果不設置,默認的即是當前的命名空間
例子:
代碼 |
<result name="success" type="chain"> <param name="actionName">bar</param> <param name="namespace">/foo</param> </result> |
將要調用的Action如下:
代碼 |
<action name="bar" class="myPackage.barAction"> ... </action> |
Velocity:它類似Jsp的執行環境(使用JavaServlet容器),將Velocity模板轉化成數據流的形式,直接通過JavaServlet輸出。
參數(Parameters)
是否必需
描 述
location
是
執行完成之後轉向的位置(一般是.vm頁面)
parse
否
默認的是“true”,如果設置爲“false”,location參數將不會被OGNL表達式語言解析
例子:
代碼 |
<result name="success" type="velocity"> <param name="location">foo.vm</param> </result> |
FreeMarker:FreeMarker是一個純Java模板引擎;一個普通的基於模板生成文本的工具,它只能應用在Web應用環境中。
參數(Parameters)
是否必需
描 述
location
是
執行完成之後轉向的位置
parse
否
默認的是“true”,如果設置爲“false”,location參數將不會被OGNL表達式語言解析
contentType
否
如果不指定,默認的是"text/html"
例子:
代碼 |
<result name="success" type="freemarker">foo.ftl</result> |
JasperReports:將Action執行的結果通過JasperReports報表形式輸出,可以指定JasperReports支持的輸出格式(PDF、HTML、XLS、CSV、XML等),默認是通過PDF格式輸出。
參數(Parameters)
是否必需
描 述
location
是
執行完成之後轉向的位置
parse
否
默認的是“true”,如果設置爲“false”,location參數將不會被OGNL表達式語言解析
dataSource
是
它是Action的一個字段(通常是一個List),OGNL表達式被用來去value stack(OgnlValueStack)重新找回這個dataSource
format
否
報表生成的數據格式,默認的是pdf
例子:
代碼 |
<result name="success" type="jasper"> <param name="location">foo.jasper</param> <param name="dataSource">mySource</param> <param name="format">CSV</param> </result> |
或者默認的pdf格式
代碼 |
<result name="success" type="jasper"> <param name="location">foo.jasper</param> <param name="dataSource">mySource</param> </result> |
XML/XSL:將結果轉換爲xml輸出
參數(Parameters)
是否必需
描述
location
是
執行完成之後轉向的位置
parse
否
默認的是“true”,如果設置爲“false”,location參數將不會被OGNL表達式語言解析
例子:
代碼 |
<result name="success" type="xslt">foo.xslt</result>
|
表達式與言EL和OGNL
OGNL介紹
OGNL是Object-Graph Navigation Language的縮寫,它是一種功能強大的表達式語言(Expression Language,簡稱爲EL),通過它簡單一致的表達式語法,可以存取對象的任意屬性,調用對象的方法,遍歷整個對象的結構圖,實現字段類型轉化等功能。它使用相同的表達式去存取對象的屬性。
XWork遵循“不要重複地發明同一個輪子”的理論,它的表達式語言核心用的就是這個OGNL。我們先來看看一個簡單的例子:
還記得我們用戶註冊的那個例子嗎?我們輸入框的name用到的名字就是OGNL的表達式,比如:用戶名的輸入框:“<input type="text" name="user.username">”,在用戶註冊成功之後我們要顯示用戶註冊的信息,用了“<ww:property value="user.username"/>”。Input輸入框裏的“user.username”,它解析成Java語句爲:getUser().setUsername();,property標籤裏的“user.username”解析爲Java語句:getUser.getUsername();。
我們的兩個表達式都是相同的,但前一個保存對象屬性的值,後一個是取得對象屬性的值。表達式語言簡單、易懂卻又功能強大,關於OGNL更多的介紹可以去http://www.ognl.org,那裏有很詳細的文檔。
值堆棧-OgnlValueStack
OGNL在框架中的應用,最主要是支持我們的值堆棧(Value Stack)——OgnlValueStack,它主要的功能是通過表達式語言來存取對象的屬性。用戶界面輸入數據,它會根據保存表達式將數據依次保存到它堆棧的對象中,業務操作完成,結果數據會通過表達式被獲取、輸出。
還記得我們用戶註冊的例子嗎?下面我們用一段程序來演示它向OgnlValueStack中保存、取得數據的步驟:
代碼 |
// DemoRegisterValueStack package example.register; import com.opensymphony.xwork.util.OgnlValueStack; /** * @author moxie-qac * [email protected] * */ public class DemoRegisterValueStack { public void demo(){ RegisterAction action = new RegisterAction(); OgnlValueStack valueStack= new OgnlValueStack(); valueStack.push(action); valueStack.setValue("user.username","Moxie"); System.out.println("username = "+valueStack.findValue("user.username")); } public static void main(String[] args) { DemoRegisterValueStack demoValueStack = new DemoRegisterValueStack(); demoValueStack.demo(); } } |
我們來看一看它的demo()方法:
1、 創建我們的Action(RegisterAction)類的對象action,將action對象壓入堆棧valueStack中。在WebWrok中Action的創建、入棧是在DefaultActionInvocation構造函數中進行的,詳細介紹見:ServletDispatcher原理。
2、 通過表達式語言,調用堆棧對象的get()、set()方法,設置該對象的值。
public void setValue(String expr, Object value)
語句:valueStack.setValue("user.username","Moxie");
的作用等同於:action.getUser().setUsername("Moxie");
3、 通過表達式語言,去堆棧對象中查找我們前面保存的值,並在控制檯打印。valueStack.findValue("user.username")等同與語句:
action.getUser().getUsername()
最後控制檯打印的結果:
username = Moxie
CompoundRoot
在OgnlValueStack中,一個堆棧其實是一個List。查看OgnlValueStack你會發現,堆棧就是com.opensymphony.xwork.util.CompoundRoot類的對象:
代碼 |
public class CompoundRoot extends ArrayList { //~ Constructors ///////////////////////////////////// public CompoundRoot() { } public CompoundRoot(List list) { super(list); } //~ Methods //////////////////////////////////////////// public CompoundRoot cutStack(int index) { return new CompoundRoot(subList(index, size())); } public Object peek() { return get(0); } public Object pop() { return remove(0); } public void push(Object o) { add(0, o); } } |
我們通過表達式向堆棧對象操作時,我們並不知道堆棧中有哪些對象。OgnlValueStack會根據堆棧由上向下的順序(先入棧在下面,最後入棧在最上面)依次去查找與表達式匹配的對象方法,找到即進行相應的存取操作。假設後面對象也有相同的方法,將不會被調用。
下面我們看一個對OgnlValueStack操作的程序,它主要演示瞭如何對Map對象的存取和OgnlValueStack堆棧的原理:
代碼 |
/* * Created on 2004-6-15 * DemoGroupValueStack.java */ package example.register; import com.opensymphony.xwork.util.OgnlValueStack; /** * @author moxie-qac * [email protected] * */ public class DemoGroupValueStack { public void demoAction(){ DemoGroupAction action = new DemoGroupAction(); OgnlValueStack valueStack= new OgnlValueStack(); valueStack.push(action); User zhao = new User(); zhao.setUsername("zhao"); zhao.setEmail("[email protected]"); User qian = new User(); qian.setUsername("qian"); qian.setEmail("[email protected]"); valueStack.setValue("users['zhao']",zhao); valueStack.setValue("users['qian']",qian); System.out.println("users['zhao'] = "+valueStack.findValue("users['zhao']")); System.out.println("users['qian'] = "+valueStack.findValue("users['qian']")); System.out.println("users size = "+valueStack.findValue("users.size")); System.out.println("allUserName[0] = "+valueStack.findValue("allUserName[0]")); } public void demoModels(){ User model_a = new User(); model_a.setUsername("model_a"); User model_b = new User(); model_b.setUsername("model_b"); User model_c = new User(); model_c.setUsername("model_c"); OgnlValueStack valueStack= new OgnlValueStack(); valueStack.push(model_a); valueStack.push(model_b); valueStack.push(model_c); System.out.println("username = "+valueStack.findValue("username")); System.out.println("[1].username = "+valueStack.findValue("[1].username")); System.out.println("[0].toString = "+valueStack.findValue("[0]")); System.out.println("[1].toString = "+valueStack.findValue("[1]")); System.out.println("[2].toString = "+valueStack.findValue("[2]")); } public static void main(String[] args) { DemoGroupValueStack demoValueStack = new DemoGroupValueStack(); demoValueStack.demoAction(); demoValueStack.demoModels(); } } /* * Created on 2004-6-15 * DemoAction.java */ package example.register; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author moxie-qac * [email protected] * */ public class DemoGroupAction { private Map users = new HashMap(); public Map getUsers(){ return this.users; } public List getAllUserName(){ return new ArrayList(users.keySet()); } public String execute(){ //執行業務操作 return null; } public String toString(){ return users.toString(); } } |
注意:
1、Map屬性的存取,它的表達式語言如:users['zhao'],注意它用’’來引用HashMap的key字符串。
2、demoModels()方法演示了OgnlValueStack中堆棧的原理,請特別注意它的[0].toString、[1].toString、[2].toString,它們依次調用堆棧中對象的toString()方法,並逐一的減少堆棧最上面的對象。
控制檯輸出的結果如下:
users['zhao'] = username=zhao;password=null;[email protected];age=0
users['qian'] = username=qian;password=null;[email protected];age=0
users size = 2
allUserName[0] = qian
username = model_c
[1].username = model_b
[0].toString = [username=model_c;password=null;email=null;age=0, username=model_b;password=null;email=null;age=0, username=model_a;password=null;email=null;age=0]
[1].toString = [username=model_b;password=null;email=null;age=0, username=model_a;password=null;email=null;age=0]
[2].toString = [username=model_a;password=null;email=null;age=0]
Interceptor(攔截器)將Action共用的行爲獨立出來,在Action執行前後運行。這也就是我們所說的AOP(Aspect Oriented Programming,面向切面編程),它是分散關注的編程方法,它將通用需求功能從不相關類之中分離出來;同時,能夠使得很多類共享一個行爲,一旦行爲發生變化,不必修改很多類,只要修改這個行爲就可以。
Interceptor將很多功能從我們的Action中獨立出來,大量減少了我們Action的代碼,獨立出來的行爲具有很好的重用性。XWork、WebWork的許多功能都是有Interceptor實現,可以在配置文件中組裝Action用到的Interceptor,它會按照你指定的順序,在Action執行前後運行。Interceptor在框架中的應用如下圖所示:
當你提交對Aciton(默認是.action結尾的Url)的請求時,ServletDispatcher會根據你的請求,去調度並執行相應的Action。在Action執行之前,調用被 Interceptor截取,Interceptor在Action執行前後運行。
我們在用戶註冊的例子中就使用了取得Request請求參數的攔截器,配置文件中<interceptor-ref name="params"/>將攔截器params組裝到RegisterAction中。“params”在我們的webwork-default.xml配置文件中有定義,webwork-default.xml中攔截器的定義如下:
代碼 |
<interceptors> <interceptor name="timer" class="com.opensymphony.xwork.interceptor.TimerInterceptor"/> <interceptor name="logger" class="com.opensymphony.xwork.interceptor.LoggingInterceptor"/> <interceptor name="chain" class="com.opensymphony.xwork.interceptor.ChainingInterceptor"/> <interceptor name="static-params" class="com.opensymphony.xwork.interceptor.StaticParametersInterceptor"/> <interceptor name="params" class="com.opensymphony.xwork.interceptor.ParametersInterceptor"/> <interceptor name="model-driven" class="com.opensymphony.xwork.interceptor.ModelDrivenInterceptor"/> <interceptor name="component" class="com.opensymphony.xwork.interceptor.component.ComponentInterceptor"/> <interceptor name="token" class="com.opensymphony.webwork.interceptor.TokenInterceptor"/> <interceptor name="token-session" class="com.opensymphony.webwork.interceptor.TokenSessionStoreInterceptor"/> <interceptor name="validation" class="com.opensymphony.xwork.validator.ValidationInterceptor"/> <interceptor name="workflow" class="com.opensymphony.xwork.interceptor.DefaultWorkflowInterceptor"/> <interceptor name="servlet-config" class="com.opensymphony.webwork.interceptor.ServletConfigInterceptor"/> <interceptor name="prepare" class="com.opensymphony.xwork.interceptor.PrepareInterceptor"/> <interceptor name="conversionError" class="com.opensymphony.webwork.interceptor.WebWorkConversionErrorInterceptor"/> <interceptor-stack name="defaultStack"> <interceptor-ref name="static-params"/> <interceptor-ref name="params"/> <interceptor-ref name="conversionError"/> </interceptor-stack> <interceptor-stack name="validationWorkflowStack"> <interceptor-ref name="defaultStack"/> <interceptor-ref name="validation"/> <interceptor-ref name="workflow"/> </interceptor-stack> </interceptors> |
這些都時有框架提供的默認的Interceptor,下面我來看看Interceptor使用的步驟:
1、 創建一個自己需要的Interceptor類,它必需實現com.opensymphony.xwork.interceptor.Interceptor接口,具體的開發見下面的Interceptor的原理。
2、 在配置文件(xwork..xml)中申明這個Interceptor類,它放在標籤<interceptor />中,同是<interceptor />標籤嵌入在<interceptors />標籤內部。
3、 創建Interceptor棧,使用標籤:<interceptor-stack />,讓一組Interceptor可以按次序調用。(可選)
4、 指定Action所要用到的Interceptor(前面申明過的),可以用<interceptor-ref />或<default-interceptor-ref />標籤。前面的標籤指定某個Action所用到的Interceptor,如果Action沒有被用<interceptor-ref />指定Interceptor,它將使用<default-interceptor-ref />指定的Interceptor。
框架中給我們提供了很多實用的Interceptor,它的定義上面已經給出,它的具體功能如下:
* timer:記錄Action執行的時間,並做爲日誌信息輸出;
* logger:在日誌信息中輸出要執行的Action信息;
* chain:將前一個執行結束的Action屬性設置到當前的Action中。它被用在ResultType爲“chain”指定結果的Action中,該結果Action對象會從OgnlValueStack中獲得前一個Action對應的屬性,它實現Action鏈之間的數據傳遞;
* static-params:將xwork.xml配置文件裏定義的Action參數,設置到對應的Action中。Action參數使用<param />標籤,是<action />標籤的直接子元素。我們這裏定義的Action類必需實現com.opensymphony.xwork.config.entities. Parameterizable接口;
* params:將Request請求的參數設置到相應Action對象的屬性中,用戶註冊例子用到過這個攔截器;
* model-driven:如果Action實現ModelDriven接口,它將getModel()取得的模型對象存入OgnlValueStack中;
* component:激活組件功能支持,讓註冊過的組件在當前Action中可用,即爲Action提供IoC(依賴倒轉控制)框架的支持;
* token:覈對當前Action請求(request)的有效標識,防止重複提交Action請求(request)。
* token-session:功能同上,但是當提交無效的Action請求標識時,它會將請求數據保存到session中。
* validation:實現使用xml配置文件({Action}-validation.xml)對Action屬性值進行驗證,詳細請看後面介紹的驗證框架。
* workflow:調用Action類的驗證功能,假設Action使用ValidationAware實現驗證(ActionSupport提供此功能),如果驗證沒有通過,workflow會將請求返回到input視圖(Action的<result />中定義的)。
* servlet-config:提供Action直接對HttpServletRequest或HttpServletResponse等JavaServlet api的訪問,Action要實現相應的接口,例如:ServletRequestAware或ServletResponseAware等。如果必需要提供對JavaServlet api的訪問,我們建議使用ServletActionContext,在前面ActionContext章節中有介紹。
* prepare:在Action執行之前調用Action的prepare()方法,這個方法是用來準備Action執行之前要做的工作。它要求我們的Action必需實現com.opensymphony.xwork. Preparable接口
* conversionError:用來處理框架進行類型轉化(Type Conversion)時的出錯信息。它將存儲在ActionContext中的類型轉化(Type Conversion)錯誤信息轉化成相應的Action字段的錯誤信息,保存在堆棧中。根據需要,可以將這些錯誤信息在視圖中顯示出來。
Interceptor的原理
下面我們來看看Interceptor是如何實現在Action執行前後調用的:
Action和Interceptor在框架中的執行,是由ActionInvocation對象調用的。它是用方法:String invoke() throws Exception;來實現的,它首先會依次調用Action對應的Interceptor,執行完成所有的Interceptor之後,再去調用Action的方法,代碼如下:
代碼 |
if (interceptors.hasNext()) { Interceptor interceptor = (Interceptor) interceptors.next(); resultCode = interceptor.intercept(this); } else { if (proxy.getConfig().getMethodName() == null) { resultCode = getAction().execute(); } else { resultCode = invokeAction(getAction(), proxy.getConfig()); } } |
它會在攔截器棧中遍歷Interceptor,調用Interceptor的方法:
String intercept(ActionInvocation invocation) throws Exception;。
我們一直都提到,Interceptor是在Action前後執行,可是從上面的代碼我們看到的卻是執行完所有Interceptor的intercept()方法之後再去調用我們的Action。“在Action前後執行”是如何實現的呢?我們來看看抽象類AroundInterceptor的intercept()實現:
代碼 |
public String intercept(ActionInvocation invocation) throws Exception { String result = null; before(invocation); result = invocation.invoke(); after(invocation, result); return result; } |
原來在intercept()方法又對ActionInvocation的invoke()方法進行遞歸調用,ActionInvocation循環嵌套在intercept()中,一直到語句result = invocation.invoke();執行結束,即:Action執行完並返回結果result,這時Interceptor對象會按照剛開始執行的逆向順序依次執行結束。這樣before()方法將在Action執行前調用,after()方法在Action執行之後運行。