第37章 MVC框架
37.1 MVC框架的實現
相信這本書的讀者對Struts的使用是得心應手了,也明白MVC框架有諸如視圖與邏輯解耦、靈活穩定、業務邏輯可重用等優點,而且還對其他的MVC框架(例如JSF、Spring MVC、WebWork)也了解一點。SSH(Struts+Spring+Hibernate)框架是Java項目常用的框架,作為一個Java開發人員,應該對SSH框架很熟悉了!我們今天就學Struts怎么用!我們要講的是MVC框架如何設計,你可以設計一個新的MVC框架與Struts抗衡。
在開始設計MVC框架前,首先要對MVC框架做一個簡單的介紹。MVC(Model ViewController)的中文名稱叫做模型視圖控制器模型,就是因為它的英文名字太流行了,中文名字反而被忽略了。它誕生于20世紀80年代,原本是為桌面應用程序建立起來的一個框架,現在反而在Web應用中大放異彩(其實也可以把B/S認為是C/S的瘦化結構),MVC框架的目的是通過控制器C將模型M(代表的是業務數據和業務邏輯)和視圖V(人機交互的界面)實現代碼分離,從而使同一個邏輯或行為或數據可以具有不同的表現形式,或者是同樣的應用邏輯共享相同、不同視圖。比如,可以用IE瀏覽器訪問某應用網站(頁面格式遵守HTML標準),也可以用手機通過WAP瀏覽器訪問(頁面格式遵守WML格式),對MVC框架來說,后臺的程序(也就是模型)不用做任何修改,只是使用的視圖不同而已。MVC框架如圖37-1所示。

圖37-1 MVC框架示意圖
該框架是Model2的結構。MVC框架有兩個版本,一個是Model1,也就是MVC的第一個版本,它的視圖中存在著大量的流程控制和代碼開發,也就是控制器和視圖還具有部分的耦合。也有人不認為Model1屬于MVC框架,那也說得通,因為在JSP頁面中融合了控制器和視圖的功能,這其實就是早期的開發模式,開發一堆的JSP頁面,然后再開發一堆的JavaBean,JavaBean就是模型了,它只是把JSP和JavaBean拆分開了。Model2版本則提倡視圖和模型的徹底分離,視圖僅僅負責展示服務,不再參與業務的行為和數據處理。我們舉例來說明MVC框架是如何圖37-1 MVC框架示意圖控制器(Controller)視圖(View)模型(Model)第37章運行的。
在做Web開發時,例如開發一個數據展示界面,從一張表中把數據全部讀出,然后展示到頁面上,也是一個簡單的表格,其中頁面展示的格式就是視圖V,怎么從數據庫中取得數據則是模型M,那控制器C是做什么的呢?它負責把接收的瀏覽器的請求轉發通知模型M處理,然后組合視圖V,最終反饋一個帶數據的視圖到用戶端,數據處理流程如圖37-2所示。

圖37-2 MVC框架的邏輯流
瀏覽器通過HTTP協議發出數據請求①,由控制器接收請求,通過路徑②委托給數據模型處理,模型通過與邏輯層和持久層的交互(路徑③④),把處理結果反饋給控制器(路徑⑤),控制器根據結果組裝視圖(路徑⑥⑦),并最終反饋給瀏覽器可以接受的HTML數據(路徑⑧)。整體MVC框架還是比較簡單的,但它帶來的優點非常多。
● 高重用性
一個模型可以有多個視圖,比如同樣是一批數據,可以是柱狀展示,也可以是條形展示,還可以是波形展示。同樣,多個模型也可以共享一個視圖,同樣是一個登錄界面,不同用戶看到的菜單數量(模型中的數據)不同,或者不同業務權限級別的用戶在同一個視圖中展示。
● 低耦合
因為模型和視圖分離,兩者沒有耦合關系,所以可以獨立地擴展和修改而不會產生相互影響。
● 快速開發和便捷部署
模型和視圖分離,可以使各個開發人員自由發揮,做視圖的人員和開發模型的人員可以制訂自己的計劃,然后在控制器的協作下實現完整的應用邏輯。
MVC框架還有很多優點,本章主要不是講解MVC技術,主要是通過講解設計MVC框架來說明設計模式該怎么應用,所以想了解更詳細的MVC框架信息請自行查閱資料。
37.1.1 MVC的系統架構
我們設計的MVC框架包含以下模塊:核心控制器(FilterDispatcher)、攔截器(Interceptor)、過濾器(Filter)、模型管理器(Model Action)、視圖管理器(View Provider)等,基本上一個MVC框架上常用的功能我們都具備了,系統架構如圖37-3所示。

圖37-3 MVC系統架構
各個模塊的職責如下:
● 核心控制器
MVC框架的入口,負責接收和反饋HTTP請求。
● 過濾器
Servlet容器內的過濾器,實現對數據的過濾處理。由于它是容器內的,因此必須依靠容器才能運行,它是容器的一項功能,與容器息息相關,本章就不詳細講述了。
● 攔截器
對進出模型的數據進行過濾,它不依賴系統容器,只過濾MVC框架內的業務數據。
● 模型管理器
提供一個模型框架,該框架內的所有業務操作都應該是無狀態的,不關心容器對象,例如Session、線程池等。
● 視圖管理器
管理所有的視圖,例如提供多語言的視圖等。
● 輔助工具
它其實就是一大堆的輔助管理工具,比如文件管理、對象管理等。
在我們的MVC框架中,核心控制器是最重要的,我們就先從它著手。核心控制器使用了Servlet容器的過濾器技術,需要編寫一個過濾器,所有進入MVC框架的請求都需要經過核心控制器的轉發,類圖如圖37-4所示。

