Struts2的結(jié)構(gòu)
1.為什么要使用框架?
(1)框架自動(dòng)完成了很多瑣屑的任務(wù)
對(duì)于Struts2來說,它幫助我們方便地完成了數(shù)據(jù)類型轉(zhuǎn)換、數(shù)據(jù)驗(yàn)證、國際化等等
Web開發(fā)中常見的任務(wù)。還有Spring中大量使用的Template模式,都是在讓我們的開發(fā)
過程更加自動(dòng)化、智能化。使用框架就是避免重新發(fā)明輪子,重新復(fù)制這些模板代碼。
框架讓我們將精力更多地放在更高級(jí)別的問題上,而不是常見工作流和基礎(chǔ)任務(wù)上。
(2)使用框架就是優(yōu)雅地繼承了框架背后的架構(gòu)
框架背后的架構(gòu)通常定義了一系列的工作流程,我們要做的就是將特定應(yīng)用的代碼
依附到這套流程上,這樣就可以享受到框架帶來的種種好處了。有些時(shí)候我們也可以
反抗框架的架構(gòu)規(guī)則,但框架通常以一種很難被拒絕的方式提供它的架構(gòu)。如此簡(jiǎn)單
就可以優(yōu)雅地繼承一個(gè)優(yōu)秀的架構(gòu),而且是免費(fèi)的,何樂而不為呢?
(3)使用框架更容易找到訓(xùn)練有素的人
我之前所在公司整個(gè)項(xiàng)目幾乎都沒有用過什么框架,從Service服務(wù)的查找(類似JNDI)
到日志打印(類似Log4j),再到數(shù)據(jù)庫連接池(類似DBCP),全都是內(nèi)部人員自己
實(shí)現(xiàn)的。一來是因?yàn)轫?xiàng)目比較老,當(dāng)時(shí)可能還沒有什么開源框架可供使用,二來也是因?yàn)?br />
公司保守的策略,擔(dān)心使用不穩(wěn)定的開源框架可能會(huì)給項(xiàng)目帶來風(fēng)險(xiǎn)。這在當(dāng)時(shí)的環(huán)境下
也許是沒錯(cuò)的,公司高層自然會(huì)從更大的視角來考慮整個(gè)項(xiàng)目。
但是當(dāng)項(xiàng)目逐漸龐大起來,同時(shí)世界上優(yōu)秀的開源框架越來越多時(shí),如果不能及時(shí)重構(gòu)
并引入一些成熟的開源框架,最后的結(jié)果可能就是新招來的開發(fā)人員必須從頭開始學(xué)習(xí)
這個(gè)復(fù)雜的系統(tǒng)(都是內(nèi)部系統(tǒng),網(wǎng)上也沒有文檔幫助),還要小心內(nèi)部框架的種種Bug,
成本真是太高了。
(4)內(nèi)部框架跟不上行業(yè)的發(fā)展
前面說到了內(nèi)部框架的Bug。對(duì)于開源框架,可能會(huì)有框架創(chuàng)始者團(tuán)隊(duì)、大批的開源愛好者、
開源社區(qū)來支持。人民的力量是無窮的,Bug的修復(fù)速度可想而知,這點(diǎn)從最近開源后的
TextMate的Bug修復(fù)進(jìn)程就可以看出了。很多擱置了很久的Bug在開源后被愛好者們迅速
解決,而內(nèi)部框架呢?在當(dāng)初開發(fā)他的人員離開公司后,在沒有重大Bug時(shí)甚至都不會(huì)有人
去讀他的源代碼吧,差距可見一斑!
(5)當(dāng)然使用框架也不是一本萬利的事情
前面也提到過,使用不成熟的框架是有風(fēng)險(xiǎn)的,對(duì)于一個(gè)不是那么激進(jìn)的項(xiàng)目還是保守為好。
(除非這是一群自由沒拘束的技術(shù)狂熱分子,可以自行決定使用什么框架,那真是幸福的事)
就像我以前用過的Java的HA高可用性服務(wù)Sequioa一樣,這個(gè)框架最終不再被開發(fā)公司提供支持
了,這時(shí)風(fēng)險(xiǎn)就更大了。
此外,使用一些不常見的框架時(shí)還要注意框架源碼的License協(xié)議,不要在項(xiàng)目中隨意引用、
修改框架的源碼以免引起不必要的法律糾紛。
2.Struts2背后的架構(gòu)
既然前面已經(jīng)分析了框架的這么多好處,那我們自然會(huì)開始學(xué)習(xí)使用Struts2了。但使用Struts2
會(huì)繼承什么樣的優(yōu)雅架構(gòu)呢?其實(shí)從較高的抽象層次上看,它依然是我們熟悉的MVC模式。
對(duì)應(yīng)之前HelloWorld的例子來看,控制器C(FilterDispatcher)也就是我們?cè)趙eb.xml中聲明的
Struts2核心類。而模型M就是我們的NewsAction動(dòng)作類。而視圖V自然就是news.jsp了。模型
的概念似乎有些模糊,什么是模型呢?其實(shí)這個(gè)聽起來很名詞的概念在Struts2中既包含了靜態(tài)
從Web前端傳來的業(yè)務(wù)數(shù)據(jù),也包含了業(yè)務(wù)邏輯的實(shí)現(xiàn)。
有人可能會(huì)說這種架構(gòu)沒什么新意嘛,MVC框架有很多,這跟其他框架有什么區(qū)別呢?讓我們
站在低一級(jí)別的抽象層次上解剖Struts2,看看它有什么與眾不同。
乍看十分復(fù)雜,如果只從用戶角度來看,在開發(fā)時(shí)我們只需要實(shí)現(xiàn)黃色的部分,也就是我們
HelloWorld實(shí)例中的struts.xml,NewsAction和news.jsp。這就是我們要做的全部,就如前面
說的,只需要做很少的事情,我們就成為了這個(gè)優(yōu)秀架構(gòu)的一部分。
現(xiàn)在來看其他部分。FilterDispatcher就是我們配置在web.xml中的Servlet過濾器,這是Struts2
的入口,所有Struts2的Web應(yīng)用都要這樣配置。接下來藍(lán)色和綠色的部分就是Struts2的核心
了,可以說這些類都是Struts2的開發(fā)人員精心設(shè)計(jì)架構(gòu)的。
(1)客戶端發(fā)送請(qǐng)求,J2EE容器解析HTTP包,將其封裝成HttpServletRequest。
(2)FilterDispatcher攔截到這個(gè)請(qǐng)求,并根據(jù)請(qǐng)求路徑到ActionMapper中查詢決定調(diào)用哪個(gè)Action。
(3)根據(jù)ActionMapper的返回結(jié)果,F(xiàn)ilterDispatcher委托ActionProxy去struts.xml中找到這個(gè)Action。
(4)ActionProxy創(chuàng)建一個(gè)ActionInvocation,開始對(duì)Interceptor和Action進(jìn)行遞歸調(diào)用。
(5)各個(gè)Interceptor完成各自任務(wù)
(6)真正對(duì)Action的調(diào)用,返回結(jié)果路徑
(7)Result對(duì)象將返回?cái)?shù)據(jù)輸出到流中
(8)返回HttpServletResponse給J2EE容器,容器發(fā)送HTTP包到客戶端。
這就是Struts2的執(zhí)行流程,核心對(duì)象是ActionInvocation和Interceptor,以及還未介紹的ActionContext。
ActionInvocation是整個(gè)流程的總調(diào)度,它跟Spring AOP中的Invocation對(duì)象很像。而Interceptor有很多
都是Struts2自帶的,最重要的是保存請(qǐng)求參數(shù),并將前臺(tái)的數(shù)據(jù)傳遞到Action的成員變量上。
而ActionContext就是保存這些數(shù)據(jù)的全局上下文對(duì)象,最重要的是用來保存Action實(shí)例的ValueStack。
所謂全局是指ActionContext可以在Action以及Result中訪問,其實(shí)它是ThreadLocal類型。每個(gè)請(qǐng)求線程
都會(huì)有自己的Action和ActionContext實(shí)例。
可以說學(xué)習(xí)Struts2主要就是學(xué)習(xí):
(1)讓Interceptor和Action配合完成任務(wù)。
(2)將前臺(tái)數(shù)據(jù)保存到Action中。
(3)Result通過ValueStack從Action中得到返回?cái)?shù)據(jù)。
3.Struts2與Struts1的不同點(diǎn)
從上面的執(zhí)行流程已經(jīng)可以看出Struts1和2的巨大區(qū)別。
(1)ActionForm哪去了?Action還是那個(gè)Action嗎?
最明顯的就是我們?cè)谡麄€(gè)流程中都看不到ActionForm對(duì)象了,而且Action雖然還是叫這個(gè)名字,但是
看起來已經(jīng)跟Struts1中的Action完全不同了。
首先ActionForm被拋棄了,從前臺(tái)傳來的數(shù)據(jù)已經(jīng)可以保存到任意POJO了。先存到ActionForm再復(fù)制
到Dto對(duì)象的日子已經(jīng)是過去了。第二,這個(gè)POJO其實(shí)是Action對(duì)象中的一個(gè)成員變量。這在Struts1
中所有請(qǐng)求共享一個(gè)Action實(shí)例時(shí)是不可能的,現(xiàn)在Struts2會(huì)為每個(gè)請(qǐng)求都創(chuàng)建一個(gè)Action實(shí)例,所以
這樣做是行得通的。第三,雖然這樣可行,可是看起來好像Action作為MVC中的模型M既保存數(shù)據(jù),又
包含了業(yè)務(wù)邏輯,這是不是不良的設(shè)計(jì)啊?其實(shí)仔細(xì)想想,這樣的設(shè)計(jì)很方便,我們已經(jīng)得到了數(shù)據(jù),
直接就可以去操作Service層了。Action的職責(zé)看似多了,其實(shí)并不多。
(2)前端Servlet怎么變成了Filter?
我們知道Struts1和Spring MVC都是通過前端Servlet來作為入口的,為什么Struts2要用Servlet的過濾器呢?
因?yàn)镾truts2是基于Webwork核心的,與Struts1已經(jīng)完全不同了。Webwork可以說降低了應(yīng)用程序與J2EE
API的耦合,比如將ActionServlet改為Servlet的Filter,再比如對(duì)HttpServletRequest/Response的直接訪問,
又如任何POJO都能擔(dān)任ActionForm的角色,任何類不用實(shí)現(xiàn)Action接口就可以作為Action使用等等,
因此Struts2也繼承了這種優(yōu)秀的非侵入式設(shè)計(jì)。
這點(diǎn)與Spring的設(shè)計(jì)思想有些相像。比如那些Ware接口,不關(guān)心的Bean完全不需要實(shí)現(xiàn),盡量降低應(yīng)用
程序代碼與框架的耦合。侵入性的確是框架設(shè)計(jì)時(shí)要考慮的一個(gè)重要因素。
(3)Filter、Action、Result間的粘合劑OGNL
下圖可以清晰明了地展示出OGNL是如何融入Struts2框架的。
在輸入頁面InputForm.html和返回頁面ResultPage.jsp使用Struts2標(biāo)簽中訪問Action中的數(shù)據(jù)是如此方便,
OGNL使訪問ValueStack中保存的Action的屬性就像訪問ValueStack自己的屬性一樣方便。
對(duì)OGNL的大量使用是Struts2的一大特色。包括前臺(tái)標(biāo)簽傳值到Action,Result從Action中取值等都會(huì)大量
用到OGNL。而OGNL中大量用到了反射,我想也許這是Struts2性能不如Struts1的一個(gè)原因吧。畢竟獲得了
靈活而低耦合的架構(gòu)的同時(shí)是要付出一定代價(jià)的。
(4)Interceptor的強(qiáng)是無敵的強(qiáng)
Struts2中另一個(gè)強(qiáng)大的特性就是Interceptor攔截器了。Struts2內(nèi)建了大量的攔截器,攔截器使大量代碼可以
重復(fù)使用,自動(dòng)化了之前我們所說的瑣屑的任務(wù),從而使Struts2達(dá)到了高水平的關(guān)注分離。這真是AOP思想
在框架中應(yīng)用的典范!
Struts2三種數(shù)據(jù)轉(zhuǎn)移方式
Struts2提供了JavaBean屬性,JavaBean對(duì)象,ModelDriven對(duì)象三種方式來保存HTTP請(qǐng)求中的參數(shù)。下面通過一個(gè)最常見的
登錄的例子來看下這三種數(shù)據(jù)轉(zhuǎn)移方式。頁面代碼很簡(jiǎn)單,提交表單中包含有用戶名和密碼,在Action中得到這兩個(gè)參數(shù)從而
驗(yàn)證用戶是否登錄成功。
一、JavaBean屬性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
<%@ page contentType="text/html;charset=UTF-8" %> < html > < head ></ head > < body > < h1 >登錄頁</ h1 > < form action = "/cdai/login" method = "post" > < div > < label for = "username" >名稱:</ label > < input id = "username" name = "username" type = "textfield" /> </ div > < div > < label for = "password" >密碼:</ label > < input id = "password" name = "password" type = "password" /> </ div > < div > < label for = "rememberMe" > < input id = "rememberMe" name = "rememberMe" type = "checkbox" /> 記住我 </ label > < input type = "submit" value = "登錄" ></ input > </ div > </ form > </ body > </ html > |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
package com.cdai.web.ssh.action; import com.cdai.web.ssh.request.LoginRequest; import com.cdai.web.ssh.service.UserService; import com.opensymphony.xwork2.Action; import com.opensymphony.xwork2.ModelDriven; public class LoginAction implements Action { private String username; private String password; private UserService userService; @Override public String execute() { System.out.println( "Login action - " + request); return SUCCESS; } public String getUsername() { return request; } public void setUsername(String username) { this .username = username; } public String getPassword() { return request; } public void setPassword(String Password) { this .Password = Password; } } |
這種方式比較簡(jiǎn)明,直接將表單中的參數(shù)保存到Action中的屬性中。Action在驗(yàn)證時(shí)可能還需要將用戶名和密碼再封裝成Dto的
形式傳給Service層進(jìn)行驗(yàn)證。所以為什么不更進(jìn)一步,直接將用戶名和密碼保存到Dto中。
二、JavaBean對(duì)象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
<%@ page contentType="text/html;charset=UTF-8" %> < html > < head ></ head > < body > < h1 >登錄頁</ h1 > < form action = "/cdai/login" method = "post" > < div > < label for = "username" >名稱:</ label > < input id = "username" name = "request.username" type = "textfield" /> </ div > < div > < label for = "password" >密碼:</ label > < input id = "password" name = "request.password" type = "password" /> </ div > < div > < label for = "rememberMe" > < input id = "rememberMe" name = "rememberMe" type = "checkbox" /> 記住我 </ label > < input type = "submit" value = "登錄" ></ input > </ div > </ form > </ body > </ html > |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package com.cdai.web.ssh.action; import com.cdai.web.ssh.request.LoginRequest; import com.cdai.web.ssh.service.UserService; import com.opensymphony.xwork2.Action; import com.opensymphony.xwork2.ModelDriven; public class LoginAction implements Action { private LoginRequest request; private UserService userService; @Override public String execute() { System.out.println( "Login action - " + request); return SUCCESS; } public LoginRequest getRequest() { return request; } public void setRequest(LoginRequest request) { this .request = request; } } |
這樣就可以很方便地直接調(diào)用Service層了。但是有一個(gè)小缺點(diǎn)就是這樣加深了頁面參數(shù)名的深度,只有為參數(shù)名加上request
前綴(Action中的屬性名)才能使Struts2通過OGNL將表單中的參數(shù)正確保存到request對(duì)象中。
三、ModelDriven對(duì)象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
<%@ page contentType="text/html;charset=UTF-8" %> < html > < head ></ head > < body > < h1 >登錄頁</ h1 > < form action = "/cdai/login" method = "post" > < div > < label for = "username" >名稱:</ label > < input id = "username" name = "username" type = "textfield" /> </ div > < div > < label for = "password" >密碼:</ label > < input id = "password" name = "password" type = "password" /> </ div > < div > < label for = "rememberMe" > < input id = "rememberMe" name = "rememberMe" type = "checkbox" /> 記住我 </ label > < input type = "submit" value = "登錄" ></ input > </ div > </ form > </ body > </ html > |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package com.cdai.web.ssh.action; import com.cdai.web.ssh.request.LoginRequest; import com.cdai.web.ssh.service.UserService; import com.opensymphony.xwork2.Action; import com.opensymphony.xwork2.ModelDriven; public class LoginAction implements Action, ModelDriven<LoginRequest> { private LoginRequest request = new LoginRequest(); private UserService userService; @Override public String execute() { System.out.println( "Login action - " + request); return SUCCESS; } @Override public LoginRequest getModel() { return request; } } |
這種方式要多實(shí)現(xiàn)一個(gè)ModelDriven接口,將ModelDriven提供的對(duì)象也保存到ValueStack上,從而使前臺(tái)頁面可以直接通過
username和password屬性名來定義表單的參數(shù)名了。
三種方式具體采用哪種不能一概而論,還是看項(xiàng)目的具體需求再自己定吧!