[+]
第一章
Web MVC簡介
Web MVC簡介
1.1、Web開發中的請求-響應模型:
在Web世界裏,具體步驟如下:
1、 Web瀏覽器(如IE)發起請求,如訪問http://sishuok.com
2、 Web服務器(如Tomcat)接收請求,處理請求(比如用戶新增,則將把用戶保存一下),最後產生響應(一般爲html)。
3、web服務器處理完成後,返回內容給web客戶端(一般就是我們的瀏覽器),客戶端對接收的內容進行處理(如web瀏覽器將會對接收到的html內容進行渲染以展示給客戶)。
因此,在Web世界裏:
都是Web客戶端發起請求,Web服務器接收、處理併產生響應。
一般Web服務器是不能主動通知Web客戶端更新內容。雖然現在有些技術如服務器推(如Comet)、還有現在的HTML5 websocket可以實現Web服務器主動通知Web客戶端。
到此我們瞭解了在web開發時的請求/響應模型,接下來我們看一下標準的MVC模型是什麼。
1.2、標準MVC模型概述
MVC模型:是一種架構型的模式,本身不引入新功能,只是幫助我們將開發的結構組織的更加合理,使展示與模型分離、流程控制邏輯、業務邏輯調用與展示邏輯分離。如圖1-2
圖1-2
首先讓我們瞭解下MVC(Model-View-Controller)三元組的概念:
Model(模型):數據模型,提供要展示的數據,因此包含數據和行爲,可以認爲是領域模型或JavaBean組件(包含數據和行爲),不過現在一般都分離開來:Value Object(數據) 和 服務層(行爲)。也就是模型提供了模型數據查詢和模型數據的狀態更新等功能,包括數據和業務。
View(視圖):負責進行模型的展示,一般就是我們見到的用戶界面,客戶想看到的東西。
Controller(控制器):接收用戶請求,委託給模型進行處理(狀態改變),處理完畢後把返回的模型數據返回給視圖,由視圖負責展示。 也就是說控制器做了個調度員的工作,。
從圖1-1我們還看到,在標準的MVC中模型能主動推數據給視圖進行更新(觀察者設計模式,在模型上註冊視圖,當模型更新時自動更新視圖),但在Web開發中模型是無法主動推給視圖(無法主動更新用戶界面),因爲在Web開發是請求-響應模型。
那接下來我們看一下在Web裏MVC是什麼樣子,我們稱其爲 Web MVC 來區別標準的MVC。
1.3、Web MVC概述
模型-視圖-控制器概念和標準MVC概念一樣,請參考1.2,我們再看一下Web MVC標準架構,如圖1-3:
如圖1-3
在Web MVC模式下,模型無法主動推數據給視圖,如果用戶想要視圖更新,需要再發送一次請求(即請求-響應模型)。
概念差不多了,我們接下來了解下Web端開發的發展歷程,和使用代碼來演示一下Web MVC是如何實現的,還有爲什麼要使用MVC這個模式呢?
1.4、Web端開發發展歷程
此處我們只是簡單的敘述比較核心的歷程,如圖1-4
圖1-4
1.4.1、CGI:(Common Gateway Interface)公共網關接口,一種在web服務端使用的腳本技術,使用C或Perl語言編寫,用於接收web用戶請求並處理,最後動態產生響應給用戶,但每次請求將產生一個進程,重量級。
1.4.2、Servlet:一種JavaEE web組件技術,是一種在服務器端執行的web組件,用於接收web用戶請求並處理,最後動態產生響應給用戶。但每次請求只產生一個線程(而且有線程池),輕量級。而且能利用許多JavaEE技術(如JDBC等)。本質就是在java代碼裏面 輸出 html流。但表現邏輯、控制邏輯、業務邏輯調用混雜。如圖1-5
圖1-5
如圖1-5,這種做法是絕對不可取的,控制邏輯、表現代碼、業務邏輯對象調用混雜在一起,最大的問題是直接在Java代碼裏面輸出Html,這樣前端開發人員無法進行頁面風格等的設計與修改,即使修改也是很麻煩,因此實際項目這種做法不可取。
1.4.3、JSP:(Java Server Page):一種在服務器端執行的web組件,是一種運行在標準的HTML頁面中嵌入腳本語言(現在只支持Java)的模板頁面技術。本質就是在html代碼中嵌入java代碼。JSP最終還是會被編譯爲Servlet,只不過比純Servlet開發頁面更簡單、方便。但表現邏輯、控制邏輯、業務邏輯調用還是混雜。如圖1-6
圖1-6
如圖1-6,這種做法也是絕對不可取的,控制邏輯、表現代碼、業務邏輯對象調用混雜在一起,但比直接在servlet裏輸出html要好一點,前端開發人員可以進行簡單的頁面風格等的設計與修改(但如果嵌入的java腳本太多也是很難修改的),因此實際項目這種做法不可取。
JSP本質還是Servlet,最終在運行時會生成一個Servlet(如tomcat,將在tomcat\work\Catalina\web應用名\org\apache\jsp下生成),但這種使得寫html簡單點,但仍是控制邏輯、表現代碼、業務邏輯對象調用混雜在一起。
1.4.4、Model1:可以認爲是JSP的增強版,可以認爲是jsp+javabean如圖1-7
特點:使用<jsp:useBean>標準動作,自動將請求參數封裝爲JavaBean組件;還必須使用java腳本執行控制邏輯。
圖1-7
此處我們可以看出,使用<jsp:useBean>標準動作可以簡化javabean的獲取/創建,及將請求參數封裝到javabean,再看一下Model1架構,如圖1-8。
圖1-8 Model1架構
Model1架構中,JSP負責控制邏輯、表現邏輯、業務對象(javabean)的調用,只是比純JSP簡化了獲取請求參數和封裝請求參數。同樣是不好的,在項目中應該嚴禁使用(或最多再demo裏使用)。
1.4.5、Model2:在JavaEE世界裏,它可以認爲就是Web MVC模型
Model2架構其實可以認爲就是我們所說的Web MVC模型,只是控制器採用Servlet、模型採用JavaBean、視圖採用JSP,如圖1-9
圖1-9 Model2架構
具體代碼事例如下:
從Model2架構可以看出,視圖和模型分離了,控制邏輯和展示邏輯分離了。
但我們也看到嚴重的缺點:
1. 1、控制器:
1.1.1、控制邏輯可能比較複雜,其實我們可以按照規約,如請求參數submitFlag=toAdd,我們其實可以直接調用toAdd方法,來簡化控制邏輯;而且每個模塊基本需要一個控制器,造成控制邏輯可能很複雜;
1.1.2、請求參數到模型的封裝比較麻煩,如果能交給框架來做這件事情,我們可以從中得到解放;
1.1.3、選擇下一個視圖,嚴重依賴Servlet API,這樣很難或基本不可能更換視圖;
1.1.4、給視圖傳輸要展示的模型數據,使用Servlet API,更換視圖技術也要一起更換,很麻煩。
1.2、模型:
1.2.1、此處模型使用JavaBean,可能造成JavaBean組件類很龐大,一般現在項目都是採用三層架構,而不採用JavaBean。
1.3、視圖
1.3.1、現在被綁定在JSP,很難更換視圖,比如Velocity、FreeMarker;比如我要支持Excel、PDF視圖等等。
1.4.5、服務到工作者:Front Controller + Application Controller + Page Controller + Context
即,前端控制器+應用控制器+頁面控制器(也有稱其爲動作)+上下文,也是Web MVC,只是責任更加明確,詳情請參考《核心J2EE設計模式》和《企業應用架構模式》如圖1-10:
圖1-10
運行流程如下:
職責:
Front Controller:前端控制器,負責爲表現層提供統一訪問點,從而避免Model2中出現的重複的控制邏輯(由前端控制器統一回調相應的功能方法,如前邊的根據submitFlag=login轉調login方法);並且可以爲多個請求提供共用的邏輯(如準備上下文等等),將選擇具體視圖和具體的功能處理(如login裏邊封裝請求參數到模型,並調用業務邏輯對象)分離。
Application Controller:應用控制器,前端控制器分離選擇具體視圖和具體的功能處理之後,需要有人來管理,應用控制器就是用來選擇具體視圖技術(視圖的管理)和具體的功能處理(頁面控制器/命令對象/動作管理),一種策略設計模式的應用,可以很容易的切換視圖/頁面控制器,相互不產生影響。
Page Controller(Command):頁面控制器/動作/處理器:功能處理代碼,收集參數、封裝參數到模型,轉調業務對象處理模型,返回邏輯視圖名交給前端控制器(和具體的視圖技術解耦),由前端控制器委託給應用控制器選擇具體的視圖來展示,可以是命令設計模式的實現。頁面控制器也被稱爲處理器或動作。
Context:上下文,還記得Model2中爲視圖準備要展示的模型數據嗎,我們直接放在request中(Servlet API相關),有了上下文之後,我們就可以將相關數據放置在上下文,從而與協議無關(如Servlet API)的訪問/設置模型數據,一般通過ThreadLocal模式實現。
到此,我們回顧了整個web開發架構的發展歷程,可能不同的web層框架在細節處理方面不同,但的目的是一樣的:
乾淨的web表現層:
模型和視圖的分離;
控制器中的控制邏輯與功能處理分離(收集並封裝參數到模型對象、業務對象調用);
控制器中的視圖選擇與具體視圖技術分離。
輕薄的web表現層:
做的事情越少越好,薄薄的,不應該包含無關代碼;
只負責收集並組織參數到模型對象,啓動業務對象的調用;
控制器只返回邏輯視圖名並由相應的應用控制器來選擇具體使用的視圖策略;
儘量少使用框架特定API,保證容易測試。
到此我們瞭解Web MVC的發展歷程,接下來讓我們瞭解下Spring MVC到底是什麼、架構及來個HelloWorld瞭解下具體怎麼使用吧。
本章具體代碼請參考 springmvc-chapter1工程。、
第二章
Spring MVC入門
瀏覽(20983)|評論(9)
交流分類:Java|筆記分類: 跟開濤學Spring……
2.1、Spring Web MVC是什麼
Spring Web MVC是一種基於Java的實現了Web MVC設計模式的請求驅動類型的輕量級Web框架,即使用了MVC架構模式的思想,將web層進行職責解耦,基於請求驅動指的就是使用請求-響應模型,框架的目的就是幫助我們簡化開發,Spring Web MVC也是要簡化我們日常Web開發的。
另外還有一種基於組件的、事件驅動的Web框架在此就不介紹了,如Tapestry、JSF等。
Spring Web MVC也是服務到工作者模式的實現,但進行可優化。前端控制器是DispatcherServlet;
應用控制器其實拆爲處理器映射器(Handler Mapping)進行處理器管理和視圖解析器(View Resolver)進行視圖管理;頁面控制器/動作/處理器爲Controller接口(僅包含ModelAndView
handleRequest(request, response)
方法)的實現(也可以是任何的POJO類);支持本地化(Locale)解析、主題(Theme)解析及文件上傳等;提供了非常靈活的數據驗證、格式化和數據綁定機制;提供了強大的約定大於配置(慣例優先原則)的契約式編程支持。
2.2、Spring Web MVC能幫我們做什麼
√讓我們能非常簡單的設計出乾淨的Web層和薄薄的Web層;
√進行更簡潔的Web層的開發;
√天生與Spring框架集成(如IoC容器、AOP等);
√提供強大的約定大於配置的契約式編程支持;
√能簡單的進行Web層的單元測試;
√支持靈活的URL到頁面控制器的映射;
√非常容易與其他視圖技術集成,如Velocity、FreeMarker等等,因爲模型數據不放在特定的API裏,而是放在一個Model裏(Map
數據結構實現,因此很容易被其他框架使用);
√非常靈活的數據驗證、格式化和數據綁定機制,能使用任何對象進行數據綁定,不必實現特定框架的API;
√提供一套強大的JSP標籤庫,簡化JSP開發;
√支持靈活的本地化、主題等解析;
√更加簡單的異常處理;
√對靜態資源的支持;
√支持Restful風格。
2.3、Spring Web MVC架構
Spring Web MVC框架也是一個基於請求驅動的Web框架,並且也使用了前端控制器模式來進行設計,再根據請求映射規則分發給相應的頁面控制器(動作/處理器)進行處理。首先讓我們整體看一下Spring Web MVC處理請求的流程:
2.3.1、Spring Web MVC處理請求的流程
如圖2-1
圖2-1
具體執行步驟如下:
1、 首先用戶發送請求————>前端控制器,前端控制器根據請求信息(如URL)來決定選擇哪一個頁面控制器進行處理並把請求委託給它,即以前的控制器的控制邏輯部分;圖2-1中的1、2步驟;
2、 頁面控制器接收到請求後,進行功能處理,首先需要收集和綁定請求參數到一個對象,這個對象在Spring Web MVC中叫命令對象,並進行驗證,然後將命令對象委託給業務對象進行處理;處理完畢後返回一個ModelAndView(模型數據和邏輯視圖名);圖2-1中的3、4、5步驟;
3、 前端控制器收回控制權,然後根據返回的邏輯視圖名,選擇相應的視圖進行渲染,並把模型數據傳入以便視圖渲染;圖2-1中的步驟6、7;
4、 前端控制器再次收回控制權,將響應返回給用戶,圖2-1中的步驟8;至此整個結束。
問題:
1、 請求如何給前端控制器?
2、 前端控制器如何根據請求信息選擇頁面控制器進行功能處理?
3、 如何支持多種頁面控制器呢?
4、 如何頁面控制器如何使用業務對象?
5、 頁面控制器如何返回模型數據?
6、 前端控制器如何根據頁面控制器返回的邏輯視圖名選擇具體的視圖進行渲染?
7、 不同的視圖技術如何使用相應的模型數據?
首先我們知道有如上問題,那這些問題如何解決呢?請讓我們先繼續,在後邊依次回答。
2.3.2、Spring Web MVC架構
1、Spring Web MVC核心架構圖,如圖2-2
圖2-2
架構圖對應的DispatcherServlet核心代碼如下:
java代碼:
-
-
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
-
HttpServletRequest processedRequest = request;
-
HandlerExecutionChain mappedHandler = null;
-
int interceptorIndex = -1;
-
-
try {
-
ModelAndView mv;
-
boolean errorView = false;
-
-
try {
-
-
processedRequest = checkMultipart(request);
-
-
mappedHandler = getHandler(processedRequest, false);
-
if (mappedHandler == null || mappedHandler.getHandler() == null) {
-
noHandlerFound(processedRequest, response);
-
return;
-
}
-
-
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
-
-
-
-
-
-
-
-
-
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
-
-
-
if (mv != null && !mv.hasView()) {
-
mv.setViewName(getDefaultViewName(request));
-
}
-
-
-
-
}
-
catch (ModelAndViewDefiningException ex) {
-
logger.debug("ModelAndViewDefiningException encountered", ex);
-
mv = ex.getModelAndView();
-
}
-
catch (Exception ex) {
-
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
-
mv = processHandlerException(processedRequest, response, handler, ex);
-
errorView = (mv != null);
-
}
-
-
-
-
-
if (mv != null && !mv.wasCleared()) {
-
render(mv, processedRequest, response);
-
if (errorView) {
-
WebUtils.clearErrorRequestAttributes(request);
-
}
-
}
-
else {
-
if (logger.isDebugEnabled()) {
-
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
-
"': assuming HandlerAdapter completed request handling");
-
}
-
}
-
-
-
-
-
-
catch (Exception ex) {
-
-
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex);
-
throw ex;
-
}
-
catch (Error err) {
-
ServletException ex = new NestedServletException("Handler processing failed", err);
-
-
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex);
-
throw ex;
-
}
-
-
finally {
-
-
if (processedRequest != request) {
-
cleanupMultipart(processedRequest);
-
}
-
}
-
}
核心架構的具體流程步驟如下:
1、 首先用戶發送請求——>DispatcherServlet,前端控制器收到請求後自己不進行處理,而是委託給其他的解析器進行處理,作爲統一訪問點,進行全局的流程控制;
2、 DispatcherServlet——>HandlerMapping, HandlerMapping將會把請求映射爲HandlerExecutionChain對象(包含一個Handler處理器(頁面控制器)對象、多個HandlerInterceptor攔截器)對象,通過這種策略模式,很容易添加新的映射策略;
3、 DispatcherServlet——>HandlerAdapter,HandlerAdapter將會把處理器包裝爲適配器,從而支持多種類型的處理器,即適配器設計模式的應用,從而很容易支持很多類型的處理器;
4、 HandlerAdapter——>處理器功能處理方法的調用,HandlerAdapter將會根據適配的結果調用真正的處理器的功能處理方法,完成功能處理;並返回一個ModelAndView對象(包含模型數據、邏輯視圖名);
5、 ModelAndView的邏輯視圖名——> ViewResolver, ViewResolver將把邏輯視圖名解析爲具體的View,通過這種策略模式,很容易更換其他視圖技術;
6、 View——>渲染,View會根據傳進來的Model模型數據進行渲染,此處的Model實際是一個Map數據結構,因此很容易支持其他視圖技術;
7、返回控制權給DispatcherServlet,由DispatcherServlet返回響應給用戶,到此一個流程結束。
此處我們只是講了核心流程,沒有考慮攔截器、本地解析、文件上傳解析等,後邊再細述。
到此,再來看我們前邊提出的問題:
1、 請求如何給前端控制器?這個應該在web.xml中進行部署描述,在HelloWorld中詳細講解。
2、 前端控制器如何根據請求信息選擇頁面控制器進行功能處理? 我們需要配置HandlerMapping進行映射
3、 如何支持多種頁面控制器呢?配置HandlerAdapter從而支持多種類型的頁面控制器
4、 如何頁面控制器如何使用業務對象?可以預料到,肯定利用Spring IoC容器的依賴注入功能
5、 頁面控制器如何返回模型數據?使用ModelAndView返回
6、 前端控制器如何根據頁面控制器返回的邏輯視圖名選擇具體的視圖進行渲染? 使用ViewResolver進行解析
7、 不同的視圖技術如何使用相應的模型數據? 因爲Model是一個Map數據結構,很容易支持其他視圖技術
在此我們可以看出具體的核心開發步驟:
1、 DispatcherServlet在web.xml中的部署描述,從而攔截請求到Spring Web MVC
2、 HandlerMapping的配置,從而將請求映射到處理器
3、 HandlerAdapter的配置,從而支持多種類型的處理器
4、 ViewResolver的配置,從而將邏輯視圖名解析爲具體視圖技術
5、處理器(頁面控制器)的配置,從而進行功能處理
上邊的開發步驟我們會在Hello World中詳細驗證。
2.4、Spring Web MVC優勢
1、清晰的角色劃分:前端控制器(DispatcherServlet
)、請求到處理器映射(HandlerMapping)、處理器適配器(HandlerAdapter)、視圖解析器(ViewResolver)、處理器或頁面控制器(Controller)、驗證器( Validator)、命令對象(Command 請求參數綁定到的對象就叫命令對象)、表單對象(Form Object 提供給表單展示和提交到的對象就叫表單對象)。
2、分工明確,而且擴展點相當靈活,可以很容易擴展,雖然幾乎不需要;
3、由於命令對象就是一個POJO,無需繼承框架特定API,可以使用命令對象直接作爲業務對象;
4、和Spring 其他框架無縫集成,是其它Web框架所不具備的;
5、可適配,通過HandlerAdapter可以支持任意的類作爲處理器;
6、可定製性,HandlerMapping、ViewResolver等能夠非常簡單的定製;
7、功能強大的數據驗證、格式化、綁定機制;
8、利用Spring提供的Mock對象能夠非常簡單的進行Web層單元測試;
9、本地化、主題的解析的支持,使我們更容易進行國際化和主題的切換。
10、強大的JSP標籤庫,使JSP編寫更容易。
………………還有比如RESTful風格的支持、簡單的文件上傳、約定大於配置的契約式編程支持、基於註解的零配置支持等等。
到此我們已經簡單的瞭解了Spring Web MVC,接下來讓我們來個實例來具體使用下這個框架。
2.5、Hello World入門
2.5.1、準備開發環境和運行環境:
☆開發工具:eclipse
☆運行環境:tomcat6.0.20
☆工程:動態web工程(springmvc-chapter2)
☆spring框架下載:
spring-framework-3.1.1.RELEASE-with-docs.zip
☆依賴jar包:
1、 Spring框架jar包:
爲了簡單,將spring-framework-3.1.1.RELEASE-with-docs.zip/dist/下的所有jar包拷貝到項目的WEB-INF/lib目錄下;
2、 Spring框架依賴的jar包:
需要添加Apache commons logging日誌,此處使用的是commons.logging-1.1.1.jar;
需要添加jstl標籤庫支持,此處使用的是jstl-1.1.2.jar和standard-1.1.2.jar;
2.5.2、前端控制器的配置
在我們的web.xml中添加如下配置:
java代碼:
-
<servlet>
-
<servlet-name>chapter2</servlet-name>
-
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
-
<load-on-startup>1</load-on-startup>
-
</servlet>
-
<servlet-mapping>
-
<servlet-name>chapter2</servlet-name>
-
<url-pattern>/</url-pattern>
-
</servlet-mapping>
load-on-startup:表示啓動容器時初始化該Servlet;
url-pattern:表示哪些請求交給Spring Web MVC處理, “/” 是用來定義默認servlet映射的。也可以如“*.html”表示攔截所有以html爲擴展名的請求。
自此請求已交給Spring Web MVC框架處理,因此我們需要配置Spring的配置文件,默認DispatcherServlet會加載WEB-INF/[DispatcherServlet的Servlet名字]-servlet.xml配置文件。本示例爲WEB-INF/ chapter2-servlet.xml。
2.5.3、在Spring配置文件中配置HandlerMapping、HandlerAdapter
具體配置在WEB-INF/ chapter2-servlet.xml文件中:
java代碼:
-
<!-- HandlerMapping -->
-
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
-
-
<!-- HandlerAdapter -->
-
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
BeanNameUrlHandlerMapping:表示將請求的URL和Bean名字映射,如URL爲 “上下文/hello”,則Spring配置文件必須有一個名字爲“/hello”的Bean,上下文默認忽略。
SimpleControllerHandlerAdapter:表示所有實現了org.springframework.web.servlet.mvc.Controller接口的Bean可以作爲Spring Web MVC中的處理器。如果需要其他類型的處理器可以通過實現HadlerAdapter來解決。
2.5.4、在Spring配置文件中配置ViewResolver
具體配置在WEB-INF/ chapter2-servlet.xml文件中:
java代碼:
-
<!-- ViewResolver -->
-
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
-
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
-
<property name="prefix" value="/WEB-INF/jsp/"/>
-
<property name="suffix" value=".jsp"/>
-
</bean>
InternalResourceViewResolver:用於支持Servlet、JSP視圖解析;
viewClass:JstlView表示JSP模板頁面需要使用JSTL標籤庫,classpath中必須包含jstl的相關jar包;
prefix和suffix:查找視圖頁面的前綴和後綴(前綴[邏輯視圖名]後綴),比如傳進來的邏輯視圖名爲hello,則該該jsp視圖頁面應該存放在“WEB-INF/jsp/hello.jsp”;
2.5.5、開發處理器/頁面控制器
java代碼:
-
package cn.javass.chapter2.web.controller;
-
import javax.servlet.http.HttpServletRequest;
-
import javax.servlet.http.HttpServletResponse;
-
import org.springframework.web.servlet.ModelAndView;
-
import org.springframework.web.servlet.mvc.Controller;
-
public class HelloWorldController implements Controller {
-
@Override
-
public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception {
-
-
-
-
-
ModelAndView mv = new ModelAndView();
-
-
mv.addObject("message", "Hello World!");
-
-
mv.setViewName("hello");
-
return mv;
-
}
-
}
org.springframework.web.servlet.mvc.Controller:頁面控制器/處理器必須實現Controller接口,注意別選錯了;後邊我們會學習其他的處理器實現方式;
public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) :功能處理方法,實現相應的功能處理,比如收集參數、驗證參數、綁定參數到命令對象、將命令對象傳入業務對象進行業務處理、最後返回ModelAndView對象;
ModelAndView:包含了視圖要實現的模型數據和邏輯視圖名;“mv.addObject("message", "Hello World!");
”表示添加模型數據,此處可以是任意POJO對象;“mv.setViewName("hello");”表示設置邏輯視圖名爲“hello”,視圖解析器會將其解析爲具體的視圖,如前邊的視圖解析器InternalResourceVi。wResolver會將其解析爲“WEB-INF/jsp/hello.jsp”。
我們需要將其添加到Spring配置文件(WEB-INF/chapter2-servlet.xml),讓其接受Spring IoC容器管理:
java代碼:
-
<!-- 處理器 -->
-
<bean name="/hello" class="cn.javass.chapter2.web.controller.HelloWorldController"/>
name="/hello":前邊配置的BeanNameUrlHandlerMapping,表示如過請求的URL爲 “上下文/hello”,則將會交給該Bean進行處理。
2.5.6、開發視圖頁面
創建 /WEB-INF/jsp/hello.jsp視圖頁面:
java代碼:
-
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
-
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
-
<html>
-
<head>
-
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-
<title>Hello World</title>
-
</head>
-
<body>
-
${message}
-
</body>
-
</html>
${message}:表示顯示由HelloWorldController處理器傳過來的模型數據。
2.5.6、啓動服務器運行測試
通過請求:http://localhost:9080/springmvc-chapter2/hello,如果頁面輸出“Hello World!
”就表明我們成功了!
2.5.7、運行流程分析
如圖2-3
圖2-3
運行步驟:
1、 首先用戶發送請求http://localhost:9080/springmvc-chapter2/hello——>web容器,web容器根據“/hello”路徑映射到DispatcherServlet(url-pattern爲/)進行處理;
2、 DispatcherServlet——>BeanNameUrlHandlerMapping進行請求到處理的映射,BeanNameUrlHandlerMapping將“/hello”路徑直接映射到名字爲“/hello”的Bean進行處理,即HelloWorldController,BeanNameUrlHandlerMapping將其包裝爲HandlerExecutionChain(只包括HelloWorldController處理器,沒有攔截器);
3、 DispatcherServlet——> SimpleControllerHandlerAdapter,SimpleControllerHandlerAdapter將HandlerExecutionChain中的處理器(HelloWorldController)適配爲SimpleControllerHandlerAdapter;
4、 SimpleControllerHandlerAdapter——> HelloWorldController處理器功能處理方法的調用,SimpleControllerHandlerAdapter將會調用處理器的handleRequest方法進行功能處理,該處理方法返回一個ModelAndView給DispatcherServlet;
5、 hello(ModelAndView的邏輯視圖名)——>InternalResourceViewResolver, InternalResourceViewResolver使用JstlView,具體視圖頁面在/WEB-INF/jsp/hello.jsp;
6、 JstlView(/WEB-INF/jsp/hello.jsp)——>渲染,將在處理器傳入的模型數據(message=HelloWorld!)在視圖中展示出來;
7、 返回控制權給DispatcherServlet,由DispatcherServlet返回響應給用戶,到此一個流程結束。
到此HelloWorld就完成了,步驟是不是有點多?而且回憶下我們主要進行了如下配置:
1、 前端控制器DispatcherServlet;
2、 HandlerMapping
3、 HandlerAdapter
4、 ViewResolver
5、 處理器/頁面控制器
6、 視圖
因此,接下來幾章讓我們詳細看看這些配置,先從DispatcherServlet開始吧。
2.6、POST中文亂碼解決方案
spring Web MVC框架提供了org.springframework.web.filter.CharacterEncodingFilter用於解決POST方式造成的中文亂碼問題,具體配置如下:
java代碼:
-
<filter>
-
<filter-name>CharacterEncodingFilter</filter-name>
-
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
-
<init-param>
-
<param-name>encoding</param-name>
-
<param-value>utf-8</param-value>
-
</init-param>
-
</filter>
-
<filter-mapping>
-
<filter-name>CharacterEncodingFilter</filter-name>
-
<url-pattern>/*</url-pattern>
-
</filter-mapping>
以後我們項目及所有頁面的編碼均爲UTF-8。
2.7、Spring3.1新特性
一、Spring2.5之前,我們都是通過實現Controller接口或其實現來定義我們的處理器類。
二、Spring2.5引入註解式處理器支持,通過@Controller 和 @RequestMapping註解定義我們的處理器類。並且提供了一組強大的註解:
需要通過處理器映射DefaultAnnotationHandlerMapping和處理器適配器AnnotationMethodHandlerAdapter來開啓支持@Controller 和 @RequestMapping註解的處理器。
@Controller:
用於標識是處理器類;
@RequestMapping:
請求到處理器功能方法的映射規則;
@RequestParam:
請求參數到處理器功能處理方法的方法參數上的綁定;
@ModelAttribute:
請求參數到命令對象的綁定;
@SessionAttributes:
用於聲明session級別存儲的屬性,放置在處理器類上,通常列出模型屬性(如@ModelAttribute)對應的名稱,則這些屬性會透明的保存到session中;
@InitBinder:
自定義數據綁定註冊支持,用於將請求參數轉換到命令對象屬性的對應類型;
三、Spring3.0引入RESTful架構風格支持(通過@PathVariable註解和一些其他特性支持),且又引入了更多的註解支持:
@CookieValue:
cookie數據到處理器功能處理方法的方法參數上的綁定;
@RequestHeader:
請求頭(header)數據到處理器功能處理方法的方法參數上的綁定;
@RequestBody:
請求的body體的綁定(通過HttpMessageConverter進行類型轉換);
@ResponseBody:
處理器功能處理方法的返回值作爲響應體(通過HttpMessageConverter進行類型轉換);
@ResponseStatus:
定義處理器功能處理方法/異常處理器返回的狀態碼和原因;
@ExceptionHandler:
註解式聲明異常處理器;
@PathVariable:
請求URI中的模板變量部分到處理器功能處理方法的方法參數上的綁定,從而支持RESTful架構風格的URI;
四、還有比如:
JSR-303驗證框架的無縫支持(通過@Valid註解定義驗證元數據);
使用Spring 3開始的ConversionService進行類型轉換(
PropertyEditor依然有效),支持
使用@NumberFormat 和 @DateTimeFormat來進行數字和日期的格式化;
HttpMessageConverter
(Http輸入/輸出轉換器,比如JSON、XML等的數據輸出轉換器);
ContentNegotiatingViewResolver
,內容協商視圖解析器,它還是視圖解析器,只是它支持根據請求信息將同一模型數據以不同的視圖方式展示(如json、xml、html等),RESTful架構風格中很重要的概念(同一資源,多種表現形式);
Spring 3 引入 一個 mvc XML的命名空間用於支持mvc配置,包括如:
<mvc:annotation-driven>:
自動註冊基於註解風格的處理器需要的DefaultAnnotationHandlerMapping、AnnotationMethodHandlerAdapter
支持Spring3的ConversionService自動註冊
支持JSR-303驗證框架的自動探測並註冊(只需把JSR-303實現放置到classpath)
自動註冊相應的HttpMessageConverter(用於支持@RequestBody 和 @ResponseBody)(如XML輸入輸出轉換器(只需將JAXP實現放置到classpath)、JSON輸入輸出轉換器(只需將Jackson實現放置到classpath))等。
<mvc:interceptors>:註冊自定義的處理器攔截器;
<mvc:view-controller>:和ParameterizableViewController類似,收到相應請求後直接選擇相應的視圖;
<mvc:resources>:邏輯靜態資源路徑到物理靜態資源路徑的支持;
<mvc:default-servlet-handler>:當在web.xml 中DispatcherServlet使用<url-pattern>/</url-pattern> 映射時,能映射靜態資源(當Spring Web MVC框架沒有處理請求對應的控制器時(如一些靜態資源),轉交給默認的Servlet來響應靜態文件,否則報404找不到資源錯誤,)。
……等等。
五、Spring3.1新特性:
對Servlet 3.0的全面支持。
@EnableWebMvc:
用於在基於Java類定義Bean配置中開啓MVC支持,和XML中的<mvc:annotation-driven>功能一樣;
新的@Contoller和@RequestMapping註解支持類:處理器映射RequestMappingHandlerMapping 和 處理器適配器RequestMappingHandlerAdapter組合來代替Spring2.5開始的處理器映射DefaultAnnotationHandlerMapping和處理器適配器AnnotationMethodHandlerAdapter,提供更多的擴展點,它們之間的區別我們在處理器映射一章介紹。
新的@ExceptionHandler 註解支持類:ExceptionHandlerExceptionResolver來代替Spring3.0的AnnotationMethodHandlerExceptionResolver,在異常處理器一章我們再詳細講解它們的區別。
@RequestMapping的"consumes" 和 "produces" 條件支持:
用於支持@RequestBody 和 @ResponseBody,
1
consumes
指定請求的內容是什麼類型的內容,即本處理方法消費什麼類型的數據,如consumes="application/json"表示JSON類型的內容,Spring會根據相應的HttpMessageConverter進行請求內容區數據到@RequestBody註解的命令對象的轉換;
2
produces
指定生產什麼類型的內容,如produces="application/json"表示JSON類型的內容,Spring的根據相應的HttpMessageConverter進行請求內容區數據到@RequestBody註解的命令對象的轉換,Spring會根據相應的HttpMessageConverter進行模型數據(返回值)到JSON響應內容的轉換
3
以上內容,本章第×××節詳述。
URI模板變量增強:
URI模板變量可以直接綁定到@ModelAttribute指定的命令對象、@PathVariable方法參數在視圖渲染之前被合併到模型數據中(除JSON序列化、XML混搭場景下)。
@Validated:
JSR-303的javax.validation.Valid一種變體(非JSR-303規範定義的,而是Spring自定義的),用於提供對Spring的驗證器(org.springframework.validation.Validator)支持,需要Hibernate
Validator 4.2及更高版本支持;
@RequestPart:
提供對“multipart/form-data”請求的全面支持,支持Servlet 3.0文件上傳(javax.servlet.http.Part)、支持內容的HttpMessageConverter(即根據請求頭的Content-Type,來判斷內容區數據是什麼類型,如JSON、XML,能自動轉換爲命令對象),比@RequestParam更強大(只能對請求參數數據綁定,key-alue格式),而@RequestPart支持如JSON、XML內容區數據的綁定;詳見本章的第×××節;
Flash 屬性 和 RedirectAttribute:
通過FlashMap存儲一個請求的輸出,當進入另一個請求時作爲該請求的輸入,典型場景如重定向(POST-REDIRECT-GET模式,1、POST時將下一次需要的數據放在FlashMap;2、重定向;3、通過GET訪問重定向的地址,此時FlashMap會把1放到FlashMap的數據取出放到請求中,並從FlashMap中刪除;從而支持在兩次請求之間保存數據並防止了重複表單提交)。
Spring Web MVC提供FlashMapManager用於管理FlashMap,默認使用
SessionFlashMapManager,即數據默認存儲在session中。
第三章
DispatcherServlet詳解
3.1、DispatcherServlet作用
DispatcherServlet是前端控制器設計模式的實現,提供Spring Web MVC的集中訪問點,而且負責職責的分派,而且與Spring IoC容器無縫集成,從而可以獲得Spring的所有好處。 具體請參考第二章的圖2-1。
DispatcherServlet主要用作職責調度工作,本身主要用於控制流程,主要職責如下:
1、文件上傳解析,如果請求類型是multipart將通過MultipartResolver進行文件上傳解析;
2、通過HandlerMapping,將請求映射到處理器(返回一個HandlerExecutionChain,它包括一個處理器、多個HandlerInterceptor攔截器);
3、通過HandlerAdapter支持多種類型的處理器(HandlerExecutionChain中的處理器);
4、通過ViewResolver解析邏輯視圖名到具體視圖實現;
5、本地化解析;
6、渲染具體的視圖等;
7、如果執行過程中遇到異常將交給HandlerExceptionResolver來解析。
從以上我們可以看出DispatcherServlet主要負責流程的控制(而且在流程中的每個關鍵點都是很容易擴展的)。
3.2、DispatcherServlet在web.xml中的配置
- <servlet>
- <servlet-name>chapter2</servlet-name>
- <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
- <load-on-startup>1</load-on-startup>
- </servlet>
- <servlet-mapping>
- <servlet-name>chapter2</servlet-name>
- <url-pattern>/</url-pattern>
- </servlet-mapping>
load-on-startup:表示啓動容器時初始化該Servlet;
url-pattern:表示哪些請求交給Spring Web MVC處理, “/” 是用來定義默認servlet映射的。也可以如“*.html”表示攔截所有以html爲擴展名的請求。
該DispatcherServlet默認使用WebApplicationContext作爲上下文,Spring默認配置文件爲“/WEB-INF/[servlet名字]-servlet.xml”。
DispatcherServlet也可以配置自己的初始化參數,覆蓋默認配置:
摘自Spring Reference
參數
|
描述
|
contextClass
|
實現WebApplicationContext接口的類,當前的servlet用它來創建上下文。如果這個參數沒有指定, 默認使用XmlWebApplicationContext。
|
contextConfigLocation
|
傳給上下文實例(由contextClass指定)的字符串,用來指定上下文的位置。這個字符串可以被分成多個字符串(使用逗號作爲分隔符) 來支持多個上下文(在多上下文的情況下,如果同一個bean被定義兩次,後面一個優先)。
|
namespace
|
WebApplicationContext命名空間。默認值是[server-name]-servlet。
|
因此我們可以通過添加初始化參數
- <servlet>
- <servlet-name>chapter2</servlet-name>
- <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
- <load-on-startup>1</load-on-startup>
- <init-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>classpath:spring-servlet-config.xml</param-value>
- </init-param>
- </servlet>
如果使用如上配置,Spring Web MVC框架將加載“classpath:spring-servlet-config.xml”來進行初始化上下文而不是“/WEB-INF/[servlet名字]-servlet.xml”。
3.3、上下文關係
集成Web環境的通用配置:
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>
- classpath:spring-common-config.xml,
- classpath:spring-budget-config.xml
- </param-value>
- </context-param>
- <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
- </listener>
如上配置是Spring集成Web環境的通用配置;一般用於加載除Web層的Bean(如DAO、Service等),以便於與其他任何Web框架集成。
contextConfigLocation:表示用於加載Bean的配置文件;
contextClass:表示用於加載Bean的ApplicationContext實現類,默認WebApplicationContext。
創建完畢後會將該上下文放在ServletContext:
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
this.context);
ContextLoaderListener初始化的上下文和DispatcherServlet初始化的上下文關係,如圖3-1
圖3-1
從圖中可以看出:
ContextLoaderListener初始化的上下文加載的Bean是對於整個應用程序共享的,不管是使用什麼表現層技術,一般如DAO層、Service層Bean;
DispatcherServlet初始化的上下文加載的Bean是隻對Spring Web MVC有效的Bean,如Controller、HandlerMapping、HandlerAdapter等等,該初始化上下文應該只加載Web相關組件。
3.4、DispatcherServlet初始化順序
繼承體系結構如下所示:
1、HttpServletBean繼承HttpServlet,因此在Web容器啓動時將調用它的init方法,該初始化方法的主要作用
:::將Servlet初始化參數(init-param)設置到該組件上(如contextAttribute、contextClass、namespace、contextConfigLocation),通過BeanWrapper簡化設值過程,方便後續使用;
:::提供給子類初始化擴展點,initServletBean(),該方法由FrameworkServlet覆蓋。
- public abstract class HttpServletBean extends HttpServlet implements EnvironmentAware{
- @Override
- public final void init() throws ServletException {
-
-
-
- try {
- PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
- BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
- ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
- bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));
- initBeanWrapper(bw);
- bw.setPropertyValues(pvs, true);
- }
- catch (BeansException ex) {
-
- }
-
- initServletBean();
- if (logger.isDebugEnabled()) {
- logger.debug("Servlet '" + getServletName() + "' configured successfully");
- }
- }
-
- }
2、FrameworkServlet繼承HttpServletBean,通過initServletBean()進行Web上下文初始化,該方法主要覆蓋一下兩件事情:
初始化web上下文;
提供給子類初始化擴展點;
- public abstract class FrameworkServlet extends HttpServletBean {
- @Override
- protected final void initServletBean() throws ServletException {
-
- try {
-
- this.webApplicationContext = initWebApplicationContext();
-
- initFrameworkServlet();
- }
-
- }
- }
- protected WebApplicationContext initWebApplicationContext() {
-
- WebApplicationContext rootContext =
- WebApplicationContextUtils.getWebApplicationContext(getServletContext());
- WebApplicationContext wac = null;
- if (this.webApplicationContext != null) {
-
- wac = this.webApplicationContext;
- if (wac instanceof ConfigurableWebApplicationContext) {
- ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
- if (!cwac.isActive()) {
- if (cwac.getParent() == null) {
- cwac.setParent(rootContext);
- }
- configureAndRefreshWebApplicationContext(cwac);
- }
- }
- }
- if (wac == null) {
-
- wac = findWebApplicationContext();
- }
- if (wac == null) {
-
- wac = createWebApplicationContext(rootContext);
- }
- if (!this.refreshEventReceived) {
-
- onRefresh(wac);
- }
- if (this.publishContext) {
-
- String attrName = getServletContextAttributeName();
- getServletContext().setAttribute(attrName, wac);
-
- }
- return wac;
- }
從initWebApplicationContext()方法可以看出,基本上如果ContextLoaderListener加載了上下文將作爲根上下文(DispatcherServlet的父容器)。
最後調用了onRefresh()方法執行容器的一些初始化,這個方法由子類實現,來進行擴展。
3、DispatcherServlet繼承FrameworkServlet,並實現了onRefresh()方法提供一些前端控制器相關的配置:
- public class DispatcherServlet extends FrameworkServlet {
-
- @Override
- protected void onRefresh(ApplicationContext context) {
- initStrategies(context);
- }
-
-
- protected void initStrategies(ApplicationContext context) {
- initMultipartResolver(context);
- initLocaleResolver(context);
- initThemeResolver(context);
- initHandlerMappings(context);
- initHandlerAdapters(context);
- initHandlerExceptionResolvers(context);
- initRequestToViewNameTranslator(context);
- initViewResolvers(context);
- initFlashMapManager(context);
- }
- }
從如上代碼可以看出,DispatcherServlet啓動時會進行我們需要的Web層Bean的配置,如HandlerMapping、HandlerAdapter等,而且如果我們沒有配置,還會給我們提供默認的配置。
從如上代碼我們可以看出,整個DispatcherServlet初始化的過程和做了些什麼事情,具體主要做了如下兩件事情:
1、初始化Spring Web MVC使用的Web上下文,並且可能指定父容器爲(ContextLoaderListener加載了根上下文);
2、初始化DispatcherServlet使用的策略,如HandlerMapping、HandlerAdapter等。
服務器啓動時的日誌分析(此處加上了ContextLoaderListener從而啓動ROOT上下文容器):
信息: Initializing Spring root WebApplicationContext //由ContextLoaderListener啓動ROOT上下文
2012-03-12 13:33:55 [main] INFO org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization started
2012-03-12 13:33:55 [main] INFO org.springframework.web.context.support.XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Mon Mar 12 13:33:55 CST 2012]; root of context hierarchy
2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader - Loading bean definitions
2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 0 bean definitions from location pattern [/WEB-INF/ContextLoaderListener.xml]
2012-03-12 13:33:55 [main] DEBUG org.springframework.web.context.support.XmlWebApplicationContext - Bean factory for Root WebApplicationContext: org.springframework.beans.factory.support.DefaultListableBeanFactory@1c05ffd: defining beans []; root of factory
hierarchy
2012-03-12 13:33:55 [main] DEBUG org.springframework.web.context.support.XmlWebApplicationContext - Bean factory for Root WebApplicationContext:
2012-03-12 13:33:55 [main] DEBUG org.springframework.web.context.ContextLoader - Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT] //將ROOT上下文綁定到ServletContext
2012-03-12 13:33:55 [main] INFO org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 438 ms //到此ROOT上下文啓動完畢
2012-03-12 13:33:55 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Initializing servlet 'chapter2'
信息: Initializing Spring FrameworkServlet 'chapter2' //開始初始化FrameworkServlet對應的Web上下文
2012-03-12 13:33:55 [main] INFO org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'chapter2': initialization started
2012-03-12 13:33:55 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Servlet with name 'chapter2' will try to create custom WebApplicationContext context of class 'org.springframework.web.context.support.XmlWebApplicationContext', using
parent context [Root WebApplicationContext: startup date [Mon Mar 12 13:33:55 CST 2012]; root of context hierarchy]
//此處使用Root WebApplicationContext作爲父容器。
2012-03-12 13:33:55 [main] INFO org.springframework.web.context.support.XmlWebApplicationContext - Refreshing WebApplicationContext for namespace 'chapter2-servlet': startup date [Mon Mar 12 13:33:55 CST 2012]; parent: Root WebApplicationContext
2012-03-12 13:33:55 [main] INFO org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from ServletContext resource [/WEB-INF/chapter2-servlet.xml]
2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader - Loading bean definitions
2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.BeanDefinitionParserDelegate - Neither XML 'id' nor 'name' specified - using generated bean name[org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping#0]
//我們配置的HandlerMapping
2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.BeanDefinitionParserDelegate - Neither XML 'id' nor 'name' specified - using generated bean name[org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter#0] //我們配置的HandlerAdapter
2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.BeanDefinitionParserDelegate - Neither XML 'id' nor 'name' specified - using generated bean name [org.springframework.web.servlet.view.InternalResourceViewResolver#0] //我們配置的ViewResolver
2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.BeanDefinitionParserDelegate - No XML 'id' specified - using '/hello' as bean name and [] as aliases
//我們的處理器(HelloWorldController)
2012-03-12 13:33:55 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 4 bean definitions from location pattern [/WEB-INF/chapter2-servlet.xml]
2012-03-12 13:33:55 [main] DEBUG org.springframework.web.context.support.XmlWebApplicationContext - Bean factory for WebApplicationContext for namespace 'chapter2-servlet': org.springframework.beans.factory.support.DefaultListableBeanFactory@1372656: defining
beans [org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping#0,org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter#0,org.springframework.web.servlet.view.InternalResourceViewResolver#0,/hello]; parent: org.springframework.beans.factory.support.DefaultListableBeanFactory@1c05ffd
//到此容器註冊的Bean初始化完畢
2012-03-12 13:33:56 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Unable to locate MultipartResolver with name 'multipartResolver': no multipart request handling provided
2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean 'org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver'
//默認的LocaleResolver註冊
2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean 'org.springframework.web.servlet.theme.FixedThemeResolver'
//默認的ThemeResolver註冊
2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping#0'
//發現我們定義的HandlerMapping 不再使用默認的HandlerMapping。
2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter#0'
//發現我們定義的HandlerAdapter 不再使用默認的HandlerAdapter。
2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean 'org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver'
//異常處理解析器ExceptionResolver
2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean 'org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver'
2012-03-12 13:33:56 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.web.servlet.view.InternalResourceViewResolver#0'
2012-03-12 13:33:56 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Published WebApplicationContext of servlet 'chapter2' as ServletContext attribute with name [org.springframework.web.servlet.FrameworkServlet.CONTEXT.chapter2]
//綁定FrameworkServlet初始化的Web上下文到ServletContext
2012-03-12 13:33:56 [main] INFO org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'chapter2': initialization completed in 297 ms
2012-03-12 13:33:56 [main] DEBUG org.springframework.web.servlet.DispatcherServlet - Servlet 'chapter2' configured successfully
//到此完整流程結束
從如上日誌我們也可以看出,DispatcherServlet會進行一些默認的配置。接下來我們看一下默認配置吧。
3.5、DispatcherServlet默認配置
DispatcherServlet的默認配置在DispatcherServlet.properties(和DispatcherServlet類在一個包下)中,而且是當Spring配置文件中沒有指定配置時使用的默認策略:
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
從如上配置可以看出DispatcherServlet在啓動時會自動註冊這些特殊的Bean,無需我們註冊,如果我們註冊了,默認的將不會註冊。
因此如第二章的BeanNameUrlHandlerMapping、SimpleControllerHandlerAdapter是不需要註冊的,DispatcherServlet默認會註冊這兩個Bean。
從DispatcherServlet.properties可以看出有許多特殊的Bean,那接下來我們就看看Spring Web MVC主要有哪些特殊的Bean。
3.6、DispatcherServlet中使用的特殊的Bean
DispatcherServlet默認使用WebApplicationContext作爲上下文,因此我們來看一下該上下文中有哪些特殊的Bean:
1、Controller:處理器/頁面控制器,做的是MVC中的C的事情,但控制邏輯轉移到前端控制器了,用於對請求進行處理;
2、HandlerMapping:請求到處理器的映射,如果映射成功返回一個HandlerExecutionChain對象(包含一個Handler處理器(頁面控制器)對象、多個HandlerInterceptor攔截器)對象;如BeanNameUrlHandlerMapping將URL與Bean名字映射,映射成功的Bean就是此處的處理器;
3、HandlerAdapter:HandlerAdapter將會把處理器包裝爲適配器,從而支持多種類型的處理器,即適配器設計模式的應用,從而很容易支持很多類型的處理器;如SimpleControllerHandlerAdapter將對實現了Controller接口的Bean進行適配,並且掉處理器的handleRequest方法進行功能處理;
4、ViewResolver:ViewResolver將把邏輯視圖名解析爲具體的View,通過這種策略模式,很容易更換其他視圖技術;如InternalResourceViewResolver將邏輯視圖名映射爲jsp視圖;
5、LocalResover:本地化解析,因爲Spring支持國際化,因此LocalResover解析客戶端的Locale信息從而方便進行國際化;
6、ThemeResovler:主題解析,通過它來實現一個頁面多套風格,即常見的類似於軟件皮膚效果;
7、MultipartResolver:文件上傳解析,用於支持文件上傳;
8、HandlerExceptionResolver:處理器異常解析,可以將異常映射到相應的統一錯誤界面,從而顯示用戶友好的界面(而不是給用戶看到具體的錯誤信息);
9、RequestToViewNameTranslator:當處理器沒有返回邏輯視圖名等相關信息時,自動將請求URL映射爲邏輯視圖名;
10、FlashMapManager:用於管理FlashMap的策略接口,FlashMap用於存儲一個請求的輸出,當進入另一個請求時作爲該請求的輸入,通常用於重定向場景,後邊會細述。
到此DispatcherServlet我們已經瞭解了,接下來我們就需要把上邊提到的特殊Bean挨個擊破,那首先從控制器開始吧。
第四章
Controller接口控制器詳解(1)
4.1、Controller簡介
Controller控制器,是MVC中的部分C,爲什麼是部分呢?因爲此處的控制器主要負責功能處理部分:
1、收集、驗證請求參數並綁定到命令對象;
2、將命令對象交給業務對象,由業務對象處理並返回模型數據;
3、返回ModelAndView(Model部分是業務對象返回的模型數據,視圖部分爲邏輯視圖名)。
還記得DispatcherServlet嗎?主要負責整體的控制流程的調度部分:
1、負責將請求委託給控制器進行處理;
2、根據控制器返回的邏輯視圖名選擇具體的視圖進行渲染(並把模型數據傳入)。
因此MVC中完整的C(包含控制邏輯+功能處理)由(DispatcherServlet + Controller)組成。
因此此處的控制器是Web MVC中部分,也可以稱爲頁面控制器、動作、處理器。
Spring Web MVC支持多種類型的控制器,比如實現Controller接口,從Spring2.5開始支持註解方式的控制器(如@Controller、@RequestMapping、@RequestParam、@ModelAttribute等),我們也可以自己實現相應的控制器(只需要定義相應的HandlerMapping和HandlerAdapter即可)。
因爲考慮到還有部分公司使用繼承Controller接口實現方式,因此我們也學習一下,雖然已經不推薦使用了。
對於註解方式的控制器,後邊會詳細講,在此我們先學習Spring2.5以前的Controller接口實現方式。
首先我們將項目springmvc-chapter2複製一份改爲項目springmvc-chapter4,本章示例將放置在springmvc-chapter4中。
大家需要將項目springmvc-chapter4/ .settings/ org.eclipse.wst.common.component下的chapter2改爲chapter4,否則上下文還是“springmvc-chapter2”。以後的每一個章節都需要這麼做。
4.2、Controller接口
- package org.springframework.web.servlet.mvc;
- public interface Controller {
- ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
- }
這是控制器接口,此處只有一個方法handleRequest,用於進行請求的功能處理,處理完請求後返回ModelAndView(Model模型數據部分 和 View視圖部分)。
還記得第二章的HelloWorld嗎?我們的HelloWorldController實現Controller接口,Spring默認提供了一些Controller接口的實現以方便我們使用,具體繼承體系如圖4-1:
圖4-1
4.3、WebContentGenerator
用於提供如瀏覽器緩存控制、是否必須有session開啓、支持的請求方法類型(GET、POST等)等,該類主要有如下屬性:
Set<String> supportedMethods:設置支持的請求方法類型,默認支持“GET”、“POST”、“HEAD”,如果我們想支持“PUT”,則可以加入該集合“PUT”。
boolean requireSession = false:是否當前請求必須有session,如果此屬性爲true,但當前請求沒有打開session將拋出HttpSessionRequiredException異常;
boolean useExpiresHeader = true:是否使用HTTP1.0協議過期響應頭:如果true則會在響應頭添加:“Expires:”;需要配合cacheSeconds使用;
boolean useCacheControlHeader = true:是否使用HTTP1.1協議的緩存控制響應頭,如果true則會在響應頭添加;需要配合cacheSeconds使用;
boolean useCacheControlNoStore = true:是否使用HTTP 1.1協議的緩存控制響應頭,如果true則會在響應頭添加;需要配合cacheSeconds使用;
private int cacheSeconds = -1:緩存過期時間,正數表示需要緩存,負數表示不做任何事情(也就是說保留上次的緩存設置),
1、cacheSeconds =0時,則將設置如下響應頭數據:
Pragma:no-cache // HTTP 1.0的不緩存響應頭
Expires:1L // useExpiresHeader=true時,HTTP 1.0
Cache-Control :no-cache // useCacheControlHeader=true時,HTTP 1.1
Cache-Control :no-store // useCacheControlNoStore=true時,該設置是防止Firefox緩存
2、cacheSeconds>0時,則將設置如下響應頭數據:
Expires:System.currentTimeMillis() + cacheSeconds * 1000L // useExpiresHeader=true時,HTTP 1.0
Cache-Control :max-age=cacheSeconds // useCacheControlHeader=true時,HTTP 1.1
3、cacheSeconds<0時,則什麼都不設置,即保留上次的緩存設置。
此處簡單說一下以上響應頭的作用,緩存控制已超出本書內容:
HTTP1.0緩存控制響應頭
Pragma:no-cache:表示防止客戶端緩存,需要強制從服務器獲取最新的數據;
Expires:HTTP1.0響應頭,本地副本緩存過期時間,如果客戶端發現緩存文件沒有過期則不發送請求,HTTP的日期時間必須是格林威治時間(GMT), 如“Expires:Wed, 14 Mar 2012 09:38:32 GMT”;
HTTP1.1緩存控制響應頭
Cache-Control :no-cache 強制客戶端每次請求獲取服務器的最新版本,不經過本地緩存的副本驗證;
Cache-Control :no-store 強制客戶端不保存請求的副本,該設置是防止Firefox緩存
Cache-Control:max-age=[秒] 客戶端副本緩存的最長時間,類似於HTTP1.0的Expires,只是此處是基於請求的相對時間間隔來計算,而非絕對時間。
還有相關緩存控制機制如Last-Modified(最後修改時間驗證,客戶端的上一次請求時間 在 服務器的最後修改時間 之後,說明服務器數據沒有發生變化 返回304狀態碼)、ETag(沒有變化時不重新下載數據,返回304)。
該抽象類默認被AbstractController和WebContentInterceptor繼承。
4.4、AbstractController
該抽象類實現了Controller,並繼承了WebContentGenerator(具有該類的特性,具體請看4.3),該類有如下屬性:
boolean synchronizeOnSession = false:表示該控制器是否在執行時同步session,從而保證該會話的用戶串行訪問該控制器。
- public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
-
- checkAndPrepare(request, response, this instanceof LastModified);
-
- if (this.synchronizeOnSession) {
- HttpSession session = request.getSession(false);
- if (session != null) {
- Object mutex = WebUtils.getSessionMutex(session);
- synchronized (mutex) {
- return handleRequestInternal(request, response);
- }
- }
- }
- return handleRequestInternal(request, response);
- }
可以看出AbstractController實現了一些特殊功能,如繼承了WebContentGenerator緩存控制功能,並提供了可選的會話的串行化訪問功能。而且提供了handleRequestInternal方法,因此我們應該在具體的控制器類中實現handleRequestInternal方法,而不再是handleRequest。
AbstractController使用方法:
首先讓我們使用AbstractController來重寫第二章的HelloWorldController:
- public class HelloWorldController extends AbstractController {
- @Override
- protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
-
-
-
-
- ModelAndView mv = new ModelAndView();
-
- mv.addObject("message", "Hello World!");
-
- mv.setViewName("hello");
- return mv;
- }
- }
- <!— 在chapter4-servlet.xml配置處理器 -->
- <bean name="/hello" class="cn.javass.chapter4.web.controller.HelloWorldController"/>
從如上代碼我們可以看出:
1、繼承AbstractController
2、實現handleRequestInternal方法即可。
直接通過response寫響應
如果我們想直接在控制器通過response寫出響應呢,以下代碼幫我們闡述:
- public class HelloWorldWithoutReturnModelAndViewController extends AbstractController {
- @Override
- protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
-
- resp.getWriter().write("Hello World!!");
-
- return null;
- }
- }
- <!— 在chapter4-servlet.xml配置處理器 -->
- <bean name="/helloWithoutReturnModelAndView" class="cn.javass.chapter4.web.controller.HelloWorldWithoutReturnModelAndViewController"/>
從如上代碼可以看出如果想直接在控制器寫出響應,只需要通過response寫出,並返回null即可。
強制請求方法類型:
- <!— 在chapter4-servlet.xml配置處理器 -->
- <bean name="/helloWithPOST" class="cn.javass.chapter4.web.controller.HelloWorldController">
- <property name="supportedMethods" value="POST"></property>
- </bean>
以上配置表示只支持POST請求,如果是GET請求客戶端將收到“HTTP Status 405 - Request method 'GET' not supported”。
比如註冊/登錄可能只允許POST請求。
當前請求的session前置條件檢查,如果當前請求無session將拋出HttpSessionRequiredException異常:
- <!— 在chapter4-servlet.xml配置處理器 -->
- <bean name="/helloRequireSession"
- class="cn.javass.chapter4.web.controller.HelloWorldController">
- <property name="requireSession" value="true"/>
- </bean>
在進入該控制器時,一定要有session存在,否則拋出HttpSessionRequiredException異常。
Session同步:
即同一會話只能串行訪問該控制器。
客戶端端緩存控制:
1、緩存5秒,cacheSeconds=5
- package cn.javass.chapter4.web.controller;
-
- public class HelloWorldCacheController extends AbstractController {
- @Override
- protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
-
-
- resp.getWriter().write("<a href=''>this</a>");
- return null;
- }
- }
- <!— 在chapter4-servlet.xml配置處理器 -->
- <bean name="/helloCache"
- class="cn.javass.chapter4.web.controller.HelloWorldCacheController">
- <property name="cacheSeconds" value="5"/>
- </bean>
如上配置表示告訴瀏覽器緩存5秒鐘:
開啓chrome瀏覽器調試工具:
服務器返回的響應頭如下所示:
添加了“Expires:Wed, 14 Mar 2012 09:38:32 GMT” 和“Cache-Control:max-age=5” 表示允許客戶端緩存5秒,當你點“this”鏈接時,會發現如下:
而且服務器也沒有收到請求,當過了5秒後,你再點“this”鏈接會發現又重新請求服務器下載新數據。
注:下面提到一些關於緩存控制的一些特殊情況:
1、對於一般的頁面跳轉(如超鏈接點擊跳轉、通過js調用window.open打開新頁面都是會使用瀏覽器緩存的,在未過期情況下會直接使用瀏覽器緩存的副本,在未過期情況下一次請求也不發送);
2、對於刷新頁面(如按F5鍵刷新),會再次發送一次請求到服務器的;
2、不緩存,cacheSeconds=0
- <!— 在chapter4-servlet.xml配置處理器 -->
- <bean name="/helloNoCache"
- class="cn.javass.chapter4.web.controller.HelloWorldCacheController">
- <property name="cacheSeconds" value="0"/>
- </bean>
以上配置會要求瀏覽器每次都去請求服務器下載最新的數據:
3、cacheSeconds<0,將不添加任何數據
響應頭什麼緩存控制信息也不加。
4、Last-Modified緩存機制
(1、在客戶端第一次輸入url時,服務器端會返回內容和狀態碼200表示請求成功並返回了內容;同時會添加一個“Last-Modified”的響應頭表示此文件在服務器上的最後更新時間,如“Last-Modified:Wed, 14 Mar 2012 10:22:42 GMT”表示最後更新時間爲(2012-03-14 10:22);
(2、客戶端第二次請求此URL時,客戶端會向服務器發送請求頭 “If-Modified-Since”,詢問服務器該時間之後當前請求內容是否有被修改過,如“If-Modified-Since: Wed, 14 Mar 2012 10:22:42 GMT”,如果服務器端的內容沒有變化,則自動返回 HTTP 304狀態碼(只要響應頭,內容爲空,這樣就節省了網絡帶寬)。
客戶端強制緩存過期:
(1、可以按ctrl+F5強制刷新(會添加請求頭 HTTP1.0 Pragma:no-cache和 HTTP1.1 Cache-Control:no-cache、If-Modified-Since請求頭被刪除)表示強制獲取服務器內容,不緩存。
(2、在請求的url後邊加上時間戳來重新獲取內容,加上時間戳後瀏覽器就認爲不是同一份內容:
http://sishuok.com/?2343243243 和 http://sishuok.com/?34334344 是兩次不同的請求。
Spring也提供了Last-Modified機制的支持,只需要實現LastModified接口,如下所示:
- package cn.javass.chapter4.web.controller;
- public class HelloWorldLastModifiedCacheController extends AbstractController implements LastModified {
- private long lastModified;
- protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
-
- resp.getWriter().write("<a href=''>this</a>");
- return null;
- }
- public long getLastModified(HttpServletRequest request) {
- if(lastModified == 0L) {
-
- lastModified = System.currentTimeMillis();
- }
- return lastModified;
- }
- }
- <!— 在chapter4-servlet.xml配置處理器 -->
- <bean name="/helloLastModified"
- class="cn.javass.chapter4.web.controller.HelloWorldLastModifiedCacheController"/>
HelloWorldLastModifiedCacheController只需要實現LastModified接口的getLastModified方法,保證當內容發生改變時返回最新的修改時間即可。
分析:
(1、發送請求到服務器,如(http://localhost:9080/springmvc-chapter4/helloLastModified),則服務器返回的響應爲:
(2、再次按F5刷新客戶端,返回狀態碼304表示服務器沒有更新過:
(3、重啓服務器,再次刷新,會看到200狀態碼(因爲服務器的lastModified時間變了)。
Spring判斷是否過期,通過如下代碼,即請求的“If-Modified-Since” 大於等於當前的getLastModified方法的時間戳,則認爲沒有修改:
this.notModified = (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
5、ETag(實體標記)緩存機制
(1:瀏覽器第一次請求,服務器在響應時給請求URL標記,並在HTTP響應頭中將其傳送到客戶端,類似服務器端返回的格式:“ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"”
(2:瀏覽器第二次請求,客戶端的查詢更新格式是這樣的:“If-None-Match:"0f8b0c86fe2c0c7a67791e53d660208e3"”,如果ETag沒改變,表示內容沒有發生改變,則返回狀態304。
Spring也提供了對ETag的支持,具體需要在web.xml中配置如下代碼:
- <filter>
- <filter-name>etagFilter</filter-name>
- <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
- </filter>
- <filter-mapping>
- <filter-name>etagFilter</filter-name>
- <servlet-name>chapter4</servlet-name>
- </filter-mapping>
此過濾器只過濾到我們DispatcherServlet的請求。
分析:
1):發送請求到服務器:“http://localhost:9080/springmvc-chapter4/hello”,服務器返回的響應頭中添加了(ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"):
2):瀏覽器再次發送請求到服務器(按F5刷新),請求頭中添加了“If-None-Match:
"0f8b0c86fe2c0c7a67791e53d660208e3"”,響應返回304代碼,表示服務器沒有修改,並且響應頭再次添加了“ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"”(每次都需要計算):
那服務器端是如何計算ETag的呢?
- protected String generateETagHeaderValue(byte[] bytes) {
- StringBuilder builder = new StringBuilder("\"0");
- DigestUtils.appendMd5DigestAsHex(bytes, builder);
- builder.append('"');
- return builder.toString();
- }
bytes是response要寫回到客戶端的響應體(即響應的內容數據),是通過MD5算法計算的內容的摘要信息。也就是說如果服務器內容不發生改變,則ETag每次都是一樣的,即服務器端的內容沒有發生改變。
此處只列舉了部分緩存控制,詳細介紹超出了本書的範圍,強烈推薦: http://www.mnot.net/cache_docs/(中文版http://www.chedong.com/tech/cache_docs.html) 詳細瞭解HTTP緩存控制及爲什麼要緩存。
緩存的目的是減少相應延遲 和 減少網絡帶寬消耗,比如css、js、圖片這類靜態資源應該進行緩存。
實際項目一般使用反向代理服務器(如nginx、apache等)進行緩存。
第四章
Controller接口控制器詳解(2)
4.5、ServletForwardingController
將接收到的請求轉發到一個命名的servlet,具體示例如下:
java代碼:
-
package cn.javass.chapter4.web.servlet;
-
public class ForwardingServlet extends HttpServlet {
-
@Override
-
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
-
throws ServletException, IOException {
-
-
resp.getWriter().write("Controller forward to Servlet");
-
-
}
-
}
java代碼:
-
<servlet>
-
<servlet-name>forwarding</servlet-name>
-
<servlet-class>cn.javass.chapter4.web.servlet.ForwardingServlet</servlet-class>
-
</servlet>
java代碼:
-
<!— 在chapter4-servlet.xml配置處理器 -->
-
<bean name="/forwardToServlet"
-
class="org.springframework.web.servlet.mvc.ServletForwardingController">
-
<property name="servletName" value="forwarding"></property>
-
</bean>
當我們請求/forwardToServlet時,會被轉發到名字爲“forwarding”的servlet處理,該sevlet的servlet-mapping標籤配置是可選的。
4.6、BaseCommandController
命令控制器通用基類,提供了以下功能支持:
1、數據綁定:請求參數綁定到一個command object(命令對象,非GoF裏的命令設計模式),這裏的命令對象是指綁定請求參數的任何POJO對象;
commandClass:表示命令對象實現類,如UserModel;
commandName:表示放入請求的命令對象名字(默認command),request.setAttribute(commandName, commandObject);
2、驗證功能:提供Validator註冊功能,註冊的驗證器會驗證命令對象屬性數據是否合法;
validators:通過該屬性注入驗證器,驗證器用來驗證命令對象屬性是否合法;
該抽象類沒有沒有提供流程功能,只是提供了一些公共的功能,實際使用時需要使用它的子類。
4.7、AbstractCommandController
命令控制器之一,可以實現該控制器來創建命令控制器,該控制器能把自動封裝請求參數到一個命令對象,而且提供了驗證功能。
1、創建命令類(就是普通的JavaBean類/POJO)
java代碼:
-
package cn.javass.chapter4.model;
-
public class UserModel {
-
private String username;
-
private String password;
-
-
}
2、實現控制器
java代碼:
-
package cn.javass.chapter4.web.controller;
-
-
public class MyAbstractCommandController extends AbstractCommandController {
-
public MyAbstractCommandController() {
-
-
setCommandClass(UserModel.class);
-
}
-
@Override
-
protected ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception {
-
-
UserModel user = (UserModel) command;
-
ModelAndView mv = new ModelAndView();
-
mv.setViewName("abstractCommand");
-
mv.addObject("user", user);
-
return mv;
-
}
-
}
java代碼:
-
<!— 在chapter4-servlet.xml配置處理器 -->
-
<bean name="/abstractCommand"
-
class="cn.javass.chapter4.web.controller.MyAbstractCommandController">
-
<!-- 也可以通過依賴注入 注入命令實現類 -->
-
<!-- property name="commandClass" value="cn.javass.chapter4.model.UserModel"/-->
-
</bean>
java代碼:
-
<!— WEB-INF/jsp/abstractCommand.jsp視圖下的主要內容 -->
-
-
${user.username }-${user.password }
當我們在瀏覽器中輸入“http://localhost:9080/springmvc-chapter4/abstractCommand?username=123&password=123”,會自動將請求參數username和password綁定到命令對象;綁定時按照JavaBean命名規範綁定;
4.8、AbstractFormController
用於支持帶步驟的表單提交的命令控制器基類,使用該控制器可以完成:
1、定義表單處理(表單的渲染),並從控制器獲取命令對象構建表單;
2、提交表單處理,當用戶提交表單內容後,AbstractFormController可以將用戶請求的數據綁定到命令對象,並可以驗證表單內容、對命令對象進行處理。
java代碼:
-
@Override
-
rotected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
-
throws Exception {
-
-
if (isFormSubmission(request)) {
-
try {
-
Object command = getCommand(request);
-
ServletRequestDataBinder binder = bindAndValidate(request, command);
-
BindException errors = new BindException(binder.getBindingResult());
-
-
return processFormSubmission(request, response, command, errors);
-
}
-
catch (HttpSessionRequiredException ex) {
-
-
return handleInvalidSubmit(request, response);
-
}
-
}
-
else {
-
-
return showNewForm(request, response);
-
}
bindOnNewForm:是否在進行表單展示時綁定請求參數到表單對象,默認false,不綁定;
sessionForm:session表單模式,如果開啓(true)則會將表單對象放置到session中,從而可以跨越多次請求保證數據不丟失(多步驟表單常使用該方式,詳解AbstractWizardFormController),默認false;
Object formBackingObject(HttpServletRequest request) :提供給表單展示時使用的表單對象(form object表單要展示的默認數據),默認通過commandName暴露到請求給展示表單;
Map referenceData(HttpServletRequest request, Object command, Errors errors):展示表單時需要的一些引用數據(比如用戶註冊,可能需要選擇工作地點,這些數據可以通過該方法提供),如:
java代碼:
-
protected Map referenceData(HttpServletRequest request) throws Exception {
-
Map model = new HashMap();
-
model.put("cityList", cityList);
-
return model;
-
}
這樣就可以在表單展示頁面獲取cityList數據。
SimpleFormController繼承該類,而且提供了更簡單的表單流程控制。
4.9、SimpleFormController
提供了更好的兩步表單支持:
1、準備要展示的數據,併到表單展示頁面;
2、提交數據數據進行處理。
第一步,展示:
第二步,提交表單:
接下來咱們寫一個用戶註冊的例子學習一下:
(1、控制器
java代碼:
-
package cn.javass.chapter4.web.controller;
-
-
public class RegisterSimpleFormController extends SimpleFormController {
-
public RegisterSimpleFormController() {
-
setCommandClass(UserModel.class);
-
setCommandName("user");
-
}
-
-
protected Object formBackingObject(HttpServletRequest request) throws Exception {
-
UserModel user = new UserModel();
-
user.setUsername("請輸入用戶名");
-
return user;
-
}
-
-
protected Map referenceData(HttpServletRequest request) throws Exception {
-
Map map = new HashMap();
-
map.put("cityList", Arrays.asList("山東", "北京", "上海"));
-
return map;
-
}
-
protected void doSubmitAction(Object command) throws Exception {
-
UserModel user = (UserModel) command;
-
-
System.out.println(user);
-
}
-
}
setCommandClass和setCommandName:分別設置了命令對象的實現類和名字;
formBackingObject和referenceData:提供了表單展示需要的視圖;
doSubmitAction:用於執行表單提交動作,由onSubmit方法調用,如果不需要請求/響應對象或進行數據驗證,可以直接使用doSubmitAction方法進行功能處理。
(2、spring配置(chapter4-servlet.xml)
java代碼:
-
<bean name="/simpleForm"
-
class="cn.javass.chapter4.web.controller.RegisterSimpleFormController">
-
<property name="formView" value="register"/>
-
<property name="successView" value="redirect:/success"/>
-
</bean>
-
<bean name="/success" class="cn.javass.chapter4.web.controller.SuccessController"/>
formView:表示展示表單時顯示的頁面;
successView:表示處理成功時顯示的頁面;“redirect:/success”表示成功處理後重定向到/success控制器;防止表單重複提交;
“/success” bean的作用是顯示成功頁面,此處就不列舉了。
(3、視圖頁面
java代碼:
-
<!-- register.jsp 註冊展示頁面-->
-
<form method="post">
-
username:<input type="text" name="username" value="${user.username}"><br/>
-
password:<input type="password" name="username"><br/>
-
city:<select>
-
<c:forEach items="${cityList }" var="city">
-
<option>${city}</option>
-
</c:forEach>
-
</select><br/>
-
<input type="submit" value="註冊"/>
-
</form>
此處可以使用${user.username}獲取到formBackingObject設置的表單對象、使用${cityList}獲取referenceData設置的表單支持數據;
到此一個簡單的兩步表單到此結束,但這個表單有重複提交表單的問題,而且表單對象到頁面的綁定是通過手工綁定的,後邊我們會學習spring標籤庫(提供自動綁定表單對象到頁面)。
4.10、CancellableFormController
一個可取消的表單控制器,繼承SimpleFormController,額外提供取消表單功能。
1、表單展示:和SimpleFormController一樣;
2、表單取消:和SimpleFormController一樣;
3、表單成功提交:取消功能處理方法爲:onCancel(Object command),而且默認返回cancelView屬性指定的邏輯視圖名。
那如何判斷是取消呢?如果請求中有參數名爲“_cancel”的參數,則表示表單取消。也可以通過cancelParamKey來修改參數名(如“_cancel.x”等)。
示例:
(1、控制器
複製RegisterSimpleFormController一份命名爲CanCancelRegisterSimpleFormController,添加取消功能處理方法實現:
java代碼:
-
@Override
-
protected ModelAndView onCancel(Object command) throws Exception {
-
UserModel user = (UserModel) command;
-
-
System.out.println(user);
-
return super.onCancel(command);
-
}
onCancel:在該功能方法內實現取消邏輯,父類的onCancel方法默認返回cancelView屬性指定的邏輯視圖名。
(2、spring配置(chapter4-servlet.xml)
java代碼:
-
<bean name="/canCancelForm"
-
class="cn.javass.chapter4.web.controller.CanCancelRegisterSimpleFormController">
-
<property name="formView" value="register"/>
-
<property name="successView" value="redirect:/success"/>
-
<property name="cancelView" value="redirect:/cancel"/>
-
</bean>
-
<bean name="/cancel" class="cn.javass.chapter4.web.controller.CancelController"/>
cancelParamKey:用於判斷是否是取消的請求參數名,默認是_cancel,即如果請求參數數據中含有名字_cancel則表示是取消,將調用onCancel功能處理方法;
cancelView:表示取消時時顯示的頁面;“redirect:/cancel”表示成功處理後重定向到/cancel控制器;防止表單重複提交;
“/cancel” bean的作用是顯示取消頁面,此處就不列舉了(詳見代碼)。
(3、視圖頁面(修改register.jsp)
java代碼:
-
<input type="submit" name="_cancel" value="取消"/>
該提交按鈕的作用是取消,因爲name="_cancel",即請求後會有一個名字爲_cancel的參數,因此會執行onCancel功能處理方法。
(4、測試:
在瀏覽器輸入“http://localhost:9080/springmvc-chapter4/canCancelForm”,則首先到展示視圖頁面,點擊“取消按鈕”將重定向到“http://localhost:9080/springmvc-chapter4/cancel”,說明取消成功了。
實際項目可能會出現比如一些網站的完善個人資料都是多個頁面(即多步),那應該怎麼實現呢?接下來讓我們看一下spring Web MVC提供的對多步表單的支持類AbstractWizardFormController。
第四章
Controller接口控制器詳解(3)
4.11、AbstractWizardFormController
嚮導控制器類提供了多步驟(嚮導)表單的支持(如完善個人資料時分步驟填寫基本信息、工作信息、學校信息等)
假設現在做一個完善個人信息的功能,分三個頁面展示:
1、頁面1完善基本信息;
2、頁面2完善學校信息;
3、頁面3完善工作信息。
這裏我們要注意的是當用戶跳轉到頁面2時頁面1的信息是需要保存起來的,還記得AbstractFormController中的sessionForm嗎? 如果爲true則表單數據存放到session中,哈哈,AbstractWizardFormController就是使用了這個特性。
嚮導中的頁碼從0開始;
PARAM_TARGET = "_target":
用於選擇嚮導中的要使用的頁面參數名前綴,如“_target0”則選擇第0個頁面顯示,即圖中的“wizard/baseInfo”,以此類推,如“_target1”將選擇第1頁面,要得到的頁碼爲去除前綴“_target”後的數字即是;
PARAM_FINISH = "_finish":
如果請求參數中有名爲“_finish”的參數,表示嚮導成功結束,將會調用processFinish方法進行完成時的功能處理;
PARAM_CANCEL = "_cancel":
如果請求參數中有名爲“_cancel”的參數,表示嚮導被取消,將會調用processCancel方法進行取消時的功能處理;
嚮導中的命令對象:
嚮導中的每一個步驟都會把相關的參數綁定到命令對象,該表單對象默認放置在session中,從而可以跨越多次請求得到該命令對象。
接下來具體看一下如何使用吧。
(1、修改我們的模型數據以支持多步驟提交:
-
public class UserModel {
-
private String username;
-
private String password;
-
private String realname; //真實姓名
-
private WorkInfoModel workInfo;
-
private SchoolInfoModel schoolInfo;
-
//省略getter/setter
-
}
-
public class SchoolInfoModel {
-
private String schoolType; //學校類型:高中、中專、大學
-
private String schoolName; //學校名稱
-
private String specialty; //專業
-
//省略getter/setter
-
}
-
public class WorkInfoModel {
-
private String city; //所在城市
-
private String job; //職位
-
private String year; //工作年限
-
//省略getter/setter
-
}
(2、控制器
-
package cn.javass.chapter4.web.controller;
-
//省略import
-
public class InfoFillWizardFormController extends AbstractWizardFormController {
-
public InfoFillWizardFormController() {
-
setCommandClass(UserModel.class);
-
setCommandName("user");
-
}
-
protected Map referenceData(HttpServletRequest request, int page) throws Exception {
-
Map map = new HashMap();
-
if(page==1) { //如果是填寫學校信息頁 需要學校類型信息
-
map.put("schoolTypeList", Arrays.asList("高中", "中專", "大學"));
-
}
-
if(page==2) {//如果是填寫工作信息頁 需要工作城市信息
-
map.put("cityList", Arrays.asList("濟南", "北京", "上海"));
-
}
-
return map;
-
}
-
protected void validatePage(Object command, Errors errors, int page) {
-
//提供每一頁數據的驗證處理方法
-
}
-
protected void postProcessPage(HttpServletRequest request, Object command, Errors errors, int page) throws Exception {
-
//提供給每一頁完成時的後處理方法
-
}
-
protected ModelAndView processFinish(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception {
-
//成功後的處理方法
-
System.out.println(command);
-
return new ModelAndView("redirect:/success");
-
}
-
protected ModelAndView processCancel(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception {
-
//取消後的處理方法
-
System.out.println(command);
-
return new ModelAndView("redirect:/cancel");
-
}
-
}
page頁碼:是根據請求中以“_target”開頭的參數名來確定的,如“_target0”,則頁碼爲0;
referenceData:提供每一頁需要的表單支持對象,如完善學校信息需要學校類型,page頁碼從0開始(而且根據請求參數中以“_target”開頭的參數來確定當前頁碼,如_target1,則page=1);
validatePage:驗證當前頁的命令對象數據,驗證應根據page頁碼來分步驟驗證;
postProcessPage:驗證成功後的後處理;
processFinish:成功時執行的方法,此處直接重定向到/success控制器(詳見CancelController);
processCancel:取消時執行的方法,此處直接重定向到/cancel控制器(詳見SuccessController);
其他需要了解:
allowDirtyBack和allowDirtyForward:決定在當前頁面驗證失敗時,是否允許嚮導前移和後退,默認false不允許;
onBindAndValidate(HttpServletRequest request, Object command, BindException errors, int page):允許覆蓋默認的綁定參數到命令對象和驗證流程。
(3、spring配置文件(chapter4-servlet.xml)
-
<bean name="/infoFillWizard"
-
class="cn.javass.chapter4.web.controller.InfoFillWizardFormController">
-
<property name="pages">
-
<list>
-
<value>wizard/baseInfo</value>
-
<value>wizard/schoolInfo</value>
-
<value>wizard/workInfo</value>
-
</list>
-
</property>
-
</bean>
pages:表示嚮導中每一個步驟的邏輯視圖名,當InfoFillWizardFormController的page=0,則將會選擇“wizard/baseInfo”,以此類推,從而可以按步驟選擇要展示的視圖。
(4、嚮導中的每一步視圖
(4.1、基本信息頁面(第一步) baseInfo.jsp:
-
<form method="post">
-
真實姓名:<input type="text" name="realname" value="${user.realname}"><br/>
-
<input type="submit" name="_target1" value="下一步"/>
-
</form>
當前頁碼爲0;
name="_target1":表示嚮導下一步要顯示的頁面的頁碼爲1;
(4.2、學校信息頁面(第二步) schoolInfo.jsp:
-
<form method="post">
-
學校類型:<select name="schoolInfo.schoolType">
-
<c:forEach items="${schoolTypeList }" var="schoolType">
-
<option value="${schoolType }"
-
<c:if test="${user.schoolInfo.schoolType eq schoolType}">
-
selected="selected"
-
</c:if>
-
>
-
${schoolType}
-
</option>
-
</c:forEach>
-
</select><br/>
-
學校名稱:<input type="text" name="schoolInfo.schoolName" value="${user.schoolInfo.schoolName}"/><br/>
-
專業:<input type="text" name="schoolInfo.specialty" value="${user.schoolInfo.specialty}"/><br/>
-
<input type="submit" name="_target0" value="上一步"/>
-
<input type="submit" name="_target2" value="下一步"/>
-
</form>
(4.3、工作信息頁面(第三步) workInfo.jsp:
-
<form method="post">
-
所在城市:<select name="workInfo.city">
-
<c:forEach items="${cityList }" var="city">
-
<option value="${city }"
-
<c:if test="${user.workInfo.city eq city}">selected="selected"</c:if>
-
>
-
${city}
-
</option>
-
</c:forEach>
-
</select><br/>
-
職位:<input type="text" name="workInfo.job" value="${user.workInfo.job}"/><br/>
-
工作年限:<input type="text" name="workInfo.year" value="${user.workInfo.year}"/><br/>
-
<input type="submit" name="_target1" value="上一步"/>
-
<input type="submit" name="_finish" value="完成"/>
-
<input type="submit" name="_cancel" value="取消"/>
-
</form>
當前頁碼爲2;
name="_target1":上一步,表示嚮導上一步要顯示的頁面的頁碼爲1;
name="_finish":嚮導完成,表示嚮導成功,將會調用嚮導控制器的processFinish方法;
name="_cancel":嚮導取消,表示嚮導被取消,將會調用嚮導控制器的processCancel方法;
到此嚮導控制器完成,此處的嚮導流程比較簡單,如果需要更復雜的頁面流程控制,可以選擇使用Spring Web Flow框架。
4.12、ParameterizableViewController
參數化視圖控制器,不進行功能處理(即靜態視圖),根據參數的邏輯視圖名直接選擇需要展示的視圖。
-
<bean name="/parameterizableView"
-
class="org.springframework.web.servlet.mvc.ParameterizableViewController">
-
<property name="viewName" value="success"/>
-
</bean>
該控制器接收到請求後直接選擇參數化的視圖,這樣的好處是在配置文件中配置,從而避免程序的硬編碼,比如像幫助頁面等不需要進行功能處理,因此直接使用該控制器映射到視圖。
4.13、AbstractUrlViewController
提供根據請求URL路徑直接轉化爲邏輯視圖名的支持基類,即不需要功能處理,直接根據URL計算出邏輯視圖名,並選擇具體視圖進行展示:
urlDecode:是否進行url解碼,不指定則默認使用服務器編碼進行解碼(如Tomcat默認ISO-8859-1);
urlPathHelper:用於解析請求路徑的工具類,默認爲org.springframework.web.util.UrlPathHelper。
UrlFilenameViewController是它的一個實現者,因此我們應該使用UrlFilenameViewController。
4.14、UrlFilenameViewController
將請求的URL路徑轉換爲邏輯視圖名並返回的轉換控制器,即不需要功能處理,直接根據URL計算出邏輯視圖名,並選擇具體視圖進行展示:
根據請求URL路徑計算邏輯視圖名;
-
<bean name="/index1/*"
-
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
-
<bean name="/index2/**"
-
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
-
<bean name="/*.html"
-
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
-
<bean name="/index3/*.html"
-
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
/index1/*:可以匹配/index1/demo,但不匹配/index1/demo/demo,如/index1/demo邏輯視圖名爲demo;
/index2/**:可以匹配/index2路徑下的所有子路徑,如匹配/index2/demo,或/index2/demo/demo,“/index2/demo”的邏輯視圖名爲demo,而“/index2/demo/demo”邏輯視圖名爲demo/demo;
/*.html:可以匹配如/abc.html,邏輯視圖名爲abc,後綴會被刪除(不僅僅可以是html);
/index3/*.html:可以匹配/index3/abc.html,邏輯視圖名也是abc;
上述模式爲Spring Web MVC使用的Ant-style 模式進行匹配的:
-
? 匹配一個字符,如/index? 可以匹配 /index1 , 但不能匹配 /index 或 /index12
-
* 匹配零個或多個字符,如/index1/*,可以匹配/index1/demo,但不匹配/index1/demo/demo
-
** 匹配零個或多個路徑,如/index2/**:可以匹配/index2路徑下的所有子路徑,如匹配/index2/demo,或/index2/demo/demo
-
-
如果我有如下模式,那Spring該選擇哪一個執行呢?當我的請求爲“/long/long”時如下所示:
-
/long/long
-
/long/**/abc
-
/long/**
-
/**
-
Spring的AbstractUrlHandlerMapping使用:最長匹配優先;
-
如請求爲“/long/long” 將匹配第一個“/long/long”,但請求“/long/acd” 則將匹配 “/long/**”,如請求“/long/aa/abc”則匹配“/long/**/abc”,如請求“/abc”則將匹配“/**”
UrlFilenameViewController還提供瞭如下屬性:
prefix:生成邏輯視圖名的前綴;
suffix:生成邏輯視圖名的後綴;
-
protected String postProcessViewName(String viewName) {
-
return getPrefix() + viewName + getSuffix();
-
}
-
<bean name="/*.htm" class="org.springframework.web.servlet.mvc.UrlFilenameViewController">
-
<property name="prefix" value="test"/>
-
<property name="suffix" value="test"/>
-
</bean>
當prefix=“test”,suffix=“test”,如上所示的/*.htm:可以匹配如/abc.htm,但邏輯視圖名將變爲testabctest。
第四章
Controller接口控制器詳解(4)
4.12、ParameterizableViewController
參數化視圖控制器,不進行功能處理(即靜態視圖),根據參數的邏輯視圖名直接選擇需要展示的視圖。
-
<bean name="/parameterizableView"
-
class="org.springframework.web.servlet.mvc.ParameterizableViewController">
-
<property name="viewName" value="success"/>
-
</bean>
該控制器接收到請求後直接選擇參數化的視圖,這樣的好處是在配置文件中配置,從而避免程序的硬編碼,比如像幫助頁面等不需要進行功能處理,因此直接使用該控制器映射到視圖。
4.13、AbstractUrlViewController
提供根據請求URL路徑直接轉化爲邏輯視圖名的支持基類,即不需要功能處理,直接根據URL計算出邏輯視圖名,並選擇具體視圖進行展示:
urlDecode:是否進行url解碼,不指定則默認使用服務器編碼進行解碼(如Tomcat默認ISO-8859-1);
urlPathHelper:用於解析請求路徑的工具類,默認爲org.springframework.web.util.UrlPathHelper。
UrlFilenameViewController是它的一個實現者,因此我們應該使用UrlFilenameViewController。
4.14、UrlFilenameViewController
將請求的URL路徑轉換爲邏輯視圖名並返回的轉換控制器,即不需要功能處理,直接根據URL計算出邏輯視圖名,並選擇具體視圖進行展示:
根據請求URL路徑計算邏輯視圖名;
-
<bean name="/index1/*"
-
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
-
<bean name="/index2/**"
-
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
-
<bean name="/*.html"
-
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
-
<bean name="/index3/*.html"
-
class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
/index1/*:可以匹配/index1/demo,但不匹配/index1/demo/demo,如/index1/demo邏輯視圖名爲demo;
/index2/**:可以匹配/index2路徑下的所有子路徑,如匹配/index2/demo,或/index2/demo/demo,“/index2/demo”的邏輯視圖名爲demo,而“/index2/demo/demo”邏輯視圖名爲demo/demo;
/*.html:可以匹配如/abc.html,邏輯視圖名爲abc,後綴會被刪除(不僅僅可以是html);
/index3/*.html:可以匹配/index3/abc.html,邏輯視圖名也是abc;
上述模式爲Spring Web MVC使用的Ant-style 模式進行匹配的:
-
? 匹配一個字符,如/index? 可以匹配 /index1 , 但不能匹配 /index 或 /index12
-
* 匹配零個或多個字符,如/index1/*,可以匹配/index1/demo,但不匹配/index1/demo/demo
-
** 匹配零個或多個路徑,如/index2/**:可以匹配/index2路徑下的所有子路徑,如匹配/index2/demo,或/index2/demo/demo
-
-
如果我有如下模式,那Spring該選擇哪一個執行呢?當我的請求爲“/long/long”時如下所示:
-
/long/long
-
/long/**/abc
-
/long/**
-
/**
-
Spring的AbstractUrlHandlerMapping使用:最長匹配優先;
-
如請求爲“/long/long” 將匹配第一個“/long/long”,但請求“/long/acd” 則將匹配 “/long/**”,如請求“/long/aa/abc”則匹配“/long/**/abc”,如請求“/abc”則將匹配“/**”
UrlFilenameViewController還提供瞭如下屬性:
prefix:生成邏輯視圖名的前綴;
suffix:生成邏輯視圖名的後綴;
-
protected String postProcessViewName(String viewName) {
-
return getPrefix() + viewName + getSuffix();
-
}
-
<bean name="/*.htm" class="org.springframework.web.servlet.mvc.UrlFilenameViewController">
-
<property name="prefix" value="test"/>
-
<property name="suffix" value="test"/>
-
</bean>
當prefix=“test”,suffix=“test”,如上所示的/*.htm:可以匹配如/abc.htm,但邏輯視圖名將變爲testabctest。
第四章
Controller接口控制器詳解(5)
4.15、MultiActionController
之前學過的控制器如AbstractCommandController、SimpleFormController等一般對應一個功能處理方法(如新增),如果我要實現比如最簡單的用戶增刪改查(CRUD Create-Read-Update-Delete),那該怎麼辦呢?
4.15.1 解決方案
1、每一個功能對應一個控制器,如果是CRUD則需要四個控制器,但這樣我們的控制器會暴增,肯定不可取;
2、使用Spring Web MVC提供的MultiActionController,用於支持在一個控制器裏添加多個功能處理方法,即將多個請求的處理方法放置到一個控制器裏,這種方式不錯。
4.15.2 問題
1、 MultiActionController如何將不同的請求映射不同的請求的功能處理方法呢?
Spring Web MVC提供了MethodNameResolver(方法名解析器)用於解析當前請求到需要執行的功能處理方法的方法名。默認使用InternalPathMethodNameResolver實現類,另外還提供了ParameterMethodNameResolver和PropertiesMethodNameResolver,當然我們也可以自己來實現,稍候我們仔細研究下它們是如何工作的。
2、那我們的功能處理方法應該怎麼寫呢?
public (ModelAndView | Map | String | void) actionName(HttpServletRequest request, HttpServletResponse response, [,HttpSession session] [,AnyObject]);
哦,原來如此,我們只需要按照如上格式寫我們的功能處理方法即可;此處需要注意一下幾點:
1、返回值:即模型和視圖部分;
ModelAndView:模型和視圖部分,之前已經見過了;
Map:只返回模型數據,邏輯視圖名會根據RequestToViewNameTranslator實現類來計算,稍候討論;
String:只返回邏輯視圖名;
void:表示該功能方法直接寫出response響應(如果其他返回值類型(如Map)返回null則和void進行相同的處理);
2、actionName:功能方法名字;由methodNameResolver根據請求信息解析功能方法名,通過反射調用;
3、形參列表:順序固定,“[]”表示可選,我們來看看幾個示例吧:
//表示到新增頁面
public ModelAndView toAdd(HttpServletRequest request, HttpServletResponse response);
//表示新增表單提交,在最後可以帶着命令對象
public ModelAndView add(HttpServletRequest request, HttpServletResponse response, UserModel user);
//列表,但只返回模型數據,視圖名會通過RequestToViewNameTranslator實現來計算
public Map list(HttpServletRequest request, HttpServletResponse response);
//文件下載,返回值類型爲void,表示該功能方法直接寫響應
public void fileDownload(HttpServletRequest request, HttpServletResponse response)
//第三個參數可以是session
public ModelAndView sessionWith(HttpServletRequest request, HttpServletResponse response, HttpSession session);
//如果第三個參數是session,那麼第四個可以是命令對象,順序必須是如下順序
public void sessionAndCommandWith(HttpServletRequest request, HttpServletResponse response, HttpSession session, UserModel user)
4、異常處理方法,MultiActionController提供了簡單的異常處理,即在請求的功能處理過程中遇到異常會交給異常處理方法進行處理,式如下所示:
public ModelAndView anyMeaningfulName(HttpServletRequest request, HttpServletResponse response, ExceptionClass exception)
MultiActionController會使用最接近的異常類型來匹配對應的異常處理方法,示例如下所示:
//處理PayException
public ModelAndView processPayException(HttpServletRequest request, HttpServletResponse response, PayException ex)
//處理Exception
public ModelAndView processException(HttpServletRequest request, HttpServletResponse response, Exception ex)
4.15.3 MultiActionController類實現
類定義:public class MultiActionController extends AbstractController implements LastModified ,繼承了AbstractController,並實現了LastModified接口,默認返回-1;
核心屬性:
delegate:功能處理的委託對象,即我們要調用請求處理方法所在的對象,默認是this;
methodNameResolver:功能處理方法名解析器,即根據請求信息來解析需要執行的delegate的功能處理方法的方法名。
核心方法:
-
//判斷方法是否是功能處理方法
-
private boolean isHandlerMethod(Method method) {
-
//得到方法返回值類型
-
Class returnType = method.getReturnType();
-
//返回值類型必須是ModelAndView、Map、String、void中的一種,否則不是功能處理方法
-
if (ModelAndView.class.equals(returnType) || Map.class.equals(returnType) || String.class.equals(returnType) ||
-
void.class.equals(returnType)) {
-
Class[] parameterTypes = method.getParameterTypes();
-
//功能處理方法參數個數必須>=2,且第一個是HttpServletRequest類型、第二個是HttpServletResponse
-
//且不能Controller接口的handleRequest(HttpServletRequest request, HttpServletResponse response),這個方法是由系統調用
-
return (parameterTypes.length >= 2 &&
-
HttpServletRequest.class.equals(parameterTypes[0]) &&
-
HttpServletResponse.class.equals(parameterTypes[1]) &&
-
!("handleRequest".equals(method.getName()) && parameterTypes.length == 2));
-
}
-
return false;
-
}
-
//是否是異常處理方法
-
private boolean isExceptionHandlerMethod(Method method) {
-
//異常處理方法必須是功能處理方法 且 參數長度爲3、第三個參數類型是Throwable子類
-
return (isHandlerMethod(method) &&
-
method.getParameterTypes().length == 3 &&
-
Throwable.class.isAssignableFrom(method.getParameterTypes()[2]));
-
}
-
private void registerHandlerMethods(Object delegate) {
-
//緩存Map清空
-
this.handlerMethodMap.clear();
-
this.lastModifiedMethodMap.clear();
-
this.exceptionHandlerMap.clear();
-
-
//得到委託對象的所有public方法
-
Method[] methods = delegate.getClass().getMethods();
-
for (Method method : methods) {
-
//驗證是否是異常處理方法,如果是放入exceptionHandlerMap緩存map
-
if (isExceptionHandlerMethod(method)) {
-
registerExceptionHandlerMethod(method);
-
}
-
//驗證是否是功能處理方法,如果是放入handlerMethodMap緩存map
-
else if (isHandlerMethod(method)) {
-
registerHandlerMethod(method);
-
registerLastModifiedMethodIfExists(delegate, method);
-
}
-
}
-
}
-
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
-
throws Exception {
-
try {
-
//1、使用methodNameResolver 方法名解析器根據請求解析到要執行的功能方法的方法名
-
String methodName = this.methodNameResolver.getHandlerMethodName(request);
-
//2、調用功能方法(通過反射調用,此處就粘貼代碼了)
-
return invokeNamedMethod(methodName, request, response);
-
}
-
catch (NoSuchRequestHandlingMethodException ex) {
-
return handleNoSuchRequestHandlingMethod(ex, request, response);
-
}
-
}
接下來,我們看一下MultiActionController如何使用MethodNameResolver來解析請求到功能處理方法的方法名。
4.15.4 MethodNameResolver
1、InternalPathMethodNameResolver:
MultiActionController的默認實現,提供從請求URL路徑解析功能方法的方法名,從請求的最後一個路徑(/)開始,並忽略擴展名;如請求URL是“/user/list.html”,則解析的功能處理方法名爲“list”,即調用list方法。該解析器還可以指定前綴和後綴,通過prefix和suffix屬性,如指定prefix=”test_”,則功能方法名將變爲test_list;
2、ParameterMethodNameResolver:
提供從請求參數解析功能處理方法的方法名,並按照如下順序進行解析:
<!--[if !supportLists]-->(1、
<!--[endif]-->methodParamNames:
根據請求的參數名解析功能方法名(功能方法名和參數名同名);
-
<property name="methodParamNames" value="list,create,update"/>
如上配置時,如果請求中含有參數名list、create、update時,則功能處理方法名爲list、create、update,這種方式的可以在當一個表單有多個提交按鈕時使用,不同的提交按鈕名字不一樣即可。
ParameterMethodNameResolver也考慮到圖片提交按鈕提交問題:
<input type="image" name="list"> 和submit類似可以提交表單,單擊該圖片後會發送兩個參數“list.x=x軸座標”和“list.y=y軸座標”(如提交後會變爲list.x=7&list.y=5);因此我們配置的參數名(如list)在會加上“.x” 和 “.y”進行匹配。
-
for (String suffix : SUBMIT_IMAGE_SUFFIXES) {//SUBMIT_IMAGE_SUFFIXES {“.x”, “.y”}
-
if (request.getParameter(name + suffix) != null) {// name是我們配置的methodParamNames
-
return true;
-
}
-
}
(2、paramName:
根據請求參數名的值解析功能方法名,默認的參數名是action,即請求的參數中含有“action=query”,則功能處理方法名爲query;
(3、logicalMappings:
邏輯功能方法名到真實功能方法名映射,如下所示:
-
<property name="logicalMappings">
-
<props>
-
<prop key="doList">list</prop>
-
</props>
-
</property>
即如果步驟1或2解析出邏輯功能方法名爲doList(邏輯的),將會被重新映射爲list功能方法名(真正執行的)。
(4、defaultMethodName:
默認的方法名,當以上策略失敗時默認調用的方法名。
3、PropertiesMethodNameResolver:
提供自定義的從請求URL解析功能方法的方法名,使用一組用戶自定義的模式到功能方法名的映射,映射使用
Properties對象存放,具體配置示例如下:
-
<bean id="propertiesMethodNameResolver"
-
class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
-
<property name="mappings">
-
<props>
-
<prop key="/create">create</prop>
-
<prop key="/update">update</prop>
-
<prop key="/delete">delete</prop>
-
<prop key="/list">list</prop>
-
<!-- 默認的行爲 -->
-
<prop key="/**">list</prop>
-
</props>
-
</property>
-
</bean>
對於/create請求將調用create方法,Spring內部使用PathMatcher進行匹配(默認實現是AntPathMatcher)。
4.15.5 RequestToViewNameTranslator
用於直接將請求轉換爲邏輯視圖名。默認實現爲DefaultRequestToViewNameTranslator。
1、DefaultRequestToViewNameTranslator:
將請求URL轉換爲邏輯視圖名,默認規則如下:
http://localhost:9080/web上下文/list -------> 邏輯視圖名爲list
http://localhost:9080/web上下文/list.html -------> 邏輯視圖名爲list(默認刪除擴展名)
http://localhost:9080/web上下文/user/list.html -------> 邏輯視圖名爲user/list
4.15.6 示例
(1、控制器UserController
-
package cn.javass.chapter4.web.controller;
-
//省略import
-
public class UserController extends MultiActionController {
-
//用戶服務類
-
private UserService userService;
-
//邏輯視圖名 通過依賴注入方式注入,可配置
-
private String createView;
-
private String updateView;
-
private String deleteView;
-
private String listView;
-
private String redirectToListView;
-
//省略setter/getter
-
-
public String create(HttpServletRequest request, HttpServletResponse response, UserModel user) {
-
if("GET".equals(request.getMethod())) {
-
//如果是get請求 我們轉向 新增頁面
-
return getCreateView();
-
}
-
userService.create(user);
-
//直接重定向到列表頁面
-
return getRedirectToListView();
-
}
-
public ModelAndView update(HttpServletRequest request, HttpServletResponse response, UserModel user) {
-
if("GET".equals(request.getMethod())) {
-
//如果是get請求 我們轉向更新頁面
-
ModelAndView mv = new ModelAndView();
-
//查詢要更新的數據
-
mv.addObject("command", userService.get(user.getUsername()));
-
mv.setViewName(getUpdateView());
-
return mv;
-
}
-
userService.update(user);
-
//直接重定向到列表頁面
-
return new ModelAndView(getRedirectToListView());
-
}
-
-
-
public ModelAndView delete(HttpServletRequest request, HttpServletResponse response, UserModel user) {
-
if("GET".equals(request.getMethod())) {
-
//如果是get請求 我們轉向刪除頁面
-
ModelAndView mv = new ModelAndView();
-
//查詢要刪除的數據
-
mv.addObject("command", userService.get(user.getUsername()));
-
mv.setViewName(getDeleteView());
-
return mv;
-
}
-
userService.delete(user);
-
//直接重定向到列表頁面
-
return new ModelAndView(getRedirectToListView());
-
}
-
-
public ModelAndView list(HttpServletRequest request, HttpServletResponse response) {
-
ModelAndView mv = new ModelAndView();
-
mv.addObject("userList", userService.list());
-
mv.setViewName(getListView());
-
return mv;
-
}
-
-
//如果使用委託方式,命令對象名稱只能是command
-
protected String getCommandName(Object command) {
-
//命令對象的名字 默認command
-
return "command";
-
}
-
}
增刪改:如果是GET請求方法,則表示到展示頁面,POST請求方法表示真正的功能操作;
getCommandName:
表示是命令對象名字,默認command,對於委託對象實現方式無法改變,因此我們就使用默認的吧。
(2、spring配置文件chapter4-servlet.xml
-
<bean id="userService" class="cn.javass.chapter4.service.UserService"/>
-
<bean name="/user/**" class="cn.javass.chapter4.web.controller.UserController">
-
<property name="userService" ref="userService"/>
-
<property name="createView" value="user/create"/>
-
<property name="updateView" value="user/update"/>
-
<property name="deleteView" value="user/delete"/>
-
<property name="listView" value="user/list"/>
-
<property name="redirectToListView" value="redirect:/user/list"/>
-
<!-- 使用PropertiesMethodNameResolver來解析功能處理方法名 -->
-
<!--property name="methodNameResolver" ref="propertiesMethodNameResolver"/-->
-
</bean>
userService:用戶服務類,實現業務邏輯;
依賴注入:對於邏輯視圖頁面通過依賴注入方式注入,redirectToListView表示增刪改成功後重定向的頁面,防止重複表單提交;
默認使用InternalPathMethodNameResolver解析請求URL到功能方法名。
(3、視圖頁面
(3.1、list頁面(WEB-INF/jsp/user/list.jsp)
-
<a href="${pageContext.request.contextPath}/user/create">用戶新增</a><br/>
-
<table border="1" width="50%">
-
<tr>
-
<th>用戶名</th>
-
<th>真實姓名</th>
-
<th>操作</th>
-
</tr>
-
<c:forEach items="${userList}" var="user">
-
<tr>
-
<td>${user.username }</td>
-
<td>${user.realname }</td>
-
<td>
-
<a href="${pageContext.request.contextPath}/user/update?username=${user.username}">更新</a>
-
|
-
<a href="${pageContext.request.contextPath}/user/delete?username=${user.username}">刪除</a>
-
</td>
-
</tr>
-
</c:forEach>
-
</table>
(3.2、update頁面(WEB-INF/jsp/user/update.jsp)
-
<form action="${pageContext.request.contextPath}/user/update" method="post">
-
用戶名: <input type="text" name="username" value="${command.username}"/><br/>
-
真實姓名:<input type="text" name="realname" value="${command.realname}"/><br/>
-
<input type="submit" value="更新"/>
-
</form>
(4、測試:
默認的InternalPathMethodNameResolver將進行如下解析:
http://localhost:9080/springmvc-chapter4/user/list————>list方法名;
http://localhost:9080/springmvc-chapter4/user/create————>create方法名;
http://localhost:9080/springmvc-chapter4/user/update————>update功能處理方法名;
http://localhost:9080/springmvc-chapter4/user/delete————>delete功能處理方法名。
我們可以將默認的InternalPathMethodNameResolver改爲PropertiesMethodNameResolver:
-
<bean id="propertiesMethodNameResolver"
-
class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
-
<property name="mappings">
-
<props>
-
<prop key="/create">create</prop>
-
<prop key="/update">update</prop>
-
<prop key="/delete">delete</prop>
-
<prop key="/list">list</prop>
-
<prop key="/**">list</prop><!-- 默認的行爲 -->
-
</props>
-
</property>
-
</bean>
-
<bean name="/user/**" class="cn.javass.chapter4.web.controller.UserController">
-
<!—省略其他配置,詳見配置文件-->
-
<!-- 使用PropertiesMethodNameResolver來解析功能處理方法名 -->
-
<property name="methodNameResolver" ref="propertiesMethodNameResolver"/>
-
</bean>
/**表示默認解析到list功能處理方法。
如上配置方式可以很好的工作,但必須繼承MultiActionController,Spring Web MVC提供給我們無需繼承MultiActionController實現方式,即使有委託對象方式,繼續往下看吧。
4.15.7、委託方式實現
(1、控制器UserDelegate
將UserController複製一份,改名爲UserDelegate,並把繼承MultiActionController去掉即可,其他無需改變。
(2、spring配置文件chapter4-servlet.xml
-
<!—委託對象-->
-
<bean id="userDelegate" class="cn.javass.chapter4.web.controller.UserDelegate">
-
<property name="userService" ref="userService"/>
-
<property name="createView" value="user2/create"/>
-
<property name="updateView" value="user2/update"/>
-
<property name="deleteView" value="user2/delete"/>
-
<property name="listView" value="user2/list"/>
-
<property name="redirectToListView" value="redirect:/user2/list"/>
-
</bean>
-
<!—控制器對象-->
-
<bean name="/user2/**"
-
class="org.springframework.web.servlet.mvc.multiaction.MultiActionController">
-
<property name="delegate" ref="userDelegate"/>
-
<property name="methodNameResolver" ref="parameterMethodNameResolver"/>
-
</bean>
delegate:控制器對象通過
delegate屬性指定委託對象,即實際調用delegate委託對象的功能方法。
methodNameResolver:此處我們使用ParameterMethodNameResolver解析器;
-
<!—ParameterMethodNameResolver -->
-
<bean id="parameterMethodNameResolver"
-
class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
-
<!-- 1、根據請求參數名解析功能方法名 -->
-
<property name="methodParamNames" value="create,update,delete"/>
-
<!-- 2、根據請求參數名的值解析功能方法名 -->
-
<property name="paramName" value="action"/>
-
<!-- 3、邏輯方法名到真實方法名的映射 -->
-
<property name="logicalMappings">
-
<props>
-
<prop key="doList">list</prop>
-
</props>
-
</property>
-
<!—4、默認執行的功能處理方法 -->
-
<property name="defaultMethodName" value="list"/>
-
</bean>
1、
methodParamNames:create,update,delete,當請求中有參數名爲這三個的將被映射爲功能方法名,如“<input
type="submit"name="create" value="新增"/>”提交後解析得到的功能方法名爲create;
2、paramName:
當請求中有參數名爲action,則將值映射爲功能方法名,如“<input type="hidden" name="action" value="delete"/>”,提交後解析得到的功能方法名爲delete;
3、logicalMappings:
邏輯功能方法名到真實功能方法名的映射,如:
http://localhost:9080/springmvc-chapter4/user2?action=doList;
首先請求參數“action=doList”,則第二步解析得到邏輯功能方法名爲doList;
本步驟會把doList再轉換爲真實的功能方法名list。
4、defaultMethodName:
以上步驟如果沒有解析到功能處理方法名,默認執行的方法名。
(3、視圖頁面
(3.1、list頁面(WEB-INF/jsp/user2/list.jsp)
-
<a href="${pageContext.request.contextPath}/user2?action=create">用戶新增</a><br/>
-
<table border="1" width="50%">
-
<tr>
-
<th>用戶名</th>
-
<th>真實姓名</th>
-
<th>操作</th>
-
</tr>
-
<c:forEach items="${userList}" var="user">
-
<tr>
-
<td>${user.username }</td>
-
<td>${user.realname }</td>
-
<td>
-
<a href="${pageContext.request.contextPath}/user2?action=update&username=${user.username}">更新</a>
-
|
-
<a href="${pageContext.request.contextPath}/user2?action=delete&username=${user.username}">刪除</a>
-
</td>
-
</tr>
-
</c:forEach>
-
</table>
(3.2、update頁面(WEB-INF/jsp/user2/update.jsp)
-
<form action="${pageContext.request.contextPath}/user2" method="post">
-
<input type="hidden" name="action" value="update"/>
-
用戶名: <input type="text" name="username" value="${command.username}"/><br/>
-
真實姓名:<input type="text" name="realname" value="${command.realname}"/><br/>
-
<input type="submit" value="更新"/>
-
</form>
通過參數
name="action" value="update"來指定要執行的功能方法名update。
(3.3、create頁面(WEB-INF/jsp/user2/create.jsp)
-
<form action="${pageContext.request.contextPath}/user2" method="post">
-
用戶名: <input type="text" name="username" value="${command.username}"/><br/>
-
真實姓名:<input type="text" name="realname" value="${command.realname}"/><br/>
-
<input type="submit" name="create" value="新增"/>
-
</form>
通過參數
name="create"來指定要執行的功能方法名create。
(4、測試:
使用ParameterMethodNameResolver將進行如下解析:
http://localhost:9080/springmvc-chapter4/user2?create ————>create功能處理方法名(參數名映射);
http://localhost:9080/springmvc-chapter4/user2?action=create————>create功能處理方法名(參數值映射);
http://localhost:9080/springmvc-chapter4/user2?update ————>update功能處理方法名;
http://localhost:9080/springmvc-chapter4/user2?action=update————>update功能處理方法名;
http://localhost:9080/springmvc-chapter4/user2?delete ————>delete功能處理方法名;
http://localhost:9080/springmvc-chapter4/user2?action=delete————>delete功能處理方法名;
http://localhost:9080/springmvc-chapter4/user2?doList ————>通過logicalMappings解析爲list功能處理方法。
http://localhost:9080/springmvc-chapter4/user2?action=doList————>通過logicalMappings解析爲list功能處理方法。
http://localhost:9080/springmvc-chapter4/user2————>默認的功能處理方法名list(默認)。
第四章
Controller接口控制器詳解(6)
4.16、數據類型轉換和數據驗證
流程:
1、首先創建數據綁定器,在此此會創建ServletRequestDataBinder類的對象,並設置messageCodesResolver(錯誤碼解析器);
2、提供第一個擴展點,初始化數據綁定器,在此處我們可以覆蓋該方法註冊自定義的PropertyEditor(請求參數——>命令對象屬性的轉換);
3、進行數據綁定,即請求參數——>命令對象的綁定;
4、提供第二個擴展點,數據綁定完成後的擴展點,此處可以實現一些自定義的綁定動作;
5、驗證器對象的驗證,驗證器通過validators注入,如果驗證失敗,需要把錯誤信息放入Errors(此處使用BindException實現);
6、提供第三個擴展點,此處可以實現自定義的綁定/驗證邏輯;
7、將errors傳入功能處理方法進行處理,功能方法應該判斷該錯誤對象是否有錯誤進行相應的處理。
4.16.1、數據類型轉換
請求參數(String)——>命令對象屬性(可能是任意類型)的類型轉換,即數據綁定時的類型轉換,使用PropertyEditor實現綁定時的類型轉換。
一、Spring內建的PropertyEditor如下所示:
類名
|
說明
|
默認是否註冊
|
ByteArrayPropertyEditor
|
String<——>byte[]
|
√
|
ClassEditor
|
String<——>Class
當類沒有發現拋出IllegalArgumentException
|
√
|
CustomBooleanEditor
|
String<——>Boolean
true/yes/on/1轉換爲true,false/no/off/0轉換爲false
|
√
|
CustomCollectionEditor
|
數組/Collection——>Collection
普通值——>Collection(只包含一個對象)
如String——>Collection
不允許Collection——>String(單方向轉換)
|
√
|
CustomNumberEditor
|
String<——>Number(Integer、Long、Double)
|
√
|
FileEditor
|
String<——>File
|
√
|
InputStreamEditor
|
String——>InputStream
單向的,不能InputStream——>String
|
√
|
LocaleEditor
|
String<——>Locale,
(String的形式爲[語言]_[國家]_[變量],這與Local對象的toString()方法得到的結果相同)
|
√
|
PatternEditor
|
String<——>Pattern
|
√
|
PropertiesEditor
|
String<——>java.lang.Properties
|
√
|
URLEditor
|
String<——>URL
|
√
|
StringTrimmerEditor
|
一個用於trim 的 String類型的屬性編輯器
如默認刪除兩邊的空格,charsToDelete屬性:可以設置爲其他字符
emptyAsNull屬性:將一個空字符串轉化爲null值的選項。
|
×
|
CustomDateEditor
|
String<——>java.util.Date
|
×
|
二、Spring內建的PropertyEditor支持的屬性(符合JavaBean規範)操作:
表達式
|
設值/取值說明
|
username
|
屬性username
設值方法setUsername()/取值方法getUsername() 或 isUsername()
|
schooInfo.schoolType
|
屬性schooInfo的嵌套屬性schoolType
設值方法getSchooInfo().setSchoolType()/取值方法getSchooInfo().getSchoolType()
|
hobbyList[0]
|
屬性hobbyList的第一個元素
索引屬性可能是一個數組、列表、其它天然有序的容器。
|
map[key]
|
屬性map(java.util.Map類型)
map中key對應的值
|
三、示例:
接下來我們寫自定義的屬性編輯器進行數據綁定:
(1、模型對象:
java代碼:
-
package cn.javass.chapter4.model;
-
-
public class DataBinderTestModel {
-
private String username;
-
private boolean bool;
-
private SchoolInfoModel schooInfo;
-
private List hobbyList;
-
private Map map;
-
private PhoneNumberModel phoneNumber;
-
private Date date;
-
private UserState state;
-
-
}
-
-
package cn.javass.chapter4.model;
-
-
public class PhoneNumberModel {
-
private String areaCode;
-
private String phoneNumber;
-
-
}
(2、PhoneNumber屬性編輯器
前臺輸入如010-12345678自動轉換爲PhoneNumberModel。
java代碼:
-
package cn.javass.chapter4.web.controller.support.editor;
-
-
public class PhoneNumberEditor extends PropertyEditorSupport {
-
Pattern pattern = Pattern.compile("^(\\d{3,4})-(\\d{7,8})$");
-
@Override
-
public void setAsText(String text) throws IllegalArgumentException {
-
if(text == null || !StringUtils.hasLength(text)) {
-
setValue(null);
-
}
-
Matcher matcher = pattern.matcher(text);
-
if(matcher.matches()) {
-
PhoneNumberModel phoneNumber = new PhoneNumberModel();
-
phoneNumber.setAreaCode(matcher.group(1));
-
phoneNumber.setPhoneNumber(matcher.group(2));
-
setValue(phoneNumber);
-
} else {
-
throw new IllegalArgumentException(String.format("類型轉換失敗,需要格式[010-12345678],但格式是[%s]", text));
-
}
-
}
-
@Override
-
public String getAsText() {
-
PhoneNumberModel phoneNumber = ((PhoneNumberModel)getValue());
-
return phoneNumber == null ? "" : phoneNumber.getAreaCode() + "-" + phoneNumber.getPhoneNumber();
-
}
-
}
PropertyEditorSupport:一個PropertyEditor的支持類;
setAsText:表示將String——>PhoneNumberModel,根據正則表達式進行轉換,如果轉換失敗拋出異常,則接下來的驗證器會進行驗證處理;
getAsText:表示將PhoneNumberModel——>String。
(3、控制器
需要在控制器註冊我們自定義的屬性編輯器。
此處我們使用AbstractCommandController,因爲它繼承了BaseCommandController,擁有綁定流程。
java代碼:
-
package cn.javass.chapter4.web.controller;
-
-
public class DataBinderTestController extends AbstractCommandController {
-
public DataBinderTestController() {
-
setCommandClass(DataBinderTestModel.class);
-
setCommandName("dataBinderTest");
-
}
-
@Override
-
protected ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception {
-
-
System.out.println(command);
-
return new ModelAndView("bindAndValidate/success").addObject("dataBinderTest", command);
-
}
-
@Override
-
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
-
super.initBinder(request, binder);
-
-
-
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-
CustomDateEditor dateEditor = new CustomDateEditor(df, true);
-
-
binder.registerCustomEditor(Date.class, dateEditor);
-
-
binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor());
-
}
-
}
initBinder:第一個擴展點,初始化數據綁定器,在此處我們註冊了兩個屬性編輯器;
CustomDateEditor:自定義的日期編輯器,用於在String<——>日期之間轉換;
binder.registerCustomEditor(Date.class, dateEditor):表示如果命令對象是Date類型,則使用dateEditor進行類型轉換;
PhoneNumberEditor:自定義的電話號碼屬性編輯器用於在String<——> PhoneNumberModel之間轉換;
binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()):表示如果命令對象是PhoneNumberModel類型,則使用PhoneNumberEditor進行類型轉換;
(4、spring配置文件chapter4-servlet.xml
java代碼:
-
<bean name="/dataBind"
-
class="cn.javass.chapter4.web.controller.DataBinderTestController"/>
(5、視圖頁面(WEB-INF/jsp/bindAndValidate/success.jsp)
java代碼:
-
EL phoneNumber:${dataBinderTest.phoneNumber}<br/>
-
EL state:${dataBinderTest.state}<br/>
-
EL date:${dataBinderTest.date}<br/>
視圖頁面的數據沒有預期被格式化,如何進行格式化顯示呢?請參考【第七章 註解式控制器的數據驗證、類型轉換及格式化】。
(6、測試:
1、在瀏覽器地址欄輸入請求的URL,如
http://localhost:9080/springmvc-chapter4/dataBind?username=zhang&bool=yes&schooInfo.specialty=computer&hobbyList[0]=program&hobbyList[1]=music&map[key1]=value1&map[key2]=value2&phoneNumber=010-12345678&date=2012-3-18 16:48:48&state=blocked
2、控制器輸出的內容:
DataBinderTestModel [username=zhang, bool=true, schooInfo=SchoolInfoModel [schoolType=null, schoolName=null, specialty=computer], hobbyList=[program, music], map={key1=value1, key2=value2}, phoneNumber=PhoneNumberModel [areaCode=010, phoneNumber=12345678],
date=Sun Mar 18 16:48:48 CST 2012, state=鎖定]
類型轉換如圖所示:
四、註冊PropertyEditor
1、使用WebDataBinder進行控制器級別註冊PropertyEditor(控制器獨享)
如“【三、示例】”中所使用的方式,使用WebDataBinder註冊控制器級別的PropertyEditor,這種方式註冊的PropertyEditor只對當前控制器獨享,即其他的控制器不會自動註冊這個PropertyEditor,如果需要還需要再註冊一下。
2、使用WebBindingInitializer批量註冊
PropertyEditor
如果想在多個控制器同時註冊多個相同的PropertyEditor時,可以考慮使用WebBindingInitializer。
示例:
(1、實現WebBindingInitializer
java代碼:
-
package cn.javass.chapter4.web.controller.support.initializer;
-
-
public class MyWebBindingInitializer implements WebBindingInitializer {
-
@Override
-
public void initBinder(WebDataBinder binder, WebRequest request) {
-
-
-
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-
CustomDateEditor dateEditor = new CustomDateEditor(df, true);
-
-
binder.registerCustomEditor(Date.class, dateEditor);
-
-
binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor());
-
}
-
}
通過實現WebBindingInitializer並通過binder註冊多個PropertyEditor。
(2、修改【三、示例】中的DataBinderTestController,註釋掉initBinder方法;
(3、修改chapter4-servlet.xml配置文件:
java代碼:
-
<!-- 註冊WebBindingInitializer實現 -->
-
<bean id="myWebBindingInitializer" class="cn.javass.chapter4.web.controller.support.initializer.MyWebBindingInitializer"/>
-
<bean name="/dataBind" class="cn.javass.chapter4.web.controller.DataBinderTestController">
-
<!-- 注入WebBindingInitializer實現 -->
-
<property name="webBindingInitializer" ref="myWebBindingInitializer"/>
-
</bean>
(4、嘗試訪問“【三、示例】”中的測試URL即可成功。
使用WebBindingInitializer的好處是當你需要在多個控制器中需要同時使用多個相同的PropertyEditor可以在WebBindingInitializer實現中註冊,這樣只需要在控制器中注入WebBindingInitializer即可注入多個PropertyEditor。
3、全局級別註冊PropertyEditor(全局共享)
只需要將我們自定義的PropertyEditor放在和你的模型類同包下即可,且你的Editor命名規則必須是“模型類名Editor”,這樣Spring會自動使用標準JavaBean架構進行自動識別,如圖所示:
此時我們把“DataBinderTestController”的“binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor());”註釋掉,再嘗試訪問“【三、示例】”中的測試URL即可成功。
這種方式不僅僅在使用Spring時可用,在標準的JavaBean等環境都是可用的,可以認爲是全局共享的(不僅僅是Spring環境)。
PropertyEditor被限制爲只能String<——>Object之間轉換,不能Object<——>Object,Spring3提供了更強大的類型轉換(Type Conversion)支持,它可以在任意對象之間進行類型轉換,不僅僅是String
<——>Object。
如果我在地址欄輸入錯誤的數據,即數據綁定失敗,Spring Web MVC該如何處理呢?如果我輸入的數據不合法呢?如用戶名輸入100個字符(超長了)那又該怎麼處理呢?出錯了需要錯誤消息,那錯誤消息應該是硬編碼?還是可配置呢?
接下來我們來學習一下數據驗證器進行數據驗證吧。
第四章
Controller接口控制器詳解(7)
4.16.2、數據驗證
1、數據綁定失敗:比如需要數字卻輸入了字母;
2、數據不合法:可以認爲是業務錯誤,通過自定義驗證器驗證,如用戶名長度必須在5-20之間,我們卻輸入了100個字符等;
3、錯誤對象:當我們數據綁定失敗或驗證失敗後,錯誤信息存放的對象,我們叫錯誤對象,在Spring Web MVC中Errors是具體的代表者;線程不安全對象;
4、錯誤消息:是硬編碼,還是可配置?實際工作應該使用配置方式,我們只是把錯誤碼(errorCode)放入錯誤對象,在展示時讀取相應的錯誤消息配置文件來獲取要顯示的錯誤消息(errorMessage);
4.16.2.1、驗證流程
1、首先進行數據綁定驗證,如果驗證失敗會通過MessageCodesResolver生成錯誤碼放入Errors錯誤對象;
2、數據不合法驗證,通過自定義的驗證器驗證,如果失敗需要手動將錯誤碼放入Errors錯誤對象;
4.16.2.2、錯誤對象和錯誤消息
錯誤對象的代表者是Errors接口,並且提供了幾個實現者,在Spring Web MVC中我們使用的是如下實現:
相關的錯誤方法如下:
Errors:存儲和暴露關於數據綁定錯誤和驗證錯誤相關信息的接口,提供了相關存儲和獲取錯誤消息的方法:
java代碼:
-
package org.springframework.validation;
-
public interface Errors {
-
-
-
void reject(String errorCode);
-
-
void reject(String errorCode, String defaultMessage);
-
-
void reject(String errorCode, Object[] errorArgs, String defaultMessage);
-
-
-
-
void rejectValue(String field, String errorCode);
-
void rejectValue(String field, String errorCode, String defaultMessage);
-
void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage);
-
-
boolean hasErrors();
-
boolean hasGlobalErrors();
-
boolean hasFieldErrors();
-
Object getFieldValue(String field);
-
}
getFieldValue:可以得到驗證失敗的失敗值,這是其他Web層框架很少支持的,這樣就可以給用戶展示出錯時的值(而不是空或其他的默認值等)。
BindingResult:代表數據綁定的結果,繼承了Errors接口。
BindException:代表數據綁定的異常,它繼承Exception,並實現了BindingResult,這是內部使用的錯誤對象。
示例:
(1、控制器
java代碼:
-
package cn.javass.chapter4.web.controller;
-
-
public class ErrorController extends AbstractCommandController {
-
public ErrorController() {
-
setCommandClass(DataBinderTestModel.class);
-
setCommandName("command");
-
}
-
@Override
-
protected ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception {
-
-
errors.reject("username.not.empty");
-
-
errors.reject("username.not.empty1", "用戶名不能爲空1");
-
-
errors.reject("username.length.error", new Object[]{5, 10});
-
-
-
Map model = errors.getModel();
-
return new ModelAndView("bindAndValidate/error", model);
-
}
-
}
errors.reject("username.not.empty"):註冊全局錯誤碼“username.not.empty”,我們必須提供messageSource來提供錯誤碼“username.not.empty”對應的錯誤信息(如果沒有會拋出NoSuchMessageException異常);
errors.reject("username.not.empty1", "用戶名不能爲空1"):註冊全局錯誤碼“username.not.empty1”,如果從messageSource沒沒有找到錯誤碼“username.not.empty1”對應的錯誤信息,則將顯示默認消息“用戶名不能爲空1”;
errors.reject("username.length.error", new Object[]{5, 10}):錯誤碼爲“username.length.error”,而且錯誤信息需要兩個參數,如我們在我們的配置文件中定義“用戶名長度不合法,長度必須在{0}到{1}之間”,則實際的錯誤消息爲“用戶名長度不合法,長度必須在5到10之間”
errors.getModel():當有錯誤信息時,一定將errors.getModel()放入我們要返回的ModelAndView中,以便使用裏邊的錯誤對象來顯示錯誤信息。
(2、spring配置文件chapter4-servlet.xml
java代碼:
-
<bean id="messageSource"
-
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
-
<property name="basename" value="classpath:messages"/>
-
<property name="fileEncodings" value="utf-8"/>
-
<property name="cacheSeconds" value="120"/>
-
</bean>
-
-
<bean name="/error" class="cn.javass.chapter4.web.controller.ErrorController"/>
messageSource:用於獲取錯誤碼對應的錯誤消息的,而且bean名字默認必須是messageSource。
messages.properties(需要執行NativeToAscii)
java代碼:
-
username.not.empty=用戶名不能爲空
-
username.length.error=用戶名長度不合法,長度必須在{0}到{1}之間
(3、視圖頁面(WEB-INF/jsp/bindAndValidate/error.jsp)
java代碼:
-
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
-
<!-- 表單的默認命令對象名爲command -->
-
<form:form commandName="command">
-
<form:errors path="*"></form:errors>
-
</form:form>
form標籤庫:此處我們使用了spring的form標籤庫;
<form:form commandName="command">:表示我們的表單標籤,commandName表示綁定的命令對象名字,默認爲command;
<form:errors path="*"></form:errors>:表示顯示錯誤信息的標籤,如果path爲“*”表示顯示所有錯誤信息。
接下來我們來看一下 數據綁定失敗和數據不合法時,如何處理。
4.16.2.3、數據綁定失敗
如我們的DataBinderTestModel類:
bool:boolean類型,此時如果我們前臺傳入非兼容的數據,則會數據綁定失敗;
date:Date類型,此時如果我們前臺傳入非兼容的數據,同樣會數據綁定失敗;
phoneNumber:自定義的PhoneNumberModel類型,如果如果我們前臺傳入非兼容的數據,同樣會數據綁定失敗。
示例:
(1、控制器,DataBinderErrorTestController。
java代碼:
-
package cn.javass.chapter4.web.controller;
-
-
public class DataBinderErrorTestController extends SimpleFormController {
-
public DataBinderErrorTestController() {
-
setCommandClass(DataBinderTestModel.class);
-
setCommandName("dataBinderTest");
-
}
-
@Override
-
protected ModelAndView showForm(HttpServletRequest request, HttpServletResponse response, BindException errors) throws Exception {
-
-
System.out.println(errors);
-
return super.showForm(request, response, errors);
-
}
-
@Override
-
protected void doSubmitAction(Object command) throws Exception {
-
System.out.println(command);
-
}
-
@Override
-
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
-
super.initBinder(request, binder);
-
-
-
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-
CustomDateEditor dateEditor = new CustomDateEditor(df, true);
-
-
binder.registerCustomEditor(Date.class, dateEditor);
-
-
-
binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor());
-
}
-
}
此處我們使用SimpleFormController;
showForm:展示表單,當提交表單有任何數據綁定錯誤會再回到該方法進行表單輸入(在此處我們打印錯誤對象);
doSubmitAction:表單提交成功,只要當表單的數據到命令對象綁定成功時,纔會執行;
(2、spring配置文件chapter4-servlet.xml
java代碼:
-
<bean name="/dataBindError"
-
class="cn.javass.chapter4.web.controller.DataBinderErrorTestController">
-
<property name="formView" value="bindAndValidate/input"/>
-
<property name="successView" value="bindAndValidate/success"/>
-
</bean>
(3、視圖頁面(WEB-INF/jsp/bindAndValidate/ input.jsp)
java代碼:
-
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
-
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
-
<!-- 表單的命令對象名爲dataBinderTest -->
-
<form:form commandName="dataBinderTest">
-
<form:errors path="*" cssStyle="color:red"></form:errors><br/><br/>
-
bool:<form:input path="bool"/><br/>
-
phoneNumber:<form:input path="phoneNumber"/><br/>
-
date:<form:input path="date"/><br/>
-
<input type="submit" value="提交"/>
-
</form:form>
此處一定要使用form標籤庫,藉此我們可以看到它的強大支持(別的大部分Web框架所不具備的,展示用戶驗證失敗的數據)。
<form:form commandName="dataBinderTest">:指定命令對象爲dataBinderTest,默認command;
<form:errors path="*" cssStyle="color:red"></form:errors>:顯示錯誤消息,當提交表單有錯誤時展示錯誤消息(數據綁定錯誤/數據不合法);
<form:input path="bool"/>:等價於(<input type=’text’>),但會從命令對象中取出bool屬性進行填充value屬性,或如果表單提交有錯誤會從錯誤對象取出之前的錯誤數據(而非空或默認值);
<input type="submit" value="提交"/>:spring沒有提供相應的提交按鈕,因此需要使用html的。
(4、測試
在地址欄輸入如下地址:http://localhost:9080/springmvc-chapter4/dataBindError
全部是錯誤數據,即不能綁定到我們的命令對象;
當提交表單後,我們又回到表單輸入頁面,而且輸出了一堆錯誤信息
1、錯誤消息不可讀;
2、表單元素可以顯示之前的錯誤的數據,而不是默認值/空;
(5、問題
這裏最大的問題是不可讀的錯誤消息,如何讓這些錯誤消息可讀呢?
首先我們看我們的showForm方法裏輸出的“errors”錯誤對象信息:
java代碼:
-
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 3 errors
-
-
Field error in object 'dataBinderTest' on field 'bool': rejected value [www]; codes [typeMismatch.dataBinderTest.bool,typeMismatch.bool,typeMismatch.boolean,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dataBinderTest.bool,bool]; arguments []; default message [bool]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'boolean' for property 'bool'; nested exception is java.lang.IllegalArgumentException: Invalid boolean value [www]]
-
-
Field error in object 'dataBinderTest' on field 'date': rejected value [123]; codes [typeMismatch.dataBinderTest.date,typeMismatch.date,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dataBinderTest.date,date]; arguments []; default message [date]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'date'; nested exception is java.lang.IllegalArgumentException: Could not parse date: Unparseable date: "123"]
-
-
Field error in object 'dataBinderTest' on field 'phoneNumber': rejected value [123]; codes [typeMismatch.dataBinderTest.phoneNumber,typeMismatch.phoneNumber,typeMismatch.cn.javass.chapter4.model.PhoneNumberModel,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dataBinderTest.phoneNumber,phoneNumber]; arguments []; default message [phoneNumber]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'cn.javass.chapter4.model.PhoneNumberModel' for property 'phoneNumber'; nested exception is java.lang.IllegalArgumentException: 類型轉換失敗,需要格式[010-12345678],但格式是[123]]
-
數據綁定失敗(類型不匹配)會自動生成如下錯誤碼(錯誤碼對應的錯誤消息按照如下順序依次查找):
1、typeMismatch.命令對象名.屬性名
2、typeMismatch.屬性名
3、typeMismatch.屬性全限定類名(包名.類名)
4、typeMismatch
⊙內部使用MessageCodesResolver解析數據綁定錯誤到錯誤碼,默認DefaultMessageCodesResolver,因此想要詳細瞭解如何解析請看其javadoc;
⊙建議使用第1個進行錯誤碼的配置。
因此修改我們的messages.properties添加如下錯誤消息(需要執行NativeToAscii):
java代碼:
-
typeMismatch.dataBinderTest.date=您輸入的數據格式錯誤,請重新輸入(格式:2012-03-19 22:17:17)
-
#typeMismatch.date=2
-
#typeMismatch.java.util.Date=3
-
#typeMismatch=4
再次提交表單我們會看到我們設置的錯誤消息:
到此,數據綁定錯誤我們介紹完了,接下來我們再看一下數據不合法錯誤。
4.16.2.4、數據不合法
1、比如用戶名長度必須在5-20之間,而且必須以字母開頭,可包含字母、數字、下劃線;
2、比如註冊用戶時 用戶名已經存在或郵箱已經存在等;
3、比如去一些論壇經常會發現,您發的帖子中包含×××屏蔽關鍵字等。
還有很多數據不合法的場景,在此就不羅列了,對於數據不合法,Spring Web MVC提供了兩種驗證方式:
◆編程式驗證器驗證
◆聲明式驗證
先從編程式驗證器開始吧。
4.16.2.4.1、編程式驗證器
一、驗證器接口
java代碼:
-
package org.springframework.validation;
-
public interface Validator {
-
boolean supports(Class<?> clazz);
-
void validate(Object target, Errors errors);
-
}
Validator接口:驗證器,編程實現數據驗證的接口;
supports方法:當前驗證器是否支持指定的clazz驗證,如果支持返回true即可;
validate方法:驗證的具體方法,target參數表示要驗證的目標對象(如命令對象),errors表示驗證出錯後存放錯誤信息的錯誤對象。
示例:
(1、驗證器實現
java代碼:
-
package cn.javass.chapter4.web.controller.support.validator;
-
-
public class UserModelValidator implements Validator {
-
private static final Pattern USERNAME_PATTERN = Pattern.compile("[a-zA-Z]\\w{4,19}");
-
private static final Pattern PASSWORD_PATTERN = Pattern.compile("[a-zA-Z0-9]{5,20}");
-
private static final Set<String> FORBINDDDEN_WORD_SET = new HashSet<String>();
-
static {
-
FORBINDDDEN_WORD_SET.add("fuc k");
-
FORBINDDDEN_WORD_SET.add("admin");
-
}
-
@Override
-
public boolean supports(Class<?> clazz) {
-
return UserModel.class == clazz;
-
}
-
@Override
-
public void validate(Object target, Errors errors) {
-
-
ValidationUtils.rejectIfEmpty(errors, "username", "username.not.empty");
-
-
UserModel user = (UserModel) target;
-
-
if(!USERNAME_PATTERN.matcher(user.getUsername()).matches()) {
-
errors.rejectValue("username", "username.not.illegal");
-
}
-
-
for(String forbiddenWord : FORBINDDDEN_WORD_SET) {
-
if(user.getUsername().contains(forbiddenWord)) {
-
errors.rejectValue("username", "username.forbidden", new Object[]{forbiddenWord}, "您的用戶名包含非法關鍵詞");
-
break;
-
}
-
}
-
if(!PASSWORD_PATTERN.matcher(user.getPassword()).matches()) {
-
errors.rejectValue("password","password.not.illegal", "密碼不合法");
-
}
-
}
-
}
-
supports方法:表示只對UserModel類型的對象驗證;
validate方法:數據驗證的具體方法,有如下幾個驗證:
1、用戶名不合法(長度5-20,以字母開頭,隨後可以是字母、數字、下劃線)
USERNAME_PATTERN.matcher(user.getUsername()).matches() //使用正則表達式驗證
errors.rejectValue("username", "username.not.illegal");//驗證失敗爲username字段添加錯誤碼
2、屏蔽關鍵詞:即用戶名中含有不合法的數據(如admin)
user.getUsername().contains(forbiddenWord) //用contains來判斷我們的用戶名中是否含有非法關鍵詞
errors.rejectValue("username", "username.forbidden", new Object[]{forbiddenWord}, "您的用戶名包含非法關鍵詞");//驗證失敗爲username字段添加錯誤碼(參數爲當前屏蔽關鍵詞)(默認消息爲"您的用戶名包含非法關鍵詞")
3、密碼不合法
在此就不羅列代碼了;
4、ValidationUtils
ValidationUtils.rejectIfEmpty(errors, "username", "username.not.empty");
表示如果目標對象的username屬性數據爲空,則添加它的錯誤碼;
內部通過(value == null || !StringUtils.hasLength(value.toString()))實現判斷value是否爲空,從而簡化代碼。
(2、spring配置文件chapter4-servlet.xml
java代碼:
-
<bean id="userModelValidator"
-
class="cn.javass.chapter4.web.controller.support.validator.UserModelValidator"/>
-
<bean name="/validator"
-
class="cn.javass.chapter4.web.controller.RegisterSimpleFormController">
-
<property name="formView" value="registerAndValidator"/>
-
<property name="successView" value="redirect:/success"/>
-
<property name="validator" ref="userModelValidator"/>
-
</bean>
此處使用了我們第4.9節創建的RegisterSimpleFormController。
(3、錯誤碼配置(messages.properties),需要執行NativeToAscii
java代碼:
-
username.not.empty=用戶名不能爲空
-
username.not.illegal=用戶名錯誤,必須以字母開頭,只能出現字母、數字、下劃線,並且長度在5-20之間
-
username.forbidden=用戶名中包含非法關鍵詞【{0}】
-
password.not.illegal=密碼長度必須在5-20之間
(4、視圖頁面(/WEB-INF/jsp/registerAndValidator.jsp)
java代碼:
-
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
-
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
-
<form:form commandName="user">
-
-
<form:errors path="*" cssStyle="color:red"></form:errors><br/>
-
-
username:<form:input path="username"/>
-
<form:errors path="username" cssStyle="color:red"></form:errors>
-
<br/>
-
-
password:<form:password path="password"/>
-
<form:errors path="password" cssStyle="color:red"></form:errors>
-
<br/>
-
<input type="submit" value="註冊"/>
-
</form:form>
-
form:errors path="username":表示只顯示username字段的錯誤信息;
(5、測試
地址:http://localhost:9080/springmvc-chapter4/validator
當我們輸入錯誤的數據後,會報錯(form:errors path="*"顯示所有錯誤信息,而form:errors path="username"只顯示該字段相關的)。
問題:
如MultiActionController控制器相關方法沒有提供給我們errors對象(Errors),我們應該怎麼進行錯誤處理呢?
此處給大家一個思路,errors本質就是一個Errors接口實現,而且在頁面要讀取相關的錯誤對象,該錯誤對象應該存放在模型對象裏邊,因此我們可以自己創建個errors對象並將其添加到模型對象中即可。
此處我們複製4.15節的UserController類爲UserAndValidatorController,並修改它的create(新增)方法添加如下代碼片段:
java代碼:
-
BindException errors = new BindException(user, getCommandName(user));
-
-
if(!StringUtils.hasLength(user.getUsername())) {
-
errors.rejectValue("username", "username.not.empty");
-
}
-
if(errors.hasErrors()) {
-
return new ModelAndView(getCreateView()).addAllObjects(errors.getModel());
-
}
-
√ new BindException(user, getCommandName(user)):使用當前的命令對象,和命令對象的名字創建了一個BindException作爲errors;
√StringUtils.hasLength(user.getUsername()):如果用戶名爲空就是用errors.rejectValue("username", "username.not.empty");注入錯誤碼;
√errors.hasErrors():表示如果有錯誤就返回到新增頁面並顯示錯誤消息;
√ModelAndView(getCreateView()).addAllObjects(errors.getModel()):此處一定把errors對象的模型數據放在當前的ModelAndView中,作爲當前請求的模型數據返回。
在瀏覽器地址欄輸入:http://localhost:9080/springmvc-chapter4/userAndValidator/create 到新增頁面
用戶名什麼都不輸入,提交後又返回到新增頁面 而且顯示了錯誤消息說明我們的想法是正確的。
4.16.2.4.2、聲明式驗證器
從Spring3開始支持JSR-303驗證框架,支持XML風格和註解風格的驗證,目前在@RequestMapping時才能使用,也就是說基於Controller接口的實現不能使用該方式(但可以使用編程式驗證,有需要的可以參考hibernate validator實現),我們將在第七章詳細介紹。
到此Spring2風格的控制器我們就介紹完了,以上控制器從spring3.0開始已經不推薦使用了(但考慮到還有部分公司使用這些@Deprecated類,在此也介紹了一下),而是使用註解控制器實現(@Controller和@RequestMapping)。
第五章
處理器攔截器詳解
5.1、處理器攔截器簡介
Spring Web MVC的處理器攔截器(如無特殊說明,下文所說的攔截器即處理器攔截器)類似於Servlet開發中的過濾器Filter,用於對處理器進行預處理和後處理。
5.1.1、常見應用場景
1、日誌記錄:
記錄請求信息的日誌,以便進行信息監控、信息統計、計算PV(Page View)等。
2、權限檢查:
如登錄檢測,進入處理器檢測檢測是否登錄,如果沒有直接返回到登錄頁面;
3、性能監控:
有時候系統在某段時間莫名其妙的慢,可以通過攔截器在進入處理器之前記錄開始時間,在處理完後記錄結束時間,從而得到該請求的處理時間(如果有反向代理,如apache可以自動記錄);
4、通用行爲:
讀取cookie得到用戶信息並將用戶對象放入請求,從而方便後續流程使用,還有如提取Locale、Theme信息等,只要是多個處理器都需要的即可使用攔截器實現。
5、OpenSessionInView:
如Hibernate,在進入處理器打開Session,在完成後關閉Session。
…………本質也是AOP(面向切面編程),也就是說符合橫切關注點的所有功能都可以放入攔截器實現。
5.1.2、攔截器接口
java代碼:
-
package org.springframework.web.servlet;
-
public interface HandlerInterceptor {
-
boolean preHandle(
-
HttpServletRequest request, HttpServletResponse response,
-
Object handler)
-
throws Exception;
-
-
void postHandle(
-
HttpServletRequest request, HttpServletResponse response,
-
Object handler, ModelAndView modelAndView)
-
throws Exception;
-
-
void afterCompletion(
-
HttpServletRequest request, HttpServletResponse response,
-
Object handler, Exception ex)
-
throws Exception;
-
}
我們可能注意到攔截器一個有3個回調方法,而一般的過濾器Filter才兩個,這是怎麼回事呢?馬上分析。
preHandle:預處理回調方法,實現處理器的預處理(如登錄檢查),第三個參數爲響應的處理器(如我們上一章的Controller實現);
返回值:true表示繼續流程(如調用下一個攔截器或處理器);
false表示流程中斷(如登錄檢查失敗),不會繼續調用其他的攔截器或處理器,此時我們需要通過response來產生響應;
postHandle:後處理回調方法,實現處理器的後處理(但在渲染視圖之前),此時我們可以通過modelAndView(模型和視圖對象)對模型數據進行處理或對視圖進行處理,modelAndView也可能爲null。
afterCompletion:整個請求處理完畢回調方法,即在視圖渲染完畢時回調,如性能監控中我們可以在此記錄結束時間並輸出消耗時間,還可以進行一些資源清理,類似於try-catch-finally中的finally,但僅調用處理器執行鏈中preHandle返回true的攔截器的afterCompletion。
5.1.3、攔截器適配器
有時候我們可能只需要實現三個回調方法中的某一個,如果實現
HandlerInterceptor接口的話,三個方法必須實現,不管你需不需要,此時spring提供了一個HandlerInterceptorAdapter適配器(一種適配器設計模式的實現),允許我們只實現需要的回調方法。
java代碼:
-
public abstract class HandlerInterceptorAdapter implements HandlerInterceptor {
-
-
}
5.1.4、運行流程圖
圖5-1 正常流程
圖5-2 中斷流程
中斷流程中,比如是HandlerInterceptor2中斷的流程(preHandle返回false),此處僅調用它之前攔截器的preHandle返回true的afterCompletion方法。
接下來看一下DispatcherServlet內部到底是如何工作的吧:
java代碼:
-
-
-
HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();
-
if (interceptors != null) {
-
for (int i = 0; i < interceptors.length; i++) {
-
HandlerInterceptor interceptor = interceptors[i];
-
if (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) {
-
-
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);
-
return;
-
}
-
interceptorIndex = i;
-
}
-
}
-
-
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
-
-
if (mv != null && !mv.hasView()) {
-
mv.setViewName(getDefaultViewName(request));
-
}
-
-
if (interceptors != null) {
-
for (int i = interceptors.length - 1; i >= 0; i--) {
-
HandlerInterceptor interceptor = interceptors[i];
-
interceptor.postHandle(processedRequest, response, mappedHandler.getHandler(), mv);
-
}
-
}
-
-
if (mv != null && !mv.wasCleared()) {
-
render(mv, processedRequest, response);
-
if (errorView) {
-
WebUtils.clearErrorRequestAttributes(request);
-
}
-
-
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);
注:以上是流程的簡化代碼,中間省略了部分代碼,不完整。
java代碼:
-
-
private void triggerAfterCompletion(HandlerExecutionChain mappedHandler, int interceptorIndex,
-
HttpServletRequest request, HttpServletResponse response, Exception ex) throws Exception {
-
-
if (mappedHandler != null) {
-
HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();
-
if (interceptors != null) {
-
for (int i = interceptorIndex; i >= 0; i--) {
-
HandlerInterceptor interceptor = interceptors[i];
-
try {
-
interceptor.afterCompletion(request, response, mappedHandler.getHandler(), ex);
-
}
-
catch (Throwable ex2) {
-
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
-
}
-
}
-
}
-
}
-
}
5.2
、入門
具體內容詳見工程springmvc-chapter5。
5.2.1、正常流程
(1、攔截器實現
java代碼:
-
package cn.javass.chapter5.web.interceptor;
-
-
public class HandlerInterceptor1 extends HandlerInterceptorAdapter {
-
@Override
-
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
-
System.out.println("===========HandlerInterceptor1 preHandle");
-
return true;
-
}
-
@Override
-
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
-
System.out.println("===========HandlerInterceptor1 postHandle");
-
}
-
@Override
-
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
-
System.out.println("===========HandlerInterceptor1 afterCompletion");
-
}
-
}
以上是HandlerInterceptor1實現,HandlerInterceptor2同理 只是輸出內容爲“HandlerInterceptor2”。
(2、控制器
java代碼:
-
package cn.javass.chapter5.web.controller;
-
-
public class TestController implements Controller {
-
@Override
-
public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception {
-
System.out.println("===========TestController");
-
return new ModelAndView("test");
-
}
-
}
(3、Spring配置文件chapter5-servlet.xml
java代碼:
-
<bean name="/test" class="cn.javass.chapter5.web.controller.TestController"/>
-
<bean id="handlerInterceptor1"
-
class="cn.javass.chapter5.web.interceptor.HandlerInterceptor1"/>
-
<bean id="handlerInterceptor2"
-
class="cn.javass.chapter5.web.interceptor.HandlerInterceptor2"/>
-
java代碼:
-
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
-
<property name="interceptors">
-
<list>
-
<ref bean="handlerInterceptor1"/>
-
<ref bean="handlerInterceptor2"/>
-
</list>
-
</property>
-
</bean>
interceptors:指定攔截器鏈,攔截器的執行順序就是此處添加攔截器的順序;
(4、視圖頁面WEB-INF/jsp/test.jsp
java代碼:
-
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
-
<%System.out.println("==========test.jsp");%>
-
test page
在控制檯輸出 test.jsp
(5、啓動服務器測試
輸入網址:http://localhost:9080/springmvc-chapter5/test
控制檯輸出:
java代碼:
-
===========HandlerInterceptor1 preHandle
-
===========HandlerInterceptor2 preHandle
-
===========TestController
-
===========HandlerInterceptor2 postHandle
-
===========HandlerInterceptor1 postHandle
-
==========test.jsp
-
===========HandlerInterceptor2 afterCompletion
-
===========HandlerInterceptor1 afterCompletion
到此一個正常流程的演示完畢。和圖5-1一樣,接下來看一下中斷的流程。
5.2.2、中斷流程
(1、攔截器
HandlerInterceptor3和HandlerInterceptor4 與 之前的 HandlerInteceptor1和HandlerInterceptor2一樣,只是在HandlerInterceptor4的preHandle方法返回false:
java代碼:
-
@Override
-
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
-
System.out.println("===========HandlerInterceptor1 preHandle");
-
onse.getWriter().print("break");
-
return false;
(2、控制器
流程中斷不會執行到控制器,使用之前的TestController控制器。
(3、Spring配置文件chapter5-servlet.xml
java代碼:
-
<bean id="handlerInterceptor3"
-
class="cn.javass.chapter5.web.interceptor.HandlerInterceptor3"/>
-
<bean id="handlerInterceptor4"
-
class="cn.javass.chapter5.web.interceptor.HandlerInterceptor4"/>
-
java代碼:
-
<bean id="handlerInterceptor3"
-
class="cn.javass.chapter5.web.interceptor.HandlerInterceptor3"/>
-
<bean id="handlerInterceptor4"
-
class="cn.javass.chapter5.web.interceptor.HandlerInterceptor4"/>
-
java代碼:
-
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
-
<property name="interceptors">
-
<list>
-
<ref bean="handlerInterceptor3"/>
-
<ref bean="handlerInterceptor4"/>
-
</list>
-
</property>
-
</bean>
interceptors:指定攔截器鏈,攔截器的執行順序就是此處添加攔截器的順序;
(4、視圖頁面
流程中斷,不會執行到視圖渲染。
(5、啓動服務器測試
輸入網址:http://localhost:9080/springmvc-chapter5/test
控制檯輸出:
java代碼:
-
===========HandlerInterceptor3 preHandle
-
===========HandlerInterceptor4 preHandle
-
===========HandlerInterceptor3 afterCompletion
此處我們可以看到只有HandlerInterceptor3的afterCompletion執行,否和圖5-2的中斷流程。
而且頁面上會顯示我們在HandlerInterceptor4 preHandle 直接寫出的響應“break”。
5.3、應用
5.3.1、性能監控
如記錄一下請求的處理時間,得到一些慢請求(如處理時間超過500毫秒),從而進行性能改進,一般的反向代理服務器如apache都具有這個功能,但此處我們演示一下使用攔截器怎麼實現。
實現分析:
1、在進入處理器之前記錄開始時間,即在攔截器的preHandle記錄開始時間;
2、在結束請求處理之後記錄結束時間,即在攔截器的afterCompletion記錄結束實現,並用結束時間-開始時間得到這次請求的處理時間。
問題:
我們的攔截器是單例,因此不管用戶請求多少次都只有一個攔截器實現,即線程不安全,那我們應該怎麼記錄時間呢?
解決方案是使用ThreadLocal,它是線程綁定的變量,提供線程局部變量(一個線程一個ThreadLocal,A線程的ThreadLocal只能看到A線程的ThreadLocal,不能看到B線程的ThreadLocal)。
代碼實現:
java代碼:
-
package cn.javass.chapter5.web.interceptor;
-
public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter {
-
private NamedThreadLocal<Long> startTimeThreadLocal =
-
new NamedThreadLocal<Long>("StopWatch-StartTime");
-
@Override
-
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
-
Object handler) throws Exception {
-
long beginTime = System.currentTimeMillis();
-
startTimeThreadLocal.set(beginTime);
-
return true;
-
}
-
-
@Override
-
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
-
Object handler, Exception ex) throws Exception {
-
long endTime = System.currentTimeMillis();
-
long beginTime = startTimeThreadLocal.get();
-
long consumeTime = endTime - beginTime;
-
if(consumeTime > 500) {
-
-
System.out.println(
-
String.format("%s consume %d millis", request.getRequestURI(), consumeTime));
-
}
-
}
-
}
NamedThreadLocal:Spring提供的一個命名的ThreadLocal實現。
在測試時需要把stopWatchHandlerInterceptor放在攔截器鏈的第一個,這樣得到的時間纔是比較準確的。
5.3.2、登錄檢測
在訪問某些資源時(如訂單頁面),需要用戶登錄後才能查看,因此需要進行登錄檢測。
流程:
1、訪問需要登錄的資源時,由攔截器重定向到登錄頁面;
2、如果訪問的是登錄頁面,攔截器不應該攔截;
3、用戶登錄成功後,往cookie/session添加登錄成功的標識(如用戶編號);
4、下次請求時,攔截器通過判斷cookie/session中是否有該標識來決定繼續流程還是到登錄頁面;
5、在此攔截器還應該允許遊客訪問的資源。
攔截器代碼如下所示:
java代碼:
-
@Override
-
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
-
Object handler) throws Exception {
-
-
if(request.getServletPath().startsWith(loginUrl)) {
-
return true;
-
}
-
-
-
-
-
if(request.getSession().getAttribute("username") != null) {
-
-
return true;
-
}
-
-
-
-
response.sendRedirect(request.getContextPath() + loginUrl);
-
return false;
-
}
提示:推薦能使用servlet規範中的過濾器Filter實現的功能就用Filter實現,因爲HandlerInteceptor只有在Spring Web MVC環境下才能使用,因此Filter是最通用的、最先應該使用的。如登錄這種攔截器最好使用Filter來實現。
第六章
註解式控制器詳解——註解式控制器運行流程及處理器定義
6.1、註解式控制器簡介
一、Spring2.5之前,我們都是通過實現Controller接口或其實現來定義我們的處理器類。已經@Deprecated。
二、Spring2.5引入註解式處理器支持,通過@Controller 和 @RequestMapping註解定義我們的處理器類。並且提供了一組強大的註解:
需要通過處理器映射DefaultAnnotationHandlerMapping和處理器適配器AnnotationMethodHandlerAdapter來開啓支持@Controller 和 @RequestMapping註解的處理器。
@Controller:
用於標識是處理器類;
@RequestMapping:
請求到處理器功能方法的映射規則;
@RequestParam:
請求參數到處理器功能處理方法的方法參數上的綁定;
@ModelAttribute:
請求參數到命令對象的綁定;
@SessionAttributes:
用於聲明session級別存儲的屬性,放置在處理器類上,通常列出模型屬性(如@ModelAttribute)對應的名稱,則這些屬性會透明的保存到session中;
@InitBinder:
自定義數據綁定註冊支持,用於將請求參數轉換到命令對象屬性的對應類型;
三、Spring3.0引入RESTful架構風格支持(通過@PathVariable註解和一些其他特性支持),且又引入了更多的註解支持:
@CookieValue:
cookie數據到處理器功能處理方法的方法參數上的綁定;
@RequestHeader:
請求頭(header)數據到處理器功能處理方法的方法參數上的綁定;
@RequestBody:
請求的body體的綁定(通過HttpMessageConverter進行類型轉換);
@ResponseBody:
處理器功能處理方法的返回值作爲響應體(通過HttpMessageConverter進行類型轉換);
@ResponseStatus:
定義處理器功能處理方法/異常處理器返回的狀態碼和原因;
@ExceptionHandler:
註解式聲明異常處理器;
@PathVariable:
請求URI中的模板變量部分到處理器功能處理方法的方法參數上的綁定,從而支持RESTful架構風格的URI;
四、Spring3.1使用新的HandlerMapping 和 HandlerAdapter來支持@Contoller和@RequestMapping註解處理器。
新的@Contoller和@RequestMapping註解支持類:處理器映射RequestMappingHandlerMapping 和 處理器適配器RequestMappingHandlerAdapter組合來代替Spring2.5開始的處理器映射DefaultAnnotationHandlerMapping和處理器適配器AnnotationMethodHandlerAdapter,提供更多的擴展點。
接下來,我們一起開始學習基於註解的控制器吧。
6.2、入門
(1、控制器實現
java代碼:
-
package cn.javass.chapter6.web.controller;
-
-
@Controller
-
public class HelloWorldController {
-
@RequestMapping(value = "/hello")
-
public ModelAndView helloWorld() {
-
-
-
-
-
ModelAndView mv = new ModelAndView();
-
-
mv.addObject("message", "Hello World!");
-
-
mv.setViewName("hello");
-
return mv;
-
}
-
}
1
可以通過在一個POJO類上放置@Controller或@RequestMapping,即可把一個POJO類變身爲處理器;
2
@RequestMapping(value = "/hello")
請求URL(/hello) 到 處理器的功能處理方法的映射;
3
模型數據和邏輯視圖名的返回。
現在的處理器無需實現/繼承任何接口/類,只需要在相應的類/方法上放置相應的註解說明下即可,非常方便。
(2、Spring配置文件chapter6-servlet.xml
(2.1、HandlerMapping和HandlerAdapter的配置
如果您使用的是Spring3.1之前版本,開啓註解式處理器支持的配置爲:DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter。
java代碼:
-
<!—Spring3.1之前的註解 HandlerMapping -->
-
<bean
-
class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
-
-
<!—Spring3.1之前的註解 HandlerAdapter -->
-
<bean
-
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
-
如果您使用的Spring3.1開始的版本,建議使用RequestMappingHandlerMapping和RequestMappingHandlerAdapter。
java代碼:
-
<!--Spring3.1開始的註解 HandlerMapping -->
-
<bean
-
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
-
<!--Spring3.1開始的註解 HandlerAdapter -->
-
<bean
-
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
下一章我們介紹DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter 與RequestMappingHandlerMapping和RequestMappingHandlerAdapter 的區別。
(2.2、視圖解析器的配置
還是使用之前的org.springframework.web.servlet.view.InternalResourceViewResolver。
(2.3、處理器的配置
java代碼:
-
<!-- 處理器 -->
-
<bean class="cn.javass.chapter6.web.controller.HelloWorldController"/>
只需要將處理器實現類註冊到spring配置文件即可,spring的DefaultAnnotationHandlerMapping或RequestMappingHandlerMapping能根據註解@Controller或@RequestMapping自動發現。
(2.3、視圖頁面(/WEB-INF/jsp/hello.jsp)
java代碼:
-
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
-
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
-
<html>
-
<head>
-
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-
<title>Hello World</title>
-
</head>
-
<body>
-
${message}
-
</body>
-
</html>
${message}:表示顯示由HelloWorldController處理器傳過來的模型數據。
(4、啓動服務器測試
地址欄輸入http://localhost:9080/springmvc-chapter6/hello,我們將看到頁面顯示“Hello World!”,表示成功了。
整個過程和我們第二章中的Hello World 類似,只是處理器的實現不一樣。接下來我們來看一下具體流程吧。
6.3、運行流程
和第二章唯一不同的兩處是:
1、HandlerMapping實現:使用DefaultAnnotationHandlerMapping(spring3.1之前)或RequestMappingHandlerMapping(spring3.1)
替換之前的BeanNameUrlHandlerMapping。
註解式處理器映射會掃描spring容器中的bean,發現bean實現類上擁有@Controller或@RequestMapping註解的bean,並將它們作爲處理器。
2、HandlerAdapter實現:使用AnnotationMeth
odHandlerAdapter(spring3.1之前)或RequestMappingHandlerAdapter(spring3.1)替換之前的SimpleControllerHandlerAdapter。
註解式處理器適配器會通過反射調用相應的功能處理方法(方法上擁有@RequestMapping註解
)。
好了到此我們知道Spring如何發現處理器、如何調用處理的功能處理方法了,接下來我們詳細學習下如何定義處理器、如何進行請求到功能處理方法的定義。
6.4、處理器定義
6.4.1、@Controller
java代碼:
-
@Controller
-
public class HelloWorldController {
-
……
-
}
推薦使用這種方式聲明處理器,它和我們的@Service、@Repository很好的對應了我們常見的三層開發架構的組件。
6.4.2、@RequestMapping
java代碼:
-
@RequestMapping
-
public class HelloWorldController {
-
……
-
}
這種方式也是可以工作的,但如果在類上使用@ RequestMapping註解一般是用於窄化功能處理方法的映射的,詳見6.4.3。
package cn.javass.chapter6.web.controller;
@Controller
@RequestMapping(value="/user") //①處理器的通用映射前綴
public class HelloWorldController2 {
@RequestMapping(value = "/hello2") //②相對於①處的映射進行窄化
public ModelAndView helloWorld() {
//省略實現
}
}
|
|
6.4.3、窄化請求映射
java代碼:
-
package cn.javass.chapter6.web.controller;
-
@Controller
-
@RequestMapping(value="/user")
-
public class HelloWorldController2 {
-
@RequestMapping(value = "/hello2")
-
public ModelAndView helloWorld() {
-
-
}
-
}
①類上的@RequestMapping(value="/user") 表示處理器的通用請求前綴;
②處理器功能處理方法上的是對①處映射的窄化。
因此http://localhost:9080/springmvc-chapter6/hello2
無法映射到HelloWorldController2的 helloWorld功能處理方法;而http://localhost:9080/springmvc-chapter6/user/hello2是可以的。
窄化請求映射可以認爲是方法級別的@RequestMapping繼承類級別的@RequestMapping。
窄化請求映射還有其他方式,如在類級別指定URL,而方法級別指定請求方法類型或參數等等,後續會詳細介紹。
到此,我們知道如何定義處理器了,接下來我們需要學習如何把請求映射到相應的功能處理方法進行請求處理。
6.5、請求映射
處理器定義好了,那接下來我們應該定義功能處理方法,接收用戶請求處理並選擇視圖進行渲染。首先我們看一下圖6-1:
http請求信息包含六部分信息:
①請求方法,如GET或POST,表示提交的方式;
②URL,請求的地址信息;
③協議及版本;
④請求頭信息(包括Cookie信息);
⑤回車換行(CRLF);
⑥請求內容區(即請求的內容或數據),如表單提交時的參數數據、URL請求參數(?abc=123 ?後邊的)等。
想要了解HTTP/1.1協議,請訪問http://tools.ietf.org/html/rfc2616。
那此處我們可以看到有①、②、④、⑥一般是可變的,因此我們可以這些信息進行請求到處理器的功能處理方法的映射,因此請求的映射分爲如下幾種:
URL路徑映射:使用URL映射請求到處理器的功能處理方法;
請求方法映射限定:如限定功能處理方法只處理GET請求;
請求參數映射限定:如限定只處理包含“abc”請求參數的請求;
請求頭映射限定:如限定只處理“Accept=application/json”的請求。
接下來看看具體如何映射吧。
轉載請註明出處【http://sishuok.com/forum/blogPost/list/6233.html#21512】