圖37-4 核心控制器類圖
由于類圖中的部分輸入參數類型較長,省略了,請讀者仔細看代碼。首先閱讀FilterDispatcher代碼,如代碼清單37-1所示。
代碼清單37-1 核心控制器
public?class?FilterDispatcher?implements?Filter?{
?????//定義一個值棧輔助類
?????private?ValueStackHelper?valueStackHelper?=?new?ValueStackHelper();
?????//應用IActionDispatcher
?????IActionDispather?actionDispatcher?=?new?ActionDispatcher();
?????//servlet銷毀時要做的事情
?????public?void?destroy()?{
?????}
?????//過濾器必須實現的方法
?????public?void?doFilter(ServletRequest?request,?ServletResponse?response,
??????????FilterChain?chain)?throws?IOException,?ServletException?{
??????????//轉換為HttpServletRequest
??????????HttpServletRequest?req?=?(HttpServletRequest)request;
??????????HttpServletResponse?res?=?(HttpServletResponse)response;
??????????//傳遞到其他過濾器處理
??????????chain.doFilter(req,?res);
??????????//獲得從HTTP請求的ACTION名稱
??????????String?actionName?=?getActionNameFromURI(req);
??????????//對ViewManager的應用
??????????ViewManager?viewManager?=?new?ViewManager(actionName);
??????????//所有參數放入值棧
??????????ValueStack?valueStack?=?valueStackHelper.putIntoStack(req);
??????????//把所有的請求傳遞給ActionDispatcher處理
??????????String?result?=actionDispatcher.actionInvoke(actionName);
??????????String?viewPath?=?viewManager.getViewPath(result);
??????????//直接轉向
??????????RequestDispatcher?rd?=?req.getRequestDispatcher(viewPath);
??????????rd.forward(req,?res);
?????}
?????public?void?init(FilterConfig?arg0)?throws?ServletException?{
??????????/*
??????????*?1、檢查XML配置文件是否正確
??????????*?2、啟動監控程序,觀察配置文件是否正確
??????????*/
?????}
?????//通過url獲得actionName
?????private?String?getActionNameFromURI(HttpServletRequest?req){
??????????String?path?=?(String)?req.getRequestURI();
??????????String?actionName?=?path.substring(path.lastIndexOf("/")?+?1,
??????????path.lastIndexOf("."));
??????????return?actionName;
?????}
}
我們按照系統的執行順序來講解,首先在容器的配置文件中需要配置該過濾器,以tomcat為例,配置如代碼清單37-2所示。
代碼清單37-2 核心控制器的配置
<?xml?version="1.0"?encoding="UTF-8"?>
<web-app>
?<filter>
??<display-name>FilterDispatcher</display-name>
??<filter-name>FilterDispatcher</filter-name>
??<filter-class>{包名}.FilterDispatcher</filter-class>
?</filter>
?<filter-mapping>
???<filter-name>FilterDispatcher</filter-name>
???<url-pattern>*.do</url-pattern>
??</filter-mapping>
</web-app>
在這里定義了對所有以.do結尾的請求進行攔截,攔截后由FilterDispatcher的doFilter方法處理。過濾器是在啟動時自動初始化,初始化完畢后立刻調用inti方法,在init方法中我們做了兩件事情。
● 檢查XML配置文件
所有的Action與視圖的對應關系是在配置文件中配置的,因此若配置文件出錯,該應用應該停止響應,這就需要在啟動時對XML文件進行完整性檢查和語法分析。
● 啟動監視器
配置文件隨時都可以修改,但是它修改后不應該需要重新啟動應用才能生效,否則對系統的正常運行有非常大的影響,因此這里要使用到Listener(監聽)行為了。
init方法需要做的這兩件事情是非常重要的,而且都還包含了幾種不同的設計模式。首先我們來看檢查XML配置文件如何實現。先看我們定義的XML格式(框架中應該定義一個DTD文件,XML文件的模板,讀者可以自行實現),如代碼清單37-3所示。
代碼清單37-3 XML配置文件
<?xml?version="1.0"?encoding="UTF-8"?>
<mvc>
?????<action?name="loginAction"?class="{類名全路徑}"?method="execute">
??????????<result?name="success">/index2.jsp</result>
??????????<result?name="fail">/index.jsp</result>
?????</action>
</mvc>
讀者思考一下該怎么檢查這個XML文件,有兩個不同的檢查策略:一是檢查XML文件的語法是否正確;二是框架邏輯檢查,這是什么意思呢?比如我們在XML文件中配置了一個類A,它只有一個方法methodA,在method中編寫的配置文件為method="methoda",方法名寫錯了,那這樣的配置是肯定不能運行的,需要框架邏輯檢查把它揪出來。這兩種不同的算法是完全可以替換的,而且很有必要替換,邏輯檢查在應用啟動的時候需要對所有的類進行過濾處理,犧牲的是效率,這在測試機上沒有問題,在生產機上要花20分鐘才能把一個應用啟動起來,在分秒必爭的業務系統中這是不允許的,因此就要求該算法可以退休,想用的時候(測試機環境)就用,不想用的時候(生產環境)就不用,想到什么模式了嗎?策略模式,這兩個算法都是對同樣的源文件進行檢查,只是算法不同,當然可以相互替換了。類圖比較簡單,就不再畫了,我們直接看代碼,抽象策略如代碼清單37-4所示。
代碼清單37-4 XML文件校驗
public?interface?IXmlValidate?{
?????//只有一個方法,檢查XML是否符合條件
?????public?boolean?validate(String?xmlPath);
}
根據一個指定的路徑,對XML進行校驗,返回校驗結果。普通XML校驗如代碼清單37-5所示。
代碼清單37-5 普通XML校驗
public?class?CommonXmlValidate?implements?IXmlValidate?{
?????//XML語法檢查,比如是否少寫了一個結束標志
?????public?boolean?validate(String?xmlPath)?{
??????????return?false;
?????}
}
由于讀寫XML文件一般使用DOM4J或者JDOM,都提供對XML文件的語法校驗功能,不符合XML語法(比如一個節點少寫了結束標志</node)的文件是不能解析的,讀者可以在自己編寫框架時使用該類型工具。
框架的邏輯算法如代碼清單37-6所示。
代碼清單37-6 框架邏輯校驗
public?class?LogicXmlValidate?implements?IXmlValidate?{
?????//檢查xmlPath是否符合邏輯,比如不會出現一個類中沒有的方法
?????public?boolean?validate(String?xmlPath)?{
??????????return?false;
?????}
}
邏輯校驗相對比較復雜,它的邏輯流程如下:
● 讀取XML文件。
● 使用反射技術初始化一個對象(配置文件中的class屬性值)。
● 檢查是否存在配置文件中配置的方法。
● 檢查方法的返回值是否是String,并且無輸入參數,同時必須繼承指定類或接口。
邏輯校驗需要把所有的對象都初始化一遍,在Action類較多的情況下,效率較低,但它可以提前發現出現訪問異常的情況,把問題解決在萌芽狀態。我們繼續來看兩個策略的場景類,如代碼清單37-7所示。
代碼清單37-7 策略的場景類
public?class?Checker?{
?????//使用哪一個策略
?????private?IXmlValidate?validate;
?????//xml配置文件的路徑
?????String?xmlPath;
?????//構造函數傳遞
?????public?Checker(IXmlValidate?_validate){
?????????this.validate?=?_validate;
?????}
?????public?void?setXmlPath(String?_xmlPath){
??????????this.xmlPath?=?_xmlPath;
?????}
?????//檢查
?????public?boolean?check(){
??????????return?validate.validate(xmlPath);
?????}
}
與通用策略模式稍有不同,每個模式在實際應用環境中都有其個性,很少出現完全照搬一個模式的情況,靈活應用設計模式才是關鍵。
在FilterDispatcher的init方法中,我們剛剛說它有兩個職責:第一個職責是XML文件校驗,這個我們完成了;第二個職責是啟動監控程序。問題是要監控什么呢?監控XML有沒有被修改,如果修改了就立刻通知校驗程序對它進行校驗。這就又用到了觀察者模式:發現文件被修改,它立刻通知檢查者處理,該片段的類圖如圖37-5所示。

