本文是專欄文章(SpringMVC深度探險)系列的文章之一,博客地址爲:http://downpour.iteye.com/blog/1330537 程序員
在咱們熟知的創建在三層結構(表示層、業務邏輯層、持久層)基礎之上的J2EE應用程序開發之中,表示層的解決方案最多。由於在表示層自身的知識觸角不少,須要解決的問題也很多,這也就不免形成與之對應的解決方案層出不窮。
筆者在不少討論中常常能夠看到相似"某某框架已死",或者"某某框架已經足以戰勝全部其餘的框架"的言論。事實上,每一種解決方案都有着自身獨有的存在價值和歷史背景。若是單單從某一個方面或者某幾個方面去看一個框架,那麼評論不免有失偏頗。
因此,整個系列的第一篇文章,咱們脫開SpringMVC框架自己,把SpringMVC放到一個更大的知識體系範圍之中,講一講整個Web開發領域、尤爲是MVC框架的發展歷程。正如"認識歷史才能看清將來",當咱們可以正確審視整個MVC框架的發展歷程,也就可以分析它的發展趨勢,而且站在一個更高的高度來對全部的解決方案進行評價。
兩種模型
從整個B/S程序的運行結構來看,J2EE的表示層解決方案其實是對"請求-響應"模式的一種實現。既然謂之"請求-響應"也就勢必存在着兩大溝通角色:
因爲這兩大角色的承載載體和編程語言實現基礎都不一樣,於是也就產生了兩種大相徑庭的針對表示層的解決方案的設計思路: web
業界對於上述這兩種不一樣的設計模型也賦予了不一樣的名詞定義:前一種被稱之爲MVC模型;後一種則被稱之爲組件模型,也有稱之爲事件模型。
注:筆者我的對於這兩種模型的概念定義並非很是認同。由於在筆者我的的觀點認爲,MVC模型的定義角度所針對的是編程元素的劃分;而組件模型(事件模型)的定義角度是動態交互方式的表述。因此咱們在這裏強調的是解決方案自身所設立的基準和側重點的不一樣。
從使用者的社區力量上來看,無疑MVC模型得到了更多程序員的青睞。這裏面的緣由不少,咱們在這裏也不想過多展開對兩種不一樣編程模型之間的討論。不過在這裏,咱們將針對同一個業務場景(用戶註冊)分別給出基於這兩個編程模型的代碼示例,幫助讀者瞭解這兩種編程模型在設計思想上的不一樣之處。 spring
【MVC模型】
在MVC模型中,咱們選取當前比較熱門的兩大框架Struts2和SpringMVC做爲代碼示例。
首先,咱們將用戶註冊場景中最爲核心的"用戶類"定義出來: 編程
public class User { 安全
private String email; 服務器
private String password; 架構
// 省略了setter和getter方法 mvc
} app
緊接着是一個簡單的JSP表單:
<form method="post" action="/register">
<label>Email:</label><input type="text" name="email" />
<label>Password:</label><input type="password" name="password" />
<input type="submit" value="submit" />
</form>
上述這兩段代碼不管是SpringMVC仍是Struts2,均可以共用。而在請求響應處理類(也就是Controller)上的設計差別是兩個框架最大的不一樣。
若是使用SpringMVC,那麼Controller的代碼看上去就像這樣:
@Controller
@RequestMapping
public class UserController {
@RequestMapping("/register")
public ModelAndView register(String email, String password) {
// 在這裏調用具體的業務邏輯代碼
return new ModelAndView("register-success");
}
}
若是使用Struts2,那麼Controller的代碼看上去就稍有不一樣:
public class UserController {
private String email;
private String password;
public String register() {
// 在這裏調用具體的業務邏輯代碼
return "register-success";
}
// 這裏省略了setter和getter方法
}
除此以外,Struts2還須要在某個配置文件中進行請求映射的配置:
<action name="register" class="com.demo2do.sandbox.web.UserController" method="register">
<result name="success">/register-success.jsp</result>
</action>
從上面的代碼示例中,咱們能夠爲整個MVC模型的實現總結概括出一些特色:
1. 框架自己並不經過某種手段來干預或者控制瀏覽器發送Http請求的行爲方式。
從上面的代碼中咱們就能夠看到,不管是SpringMVC仍是Struts2,它們在請求頁面的實現中都使用了原生HTML代碼。就算是Http請求的發送,也藉助於HTML之中對Form提交請求的支持。
2. 頁面(View層)和請求處理類(Controller)之間的映射關係經過某一種配置形式維繫起來。
咱們能夠看到在瀏覽器和Web服務器之間的映射關係在不一樣的框架中被賦予了不一樣的表現形式:在SpringMVC中,使用了Annotation註解;在Struts2中,默認採起XML配置文件。不過不管是哪種配置形式,隱藏在其背後的都是對於請求映射關係的定義。
3. Controller層的設計差別是不一樣MVC框架之間最主要的差別。
這一點其實是咱們在對於MVC模型自身進行定義時就反覆強調的一點。在上面的例子中,咱們能夠看到SpringMVC使用方法參數來對請求的數據進行映射;而Struts2使用Controller類內部的屬性來進行數據請求的映射。
在MVC模型中,瀏覽器端和服務器端的交互關係很是明確:不管採起什麼樣的框架,老是以一個明確的URL做爲中心,輔之以參數請求。所以,URL看上去就像是一個明文的契約,固然,真正蘊藏在背後的是Http協議。全部的這些東西都被放在了檯面上,咱們能夠很是明確地獲取到一次交互中全部的Http信息。這也是MVC模型中最爲突出的一個特色。
【組件模型】
在組件模型中,咱們則選取較爲成熟的Tapestry5做爲咱們的代碼示例。
首先,咱們來看看請求頁面的狀況:
<form t:type="form" t:id="form">
<t:label for="email"/>:<input t:type="TextField" t:id="email" t:validate="required,minlength=3" size="30"/>
<t:label for="password"/>:<input t:type="PasswordField" t:id="password" t:validate="required,minlength=3" size="30"/>
<input type="submit" value="Login"/>
</form>
在這裏,請求的頁面再也不是原生的HTML代碼,而是一個擴展後的HTML,這一擴展包含了對HTML標籤的擴展(增長了新的標籤,例如<t:label>),也包含了對HTML自身標籤中屬性的擴展(增長新的支持屬性,例如t:type,t:validate)。
接着咱們來看看服務器端響應程序:
public class Register {
private String email;
private String password;
@Component(id = "password")
private PasswordField passwordField;
@Component
private Form form;
String onSuccess() {
return "PostRegister";
}
// 這裏省略了setter和getter方法
從上面的代碼示例中,咱們能夠看到一些與MVC模型大相徑庭的特色:
1. 框架經過對HTML進行行爲擴展來干預和控制瀏覽器與服務器的交互過程。
咱們能夠發現,Tapestry5的請求頁面被加入了更多的HTML擴展,這些擴展包括對HTML標籤的擴展以及HTML標籤中屬性的擴展。而這些擴展中,有很多直接干預了瀏覽器與服務器的交互。例如,上面例子中的t:validate="required,minlength=3"擴展實際上就會被自動映射到服務器端程序中帶有@Component(id="password")標註的PasswordField組件上,並在提交時自動進行組件化校驗。而當頁面上的提交按鈕被點擊觸發時,默認在服務器端的onSuccess方法會造成響應並調用其內部邏輯。
2. 頁面組件的實現是整個組件模型的絕對核心
從上述的例子中,咱們能夠看到組件模型的實現不只須要服務器端實現,還須要在頁面上指定與某個特定組件進行事件綁定。二者缺一不可,必須相互配合,共同完成。所以整個Web程序的交互能力徹底取決於頁面組件的實現好壞。
3. 頁面組件與服務器端響應程序之間的映射契約並不基於Http協議進行
在上面的例子中,從頁面組件到服務器端的響應程序之間的映射關係是經過名稱契約而定的。而頁面上的每一個組件能夠指定映射到服務器端程序的具體某一個方法。咱們能夠看到這種映射方式並非一種基於URL或者Http協議的映射方式,而是一種命名指定的方式。
在組件模型中,瀏覽器端和服務器端的交互關係並不以一個具體的URL爲核心,咱們在上述的例子中甚至徹底沒有看到任何URL的影子。不過這種事件響應式的方式,也提供給咱們另一個編程的思路,而這種基於契約式的請求-響應映射也獲得了一部分程序員的喜好。於是組件模型的粉絲數量也是不少的。
MVC模型的各類形態
以前咱們已經談到,MVC模型是一種以服務器響應程序(也就是Controller)爲核心進行程序設計的,於是全部的MVC框架的歷史發展進程其實是一個圍繞着Controller不斷進行重構和改造的過程。而在這個過程當中,不一樣的MVC框架也就表現出了不一樣的表現形態。接下來,咱們就給出一些具備表明意義的MVC框架表現形態。
注:筆者在這裏將提到三種不一樣的MVC框架的表現形態,實際上與請求-響應的實現模式有着密切的聯繫,有關這一方面的內容,請參閱另一篇博文的內容:《Struts2技術內幕》 新書部分篇章連載(五)—— 請求響應哲學
【Servlet】
Servlet規範是最基本的J2EE規範,也是咱們進行Web開發的核心依賴。它雖然自身並不構成開發框架,可是咱們不得不認可全部的MVC框架都是從最基本的Servlet規範發展而來。所以,咱們能夠得出一個基本結論:
downpour 寫道
Servlet是MVC模型最爲基本的表現形態。
在Servlet規範中所定義的請求處理響應接口是這樣的:
咱們能夠看到,Servlet的基本接口定義中:
參數列表 —— Http請求被封裝爲一個HttpServletRequest對象(或者ServletRequest對象),而Http響應封裝爲一個HttpServletResponse對象(或者ServletResponse對象)
返回值 —— 方法不存在返回值(返回值爲void)
在這個設計中,HttpServletRequest和HttpServletResponse承擔了完整的處理Http請求的任務。而這兩個Servlet對象的職責也有所分工:
HttpServletRequest對象 —— 主要用於處理整個Http生命週期中的數據。
HttpServletResponse對象 —— 主要用於處理Http的響應結果。
這裏實際上有一點"數據與行爲分離"的意味。也就是說,在Servlet處理請求的過程當中,其實也是Servlet中響應方法內部的邏輯執行過程當中,若是須要處理請求數據或者返回數據,那麼咱們須要和HttpServletRequest打交道;若是須要處理執行完畢以後的響應結果,那麼咱們須要和HttpServletResponse打交道。
這樣的設計方式,是一種骨架式的設計方式。由於Servlet是咱們進行Web開發中最底層的標準,因此咱們能夠看到接口設計中的返回值對於一個最底層標準而言毫無心義。由於不存在一個更底層的處理程序會對返回值進行進一步的處理,咱們不得不在Servlet的過程當中自行處理瀏覽器的行爲控制。
MVC模型的這一種形態,被筆者冠以一個名稱:參數-參數(Param-Param)實現模式。由於在響應方法中,數據與行爲的操做載體都以參數的形式出現。
Servlet的設計模型是全部MVC模型表現形態中最爲基礎也是最爲底層的一種模型,全部其餘模型都是創建在這一模型的基礎之上擴展而來。
【Struts1.X】
Struts1.X是一個較爲早期的MVC框架實現,它的歷史最先能夠追溯到2000年,做爲Apache開源組織的一個重要項目,取名爲"Struts",有"基礎構建"的含義。在那個程序框架尚處於朦朧階段的年代,"基礎構建"無疑是每一個程序員求之不得的東西。
對於Struts1.X,咱們仍是把關注的重點放在Struts中的Controller層的定義上:
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response);
若是和以前的Servlet模型加以比較咱們就能夠發現,Struts1.X對於基本的Servlet模型作了必定的擴展和重構:
通過一番擴展和重構,咱們能夠發現Struts1.X相比較於原始的Servlet模型已經有了必定的進步。好比,咱們能夠再也不直接操做HttpServletResponse這樣的原生Servlet對象來進行Http返回的處理;再好比,對於一些簡單的請求數據讀取,咱們能夠沒必要直接操做生硬的HttpServletRequest接口,而經過ActionForm來完成。
MVC模型發展到了這裏,咱們能夠看到響應方法中的"返回值"已經可以被調動起來用在整個Http請求的處理過程當中。所以,這種在響應方法中參數和返回值同時參與到Http請求的處理過程當中的表現形態,被筆者冠以另一個名稱:參數-返回值(Param-Return)實現模式。
因爲Struts1.X已經再也不是一個底層的實現規範,因而響應方法"返回值"被框架引入,加入到了整個處理過程之中。咱們能夠看到,在這裏最大的進步之處就在於:引入了新的編程元素,從而優化整個邏輯處理過程。編程元素的引入很是重要,由於對於一個任何一個程序員而言,充分調用全部能夠利用的編程要素是衡量一個程序寫得好壞的重要標準。以後,咱們還能夠看到其餘的框架在引入編程元素這個方面所作的努力。
【Webwork2 / Struts2】
隨着時間的推動,愈來愈多的程序員在使用Struts1.X進行開發的過程當中發現Struts1.X在設計上存在的一些不足。而與此同時,各類各樣的Web層的解決方案也如雨後春筍般涌現出來。不只僅是以MVC模型爲基礎的開發框架,還有包括JSF和Tapestry之類的基於組件模型的開發框架也在這個時期誕生並不斷髮展壯大。所以,這個時期應該是整個Web層解決方案的大力發展時期。
而在這些框架中,有一個來自於Opensymphony開源社區的優秀框架Webwork2探索了一條與傳統Servlet模型不一樣的解決方案,逐漸被你們熟識和理解,不斷髮展並獲得了廣大程序員的承認。2004年,Webwork2.1.7版本發佈,成爲Webwork2的一個重要里程碑,它以優秀的設計思想和靈活的實現,吸引了大批的Web層開發人員投入它的懷抱。
或許是看到了Struts1.X發展上的侷限性,Apache社區與Opensymphony開源組織在2005年末宣佈將來的Struts項目將與Webwork2項目合併,並聯合推出Struts2,經過Apache社區的人氣優點與OpenSymphony的技術優點,共同打造下一代的Web層開發框架。這也就是Struts2的由來。
從整個過程當中,咱們能夠發現,Webwork2和Struts2是一脈相承的Web層解決方案。而二者可以在一個至關長的時間段內佔據開發市場主導地位的重要緣由在於其技術上的領先優點。而這一技術上的領先優點,突出表現爲對Controller的完全改造:
public class UserController {
private User user
public String execute() {
// 這裏加入業務邏輯代碼
return "success";
}
// 這裏省略了setter和getter方法
}
從上面的代碼中,咱們能夠看到Webwork2 / Struts2對於Controller最大的改造有兩點:
這兩大改造被看做是框架的神來之筆。由於經過這一改造,整個Controller類完全與Web容器解耦,能夠方便地進行單元測試。而擺脫了Servlet束縛的Controller,也爲整個編程模型賦予了全新的定義。
固然,這種改造的前提條件在於Webwork2 / Struts2引入了另一個重要的編程概念:ThreadLocal模式。使得Controller成爲一個線程安全的對象被Servlet模型所調用,這也就突破了傳統Servlet體系下,Servlet對象並不是一個線程安全的對象的限制條件。
注:有關ThreadLocal模式相關的話題,請參考另一篇博文:《Struts2技術內幕》 新書部分篇章連載(七)—— ThreadLocal模式
從引入新的編程元素的角度來講,Webwork2 / Struts2無疑也是成功的。由於在傳統Servlet模式中的禁地Controller中的屬性變量被合理利用了起來做爲請求處理過程當中的數據部分。這樣的改造不只使得表達式引擎可以獲得最大限度的發揮,同時使得整個Controller看起來更像是一個POJO。於是,這種表現形態被筆者冠以的名稱是:POJO實現模式。
POJO實現模式是一種具備革命性意義的模式,由於它可以把解耦合這樣一個觀點發揮到極致。從面向對象的角度來看,POJO模式無疑也是全部程序員所追求的一個目標。這也就是Webwork2 / Struts2那麼多年來經久不衰的一個重要緣由。
【SpringMVC】
相比較Webwork2 / Struts2,SpringMVC走了一條比較溫和的改良路線。由於SpringMVC自始至終都沒有突破傳統Servlet編程模型的限制,而是在這過程當中不斷改良,不斷重構,反而在發展中開拓了一條嶄新的道路。
咱們能夠看看目前最新版本的SpringMVC中對於Controller的定義:
@Controller
@RequestMapping
public class UserController {
@RequestMapping("/register")
public ModelAndView register(String email, String password) {
// 在這裏調用具體的業務邏輯代碼
return new ModelAndView("register-success");
}
}
咱們在這裏引用了在以前的講解中曾經使用過的代碼片斷。不過這一代碼片斷剛恰好能夠說明SpringMVC在整個Controller改造中所涉及到的一些要點:
1. 使用參數-返回值(Param-Return)實現模式來打造Controller
方法的參數(email和password)被視做是Http請求參數的歸納。而在這裏,它們已經被SpringMVC的框架有效處理並屏蔽了內在的處理細節,呈現出來的是與請求參數名稱一一對應的參數列表。而返回值ModelAndView則表示Http的響應是一個數據與視圖的結合體,表示Http的處理結果。
2. 引入Annotation來完成請求-響應的映射關係
引入Annotation來完成請求-響應的映射關係,是SpringMVC的一個重大改造。在早期的SpringMVC以及其餘的MVC框架中,一般都是使用XML做爲基礎配置的。而Annotation的引入將本來分散的關注點合併到了一塊兒,爲實現配置簡化打下了堅實的基礎。
3. 泛化參數和返回值的含義
這是一個蘊含的特色。事實上,SpringMVC在響應方法上,能夠支持多種多樣不一樣的參數類型和返回值類型。例如,當參數類型爲Model時,SpringMVC將會自動將請求參數封裝於Model內部而傳入請求方法;當返回值類型是String時,直接表示SpringMVC須要返回的視圖類型和視圖內容。固然,這些泛化的參數和返回值的內容所有都由SpringMVC在框架內部處理了。
若是咱們來評述一下這些特色就會發現,SpringMVC雖然是一個溫和的改良派,倒是在改良這個領域作得最爲出色的。以引入Annotation爲例,引入Annotation來完成請求-響應映射,不正是咱們反覆強調的引入併合理使用新的編程元素來完成處理任務嘛?而泛化後的參數和返回值,則可讓程序員在寫Controller的代碼時能夠爲所欲爲,再也不受到任何契約的束縛,這樣一來接口的邏輯語義也就可以更加清晰。
MVC模型的發展軌跡
以前講了那麼多MVC模型的實現形態,咱們是否能從中總結出一條發展軌跡呢?答案是確定的,筆者在這裏做了一副圖:
從圖中,咱們能夠看到三類徹底不一樣的發展方向。目前,Struts1.X這一條路被證實已經窮途末路;另外的兩條發展軌跡整體來講實力至關,SpringMVC大有趕超之勢。
那麼,爲何曾經一度佔領了大部分市場的Struts2會在近一段時間內被SpringMVC大幅趕超呢?這裏面的緣由多種多樣,有自身架構上的緣由,有設計理念上的緣由,可是筆者認爲,其本質緣由仍是在於Struts2對於技術革新的力度遠不及SpringMVC。
若是咱們回顧一下Struts2過去可以獨佔鰲頭的緣由就能夠發現,Struts2的領先在於編程模型上的領先。其引入的POJO模型幾乎是一個殺手級的武器。而基於這一模型上的攔截器、OGNL等技術的支持使得其餘的編程模型在短期很難超越它。
可是隨着時代的發展,Struts2在技術革新上的做爲彷佛步子就邁得比較小。咱們能夠看到,在JDK1.5普及以後,Annotation做爲一種新興的Java語法,逐漸被你們熟知和應用。這一點上SpringMVC緊跟了時代的潮流,直接用於請求-響應的映射。而Struts2卻遲遲沒法在單一配置源的問題上造成突破。固然,這只是技術革新上的一個簡單的例子,其餘的例子還有不少。
有關Struts2和SpringMVC的比較話題,咱們在以後的討論中還會有所涉及,不過筆者並不但願在這裏引發框架之間的爭鬥。你們應該客觀看待每一個框架自身設計上的優秀之處和不足之處,從而造成我的本身的觀點。
從整個MVC框架的發展軌跡來看,咱們能夠得出一個很重要的結論:
downpour 寫道
MVC框架的發展軌跡,始終是伴隨着技術的革新(不管是編程模型的改變仍是引入新的編程元素)共同向前發展。而每一次的技術革新,都會成爲MVC框架發展過程當中的里程碑。
小結 在本文中所講的一些話題觸角涉及到了Web開發的各個方面。做爲SpringMVC的前傳,筆者我的認爲將整個MVC框架的發展歷程講清楚,你們才能更好地去了解SpringMVC自己。而咱們在這裏所談到的一些概念性的話題,也算是對過去十年以來MVC框架的一個小結,但願對讀者有所啓示。