圖37-5 XML文件監控類圖
為什么要在這里定義一個Watchable接口呢?它表示所有可以監視的資源,比如數據庫、日志文件、磁盤空間等。我們來看代碼,監聽接口如代碼清單37-8所示。
代碼清單37-8 監聽接口
public?interface?Watchable?{
?????//監聽
?????public?void?watch();
}
文件監聽者是觀察者模式的被觀察者,它一旦發現文件發生變化立刻通知觀察者,如代碼清單37-9所示。
代碼清單37-9 文件監聽者
public?class?FileWatcher?extends?Observable?implements?Watchable{
?????//是否要重新加載XML文件
?????private?boolean?isReload?=?false;
?????//啟動監視
?????public?void?watch(){
??????????//啟動一個線程,每隔15秒掃描一下文件,發現文件日期被修改,立刻通知觀察者
??????????super.addObserver(new?Checker());
??????????super.setChanged();
??????????super.notifyObservers(isReload);
?????}
}
由于框架是在操作系統之上運行的,文件變化時操作系統是不會通知應用系統的,因此我們能做的就是啟動一個線程監視一批文件,發現文件改變了,立刻通知相關的處理者,它雖然有時間延遲,但對于一個應用框架來說是非常有必要的,避免了重啟應用才能使配置生效的情況。
讀者可能很疑惑,這種死循環的監控方式會不會對性能產生影響,答案是不會!為什么呢?
檢查一個文件的時間一般是毫秒級的,相對于我們設置的運行周期(比如15秒執行一次)是一個非常微小的運行時間,對應用不會產生任何影響。大家都在使用Log4j進行日志處理,它有一個線程是每5秒檢查一次日志是否滿,大家覺得性能受影響了嗎?基本上性能影響可以忽略不計。
由于Checker還要作為觀察者,因此它要實現Observer接口,同時實現update方法,如代碼清單37-10所示。
代碼清單37-10 修正后的檢查者
public?class?Checker?implements?Observer{
?????public?void?update(Observable?arg0,?Object?arg1)?{
??????????//檢查是否符合條件
??????????arg1?=?check();
?????}
}
到此為止,我們把init方法已經講解完畢,它是在容器初始化時調用。有一個HTTP請求發送過來,容器調用我們編寫的doFilter方法。仔細看一下我們的代碼,其中有這樣一句話:Chain.doFilter(req,res),這句話是什么意思呢?是說讓后續的過濾器先運行,等它們運行完畢后該過濾器再運行,應該想到這是一個責任鏈模式,它的類型是FilterChain。Servlet容器把所有的過濾器組合在一起形成了一個過濾器鏈,它是怎么做到的呢?容器啟動的時候,把所有的過濾器都初始化完畢,然后根據它們在web.xml中的配置順序,從上向下組裝一個過濾器鏈。注意所有的過濾器都必須實現Filter接口,這是建立過濾器鏈的首要前提。
我們再回過頭來仔細看看類圖,是不是有點熟悉?對,類似于中介者模式,我們并沒有把中介者傳遞到各個同事類,只是我們采用中介者模式的思想,把中介者的職責分發出去由各個同事類來處理。
37.1.2 模型管理器
模型管理器是整個MVC框架的難點,在這里我們會看到非常多的設計模式。我們在核心控制器的類圖中看到有一個IActionDispatcher接口,它實現的模型行為分發是一個門面模式,如代碼清單37-11所示。
代碼清單37-11 模型行為分發接口
public?interface?IActionDispather?{
??????//根據Action的名字,返回處理結果
??????public?String?actionInvoke(String?actionName);
}
它的職責非常簡單,得到actionName就執行,熟悉Struts的讀者可能很清楚這個方法是非常復雜的,它要從配置文件中找到執行對象,然后執行方法,還要考慮值棧、異常等,非常復雜。我們這里就有一個方法,它對外提供一個門面,所有的訪問都是通過該門面來完成,其實現類如代碼清單37-12所示。
代碼清單37-12 模型分發實現
public?class?ActionDispather?implements?IActionDispather?{
?????//需要執行的Action
?????private?ActionManager?actionManager?=?new?ActionManager();
?????//攔截器鏈
?????private?ArrayList<Interceptors>?listInterceptors?=?InterceptorFactory.createInterceptors();
?????public?String?actionInvoke(String?actionName)?{
??????????//前置攔截器
??????????return?actionManager.execAction(actionName);
??????????//后置攔截器
?????}
}
它是一個非常簡單的類,對外部提供統一封裝好的行為。模型管理器的類圖如圖37-6所示。
首先說ActionManager類,它負責管理所有的行為類Action,那就必須定義一個行為類的接口或抽象類,如代碼清單37-13所示。
代碼清單37-13 抽象Action
public?abstract?class?ActionSupport?{
?????public?final?static?String?SUCCESS?=?"success";
?????public?final?static?String?FAIL?=?"fail";
?????//默認的執行方法
?????public?String?execute(){
??????????return?SUCCESS;
?????}
}

圖37-6 模型管理器類圖
抽象的ActionSupport類看起來很簡單,其實它可不簡單,所有的模型行為都繼承該類,它之所以提供一個默認的execute方法,是因為在xml的配置文件中,可以省略掉method="XXX"這句話,默認就是調用該方法。它還有一個非常重要的行為:對象映射,把HTTP傳遞過來的字符串映射到一個業務對象上,我們會在值棧中詳細講解。
讀者可能很疑惑,Action的操作是需要獲得環境數據的,比如HTTPServletRequest的數據,還有系統中的Session數據,單單一個ActionManager如何獲得這些數據呢?通過值棧,在值棧中保存著該Action需要的所有數據。
我們再來看ActionManager類,如代碼清單37-14所示。
代碼清單37-14 Action管理類
public?class?ActionManager?{
?????//執行Action的指定方法
?????public?String?execAction(String?actionName){
?????????return?null;
?????}
}
就這么簡單嗎?非也,其中的參數actionName指xml配置中的name屬性值,它與從HTTP傳遞過來的請求對象是一致的,根據HTTP傳遞過來的actionName在xml文件中查找對應的節點(Node),然后就可以獲取到該類的名稱和方法,通過動態代理的方式執行該方法,在這里我們使用到了代理模式。
有讀者可能聽說過反射是影響性能的,它提供解釋型操作。是這樣的,但是實際應用還沒有這么高的要求,把數據庫設計得優秀一點,系統架構多考慮一點,提升的性能遠比這個多。
然后我們再來看攔截器,攔截器和過濾器的區別就是:攔截器可以脫離容器(J2EE容器)運行,而過濾器不行。攔截器的目的是對數據和行為進行過濾,符合條件的才可以執行Action,或者是在Action執行完畢后,調用攔截器進行回收處理。我們定義一個抽象的攔截器,如代碼清單37-15所示。
代碼清單37-15 抽象攔截器
public?abstract?class?AbstractInterceptor?{
?????//獲得當前的值棧
?????private?ValueStack?valueStack?=?ValueStackHelper.getValueStack();
?????//攔截器類型:前置、后置、環繞
?????private?int?type?=0;
?????//當前的值棧
?????protected?ValueStack?getValueStack(){
??????????return?valueStack;
?????}
?????//攔截處理
?????public?final?void?exec(){
??????????//根據type不同,處理方式也不同
?????}
?????//攔截器類型
?????protected?abstract?void?setType(int?type);
?????//子類實現的攔截器
?????protected?abstract?void?intercept();
}
這怎么和Struts的攔截器不相同呀!是的,Struts的攔截器的攔截方法intercept是要接收一個ActionInvocation對象,這里卻沒有,我們主要是講解模式,是為了技術實現,而類似Struts的MVC框架屬于工業級別的應用框架,考慮了太多的外界因素。攔截器分為三種。
● 前置攔截器
在Action調用前執行,對Action需要的場景數據進行過濾或重構。
● 后置攔截器
在Action調用后執行,負責回收場景,或對Action的后續事務進行處理。
● 環繞攔截器
在Action調用前后都執行。
我們的框架在這里使用了一個模板方法模式,開發者繼承AbstractInterceptor后,只要完成兩個職責即可:定義攔截類型(setType)和實現攔截器要攔截的方法(intercept),不用考慮它到底如何調用ActionInvocation,相對來說簡單又實用。
有攔截器就肯定有攔截器鏈,多個攔截器組合在一起就成了攔截器鏈,如代碼清單37-16所示。
代碼清單37-16 攔截器鏈
public?class?Interceptors?implements?Iterable<AbstractInterceptor>?{
?????//根據攔截器列表建立一個攔截器鏈
?????public?Interceptors(ArrayList<AbstractInterceptor>?list){
?????}
?????//列出所有的攔截器
?????public?Iterator<AbstractInterceptor>?iterator()?{
??????????return?null;
?????}
?????//攔截器鏈的執行方法
?????public?void?intercept(){
??????????//委托攔截器執行
?????}
}
它實現了Iterable接口,提供了一個方便遍歷攔截器的方法,這是迭代器模式。同時,由于是一個鏈結構,我們就想到了責任鏈,這里確實也是一個責任鏈模式,只是核心控制器上的過濾鏈是Servlet容器自己實現的,而攔截器鏈則需要我們自己編碼實現。代碼不復雜,讀者可以參考責任鏈章節。
這里還有兩個很有意思的方法。我們來看構造函數,它通過一個容納有攔截器的動態數組生成一個攔截器鏈,它是一個自激行為,在XML文件中配置一個攔截器,其中包含多個攔截器,我們的構造函數就是這樣的用途,自己建立一條鏈,而不是父類或者高層模塊。再看intercept方法,鏈中每個節點都是一個攔截器,都有一個intercept方法,攔截器鏈中的intercept方法行為是委托第一個節點攔截器的intercept方法,然后所有的攔截器都會按照順序執行一遍,這一點和我們的責任鏈模式是不同的,責任鏈模式是只要有節點處理就可以認為是結束,后續節點可以不再參與處理。
Struts還實現了方法攔截器,只要繼承MethodFilterInterceptor即可,主要使用了反射技術,有興趣的話可以看看源代碼。注意我們這里使用了攔截器鏈而不像Struts那樣是攔截器棧,一字之差,系統設計差別可就大了。
注意 攔截器是會影響系統性能的,所有的Action在執行前后都會被攔截器過濾一遍,即使不符合攔截條件的也會被檢查一遍,所以非必要情況不要使用攔截器。
由于在XML配置文檔中有太多的攔截器鏈,因此需要有一個工廠來創建它,否則太煩瑣。如代碼清單37-17所示。
代碼清單37-17 攔截器鏈工廠
public?class?InterceptorFactory?{
?????public?static?ArrayList<Interceptors>?createInterceptors(){
??????????//根據配置文件創建出所有的攔截器鏈
??????????return?null;
?????}
}
它的作用是根據配置文件一次性地創建出所有的攔截器,很簡單的工廠方法模式。如果讀者還記得我們剛剛講的配置文件更新問題的話,應該想到這里也應該有一個觀察者,配置文件修改了,攔截器鏈當然也要重建了,確實應該有這樣一個觀察者,讀者可以自行思考如何實現。
37.1.3 值棧
值棧按道理說應該很簡單,就是把HTTP傳遞過來的String字符串壓到堆棧中。聽起來很簡單,實現起來就比較有難度了,它要完成兩個職責。
● 管理堆棧
不僅僅是出棧、入棧這么簡單,它要管理棧中數據,同時還要允許前置攔截器對棧中數據進行修改,限制后置攔截器對棧的修改,還要把棧中數據與HTTPServletRequest中的數據建立關聯。
● 值映射
從HTTP傳遞過來的數據都是字符串結構,那怎么才能轉化成一個業務對象呢?比如在頁面上有一個登錄框,輸入用戶名(userName)和密碼(password)。提交到MVC框架中怎么才能轉為一個User對象呢?這也是值棧要完成的職責。
這里說一下值映射,怎么實現一個值的映射,這也是一個反射操作的結果。首先是HTTP傳遞過來的參數名稱中要明確映射到哪一個對象,例如使用點號(.)區分,點號前是對象名稱,點號后是屬性名,如此規定后就可以輕松地處理了。由于使用的模式較少,這里就不再贅述。讀者若有興趣可以考慮使用一些開源工具,比如dozer等。
37.1.4 視圖管理器
視圖管理器的功能很單一,按照模型指定的要求返回視圖,在這里用到的主要模式就是橋梁模式,如果大家做過多語言的開發就非常清楚了,比如一個外部網站,提供中日英三種語言版本,我們不可能每個語言都寫一套頁面吧。一般是定義一個語言資源文件,然后視圖根據不同的語言環境加載不同的語言。我們先來說視圖,它包含三部分。
● 靜態頁面
比如圖片放在什么地方,字體大小是什么樣子,菜單應該放置在什么地方,這部分工作是由前臺人員開發的,不涉及業務邏輯和業務數據。
● 動態頁面元素
它指的是在一個固定場景下不發生變化但在異構場景中發生變化的元素,其中語言就屬于動態頁面元素,還有為使用不同瀏覽器而開發的代碼。比如瀏覽器IE、Firefox、Chrome等,雖然基本上都是符合HTML,但是還有一些細節差異,特別是在JavaScript的處理方面,稍不注意就可能產生災難。
● 動態數據
由模型產生的數據,它對視圖來說是結構固定,并可反復加載。
在這三部分中,靜態頁面是完全靜態的,動態頁面元素是稍微有點動感,動態數據完全是多變的(數據結構不發生變化,否則頁面無法展現)。把動態數據融入到靜態頁面中比較容易,已經在配置文件中指定要把模型中的數據放到哪個頁面中,現在的問題是怎么把動態頁面元素融入到靜態頁面中。靜態頁面有很多,語言類型也有很多,怎么融合在一起提供給瀏覽器訪問呢?
橋梁模式可以解決用什么筆(圓珠筆、鉛筆)和畫什么圖形(圓形、方形)的問題,我們遇到的問題與此場景類似。先看類圖,如圖37-7所示。

圖37-7 視圖與語言類圖
大家還記得Struts是怎么配置多語言的文件嗎?我們采用類似的結構,如代碼清單37-18所示。
代碼清單37-18 資源配置文件
title=標題
menu=菜單
英文配置菜單與此類似,它的結構就是一個Map類型,我們把它讀入到Map中,抽象類如代碼清單37-19所示。
代碼清單37-19 抽象語言
public?abstract?class?AbsLangData?{
??????//獲得所有的動態元素的配置項
??????public?abstract?Map<String,String>?getItems();
}
getItems方法是獲得一種語言下的所有配置。我們來看中文語言包,如代碼清單37-20所示。
代碼清單37-20 中文語言
public?class?GBLangData?extends?AbsLangData?{
?????@Override
?????public?Map<String,?String>?getItems()?{
??????????/*
???????????*?Map?的結構為:
???????????*?key='title',?value='標題'
???????????*?key='menu',?value='菜單'
???????????*/
??????????return?null;
?????}
}
英文語言如代碼清單37-21所示。
代碼清單37-21 英文語言
public?class?ENLangData?extends?AbsLangData?{
?????@Override
?????public?Map<String,?String>?getItems()?{
??????????/*
???????????*?Map結構為:
???????????*?key='title',value='title';
???????????*?key='menu',?value='menu'
???????????*/
??????????return?null;
?????}
}
視圖分為兩種類圖,一種是需要直接替換資源文件的視圖,比如JSP文件,框架直接把語言包中的資源項替換掉JSP中的條目即可,把{title}替換為“標題”,把{menu}替換為“菜單”,替換后存在框架的緩存目錄中,提高系統的訪問效率。另一種視圖是不能替換的,比如SWF文件,它的資源可以通過類似HTTP傳遞參數的形式傳遞,重寫一個URL即可。我們首先來看抽象視圖,如代碼清單37-22所示。
代碼清單37-22 抽象視圖
public?abstract?class?AbsView?{
?????private?AbsLangData?langData;
?????//必須有一個語言文件
?????public?AbsView(AbsLangData?_langData){
??????????this.langData?=?_langData;
?????}
?????//獲得當前的語言
?????public?AbsLangData?getLangData(){
??????????return?langData;
?????}
?????//頁面的URL路徑
?????public?String?getURI(){
??????????return?null;
?????}
?????//組裝一個頁面
?????public?abstract?void?assemble();
}
JSP視圖是需要替換資源項,如代碼清單37-23所示。
代碼清單37-23 JSP視圖
public?class?JspView?extends?AbsView?{
?????//傳遞語言配置
?????public?JspView(AbsLangData?_langData){
??????????super(_langData);
?????}
?????@Override
?????public?void?assemble()?{
??????????Map<String,String>?langMap?=?getLangData().getItems();
??????????for(String?key:langMap.keySet()){
????????????????/*
?????????????????*?直接替換文件中的語言條目
?????????????????*
?????????????????*/
???????????}
?????}
}
SWF文件是不能替換的,采用重寫URL的方式,如代碼清單37-24所示。
代碼清單37-24 SWF視圖
public?class?SwfView?extends?AbsView?{
?????public?SwfView(AbsLangData?_langData){
??????????super(_langData);
?????}
?????@Override
?????public?void?assemble()?{
???????????Map<String,String>?langMap?=?getLangData().getItems();
???????????for(String?key:langMap.keySet()){
??????????????????/*
???????????????????*?組裝一個HTTP的請求格式:
???????????????????*?http://abc.com/xxx.swf?key1=value&key2=value
???????????????????*/
??????????????????}
??????????}
}
ViewManager是一個視圖模塊的入口,所有的訪問都是通過它傳遞進來的,如代碼清單37-25所示。
代碼清單37-25 視圖管理
public?class?ViewManager?{
??????//Action的名稱
??????private?String?actionName;
??????//當前的值棧
??????private?ValueStack?valueStack?=?ValueStackHelper.getValueStack();
??????//接收一個ActionName
??????public?ViewManager(String?_actionName){
????????????this.actionName?=?_actionName;
??????}
??????//根據模型的返回結果提供視圖
??????public?String?getViewPath(String?result){
?????????????//根據值棧查找到需要提供的語言
?????????????AbsLangData?langData?=?new?GBLangData();
?????????????//根據action和result查找到指定的視圖,并加載語言
?????????????AbsView?view?=?new?JspView(langData);
?????????????//返回視圖的地址
?????????????return?view.getURI();
??????}
}
通過橋梁模式我們把不同的語言和不同類型的視圖結合起來,共同提供一個多語言的應用系統,即使以后增加語言也非常容易擴展。
37.1.5 工具類
每個框架或項目都有大量的工具類,MVC框架也不例外。先來看操作XML文件的工具類,不可能自己讀寫XML文件,我們使用DOM4J來實現,它在大文件的處理上性能很有優勢,而且比較簡單,架構也非常優秀。
使用DOM4J從XML文件中讀出的對象是節點(Node)、元素(Element)、屬性(Attribute)等,這些對象還是比較容易理解的,但是不能保證一個開發組的人對這些都了解,因此需要把它轉換成每個開發成員都理解的對象,比如我們處理這樣一段XML代碼,如代碼清單37-26所示。
代碼清單37-26 XML文件片段
<action?name="loginAction"?class="{類名全路徑}"?method="execute">
?????<result?name="success">/index2.jsp</result>
?????<result?name="fail">/index.jsp</result>
</action>
使用DOM4J查找到該節點是一個Node對象,如果要取得屬性,就需要轉換為一個元素(Element)對象,這不是每個開發成員都能理解的,于是給架構師提出的問題就是:如何把一個DOM4J對象轉換成自己設計的對象。答案是適配器模式,我們首先定義一個Action節點類,如代碼清單37-27所示。
代碼清單37-27 Action節點類
public?abstract?class?ActionNode?{
?????//Action的名稱
?????private?String?actionName;
?????//Action的類名
?????private?String?actionClass;
?????//方法名,默認是execute
?????private?String?methodName?=?"excuete";
?????//視圖路徑
?????private?String?view;
?????public?String?getActionName()?{
??????????return?actionName;
?????}
?????public?String?getActionClass()?{
??????????return?actionClass;
?????}
?????public?String?getMethodName()?{
??????????return?methodName;
?????}
?????public?abstract?String?getView(String?Result);
}
它是一個抽象類,其中的getView是一個抽象方法,是根據執行結果查找到視圖路徑。只要編寫一個適配器就可以把Elemet對象轉為Action節點,如代碼清單37-28所示。
代碼清單37-28 Action節點
public?class?XmlActionNode?extends?ActionNode?{
?????//需要轉換的element
?????private?Element?el;
?????//通過構造函數傳遞
?????public?XmlActionNode(Element?_el){
??????????this.el?=?_el;
?????}
?????@Override
?????public?String?getActionName(){
??????????return?getAttValue("name");
?????}
?????@Override
?????public?String?getActionClass(){
??????????return?getAttValue("class");
?????}
?????@Override
?????public?String?getMethodName(){
??????????return?getAttValue("method");
?????}
?????public?String?getView(String?result){
??????????ViewPathVisitor?visitor?=?new?ViewPathVisitor("success");
??????????el.accept(visitor);
??????????return?visitor.getViewPath();
?????}
?????//獲得指定屬性值
?????private?String?getAttValue(String?attName){
??????????Attribute?att?=?el.attribute(attName);
??????????return?att.getText();
?????}
}
這是一個對象適配器,傳遞進來一個Element對象,把它轉換為ActionNode對象,這樣設計以后,系統開發人員就不用考慮開源工具對系統的影響,屏蔽了工具系統的影響,這是一個典型的適配器模式應用。
不知道讀者是否注意到getView方法,它使用了一個訪問者模式,這是DOM4J提供的一個非常優秀的API接口,傳遞進去一個訪問者就可以遍歷出我們需要的對象。我們來看自己定義的訪問者,如代碼清單37-29所示。
代碼清單37-29 訪問者
public?class?ViewPathVisitor?extends?VisitorSupport?{
?????//獲得指定的路徑
?????private?String?viewPath;
?????private?String?result;
?????//傳遞模型結果
?????public?ViewPathVisitor(String?_result){
??????????result?=?_result;
?????}
?????@Override
?????public?void?visit(Element?el){
??????????Attribute?att?=?el.attribute("name");
??????????if(att?!=?null){
????????????????if(att.getName().equals("name")?&&?att.getText().equals(result)){
??????????????????????viewPath?=?el.getText();
?????????????????}
???????????}
?????}
?????public?String?getViewPath(){
??????????return?viewPath;
?????}
}
DOM4J提供了VisitorSupport抽象接口,可以接受元素、節點、屬性等訪問者。我們這里接受了一個元素訪問者,對所有的元素過濾一遍,然后找到自己需要的元素,非常強大!
我們繼續分析,在IoC容器中都會區分對象是單例模式還是多例模式。想想我們的框架,每個HTTP請求都會產生一個線程,如果我們的Action初始化的時候是單例模式會出現什么情況?當并發足夠多的時候就會產生阻塞,性能會嚴重下降,在特殊情況下還會產生線程不安全,這時就需要考慮多例情況。那多例是如何處理呢?使用Clone技術,首先在系統啟動時初始化所有的Action,然后每過來一個請求就拷貝一個Action,減少了初始化對象的性能消耗。典型的原型模式,但問題也同時產生了,并發較多時,就可能會產生內存溢出的情況,內存不夠用了!于是享元模式就可以上場了,建立一個對象池以容納足夠多的對象。
- 前言
- 第一部分 大旗不揮,誰敢沖鋒——6大設計原則全新解讀
- 第1章 單一職責原則
- 1.2 絕殺技,打破你的傳統思維
- 1.3 我單純,所以我快樂
- 1.4 最佳實踐
- 第2章 里氏替換原則
- 2.2 糾紛不斷,規則壓制
- 2.3 最佳實踐
- 第3章 依賴倒置原則
- 3.2 言而無信,你太需要契約
- 3.3 依賴的三種寫法
- 3.4 最佳實踐
- 第4章 接口隔離原則
- 4.2 美女何其多,觀點各不同
- 4.3 保證接口的純潔性
- 4.4 最佳實踐
- 第5章 迪米特法則
- 5.2 我的知識你知道得越少越好
- 5.3 最佳實踐
- 第6章 開閉原則
- 6.2 開閉原則的廬山真面目
- 6.3 為什么要采用開閉原則
- 6.4 如何使用開閉原則
- 6.5 最佳實踐
- 第二部分 真刀實槍 ——23種設計模式完美演繹
- 第7章 單例模式
- 7.2 單例模式的定義
- 7.3 單例模式的應用
- 7.4 單例模式的擴展
- 7.5 最佳實踐
- 第8章 工廠方法模式
- 8.2 工廠方法模式的定義
- 8.3 工廠方法模式的應用
- 8.4 工廠方法模式的擴展
- 8.5 最佳實踐
- 第9章 抽象工廠模式
- 9.2 抽象工廠模式的定義
- 9.3 抽象工廠模式的應用
- 9.4 最佳實踐
- 第10章 模板方法模式
- 10.2 模板方法模式的定義
- 10.3 模板方法模式的應用
- 10.4 模板方法模式的擴展
- 10.5 最佳實踐
- 第11章 建造者模式
- 11.2 建造者模式的定義
- 11.3 建造者模式的應用
- 11.4 建造者模式的擴展
- 11.5 最佳實踐
- 第12章 代理模式
- 12.2 代理模式的定義
- 12.3 代理模式的應用
- 12.4 代理模式的擴展
- 12.5 最佳實踐
- 第13章 原型模式
- 13.2 原型模式的定義
- 13.3 原型模式的應用
- 13.4 原型模式的注意事項
- 13.5 最佳實踐
- 第14章 中介者模式
- 14.2 中介者模式的定義
- 14.3 中介者模式的應用
- 14.4 中介者模式的實際應用
- 14.5 最佳實踐
- 第15章 命令模式
- 15.2 命令模式的定義
- 15.3 命令模式的應用
- 15.4 命令模式的擴展
- 15.5 最佳實踐
- 第16章 責任鏈模式
- 16.2 責任鏈模式的定義
- 16.3 責任鏈模式的應用
- 16.4 最佳實踐
- 第17章 裝飾模式
- 17.2 裝飾模式的定義
- 17.3 裝飾模式應用
- 17.4 最佳實踐
- 第18章 策略模式
- 18.2 策略模式的定義
- 18.3 策略模式的應用
- 18.4 策略模式的擴展
- 18.5 最佳實踐
- 第19章 適配器模式
- 19.2 適配器模式的定義
- 19.3 適配器模式的應用
- 19.4 適配器模式的擴展
- 19.5 最佳實踐
- 第20章 迭代器模式
- 20.2 迭代器模式的定義
- 20.3 迭代器模式的應用
- 20.4 最佳實踐
- 第21章 組合模式
- 21.2 組合模式的定義
- 21.3 組合模式的應用
- 21.4 組合模式的擴展
- 21.5 最佳實踐
- 第22章 觀察者模式
- 22.2 觀察者模式的定義
- 22.3 觀察者模式的應用
- 22.4 觀察者模式的擴展
- 22.5 最佳實踐
- 第23章 門面模式
- 23.2 門面模式的定義
- 23.3 門面模式的應用
- 23.4 門面模式的注意事項
- 23.5 最佳實踐
- 第24章 備忘錄模式
- 24.2 備忘錄模式的定義
- 24.3 備忘錄模式的應用
- 24.4 備忘錄模式的擴展
- 24.5 最佳實踐
- 第25章 訪問者模式
- 25.2 訪問者模式的定義
- 25.3 訪問者模式的應用
- 25.4 訪問者模式的擴展
- 25.5 最佳實踐
- 第26章 狀態模式
- 26.2 狀態模式的定義
- 26.3 狀態模式的應用
- 第27章 解釋器模式
- 27.2 解釋器模式的定義
- 27.3 解釋器模式的應用
- 27.4 最佳實踐
- 第28章 享元模式
- 28.2 享元模式的定義
- 28.3 享元模式的應用
- 28.4 享元模式的擴展
- 28.5 最佳實踐
- 第29章 橋梁模式
- 29.2 橋梁模式的定義
- 29.3 橋梁模式的應用
- 29.4 最佳實踐
- 第三部分 誰的地盤誰做主 ——設計模式PK
- 第30章 創建類模式大PK
- 30.1 工廠方法模式VS建造者模式
- 30.2 抽象工廠模式VS建造者模式
- 第31章 結構類模式大PK
- 31.1 代理模式VS裝飾模式
- 31.2 裝飾模式VS適配器模式
- 第32章 行為類模式大PK
- 32.1 命令模式VS策略模式
- 32.2 策略模式VS狀態模式
- 32.3 觀察者模式VS責任鏈模式
- 第33章 跨戰區PK
- 33.1 策略模式VS橋梁模式
- 33.2 門面模式VS中介者模式
- 33.3 包裝模式群PK
- 第四部分 完美世界 ——設計模式混編
- 第34章 命令模式+責任鏈模式
- 34.2 混編小結
- 第35章 工廠方法模式+策略模式
- 35.2 混編小結
- 第36章 觀察者模式+中介者模式
- 36.2 混編小結
- 第五部分 擴展篇
- 第37章 MVC框架
- 37.2 最佳實踐
- 第38章 新模式
- 38.1 規格模式
- 38.2 對象池模式
- 38.3 雇工模式
- 38.4 黑板模式
- 38.5 空對象模式
- 附錄 23種設計模式彩圖