SpringMVC核心思想詳解
Spring MVC是spring的一個web元件,它為構建穩健的web應用提供了豐富的功能。
Spring MVC是基於每個邏輯和功能是高可配置的這樣架構和設計的。當然spring MVC可以與其它流行的web框架像struts、webwork、javaserverface及tapestry實現無縫整合。我們看一下spring MVC的核心思想。
Spring請求的生命週期
為了便於理解,我把spring MVC的生命週期拆解為如下一個步驟總結一下springMVC幾個關鍵的步驟,總共可以分為六個步驟,分別為:
(1) 客戶端向spring容器發起一個http請求
(2) 發起的請求被前端控制起所攔截(DispatcherServlet),前端控制器會去找恰當的對映處理器來處理這次請求。
(3) 根據處理器對映(Handler Mapping)來選擇並決定將請求傳送給那一個控制器。
(4) 在控制器中處理所傳送的請求,並以modelAndView(屬性值和返回的頁面)的形式返回給向前端控制器。
(5) 前端控制器通過查詢viewResolver物件來試著解決從控制返回的檢視。
(6) 如果前端控制找到對應的檢視,則將檢視返回給客戶端,否則拋異常。
通過上面的圖和springMVC生命週期的六個步驟,想必大家對springMVC的核心思想有個了大概的瞭解了,下面我們以例項為主,帶領大家慢慢熟悉整個springMVC及如何使用springMVC。(本教程基於maven實現springMVC中的例子,所以大家得對maven需要有大概的瞭解)。
如果僅僅關注於web方面的支援,Spring有下面一些特點:
· 清晰的角色劃分:控制器(controler),驗證器(Validate),命令物件(Object),表單物件(BeanObject)和模型物件(Model);分發器(Adapter),處理器對映(HandlerAdapter)和檢視解析器(ViewResolver);等等。
· 直接將框架類和應用類都作為JavaBean配置,包括通過應用上下文配置中間層引用,例如,從web控制器到業務物件和驗證器的引用。
· 可適應性,但不具有強制性:根據不同的情況,使用任何你需要的控制器子類(普通控制器,命令,表單,嚮導,多個行為,或者自定義的),而不是要求任何東西都要從Action/ActionForm繼承。
可重用的業務程式碼,而不需要程式碼重複:你可以使用現有的業務物件作為命令物件或表單物件,而不需要在ActionForm的子類中重複它們的定義
· 可定製的繫結和驗證:將型別不匹配作為應用級的驗證錯誤,這可以儲存錯誤的值,以及本地化的日期和數字繫結等,而不是隻能使用字串表單物件,手動解析它並轉換到業務物件。
· 可定製的處理器對映,可定製的檢視解析:靈活的模型可以根據名字/值對映,處理器對映和檢視解析使應用策略從簡單過渡到複雜,而不是隻有一種單一的方法。
· 可定製的本地化和主題解析,支援JSP,無論有沒有使用Spring標籤庫,支援JSTL,支援不需要額外過渡的Velocity,等等。
簡單而強大的標籤庫,它儘可能地避免在HTML生成時的開銷,提供在標記方面的最大靈活性。(1)在WEB-INF/web.xml中加入如下程式碼:
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:spring-servlet.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
上述的配置的就是前段控制器,在servlet-mapping配置了*.html,意味著所有以.html結尾的請求多會通過這個servlet,當dispatcherServlet啟動時,他預設會在web-info目錄下查詢一個spring-servlet.xml的配置檔案。上面我們通過顯示指定了這個檔案的位置,即在類路徑底下的spring-servlet.xml.這個檔案我們會在第二步點給他家做詳細介紹。
(2)在類路徑底下新增spring-servlet.xml檔案,其內容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan
base-package="com.pango.spring.helloworld.controller" />
<bean id="viewResolver"
class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
上面這個檔案,我們定義了一個<context:component-scan
base-package="com.pango.spring.helloworld.controller" />
這樣的標籤,定義了這個後,當spring在啟動時,會載入com.pango.spring.helloworld.controller這個包底下及子包底下的所有的元件(這就包的自動掃描機制,即spring會將標有@Controller @Component等類載入到spring容器管理中),後面我們還定義了
<bean id="viewResolver"
class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
ViewResolver是一個試圖解析器,就是我們第一部分提到的springMVC生命週期中的第五步,上面這段的配置的意思就是,當我們從後端控制器中返回的檢視時,前端控制器就根據這一段配置來返回一個具體的檢視,如後端控制返回的是一個hello,根據上面的配置,最後前端控制器會組併成這樣的一個地址:/web-inf/jsp/hello.jsp,然後從/web-inf/jsp/這個目錄下面查詢一個hello.jsp返回客戶端。第三部分我們看我們寫得HelloworldController後臺控制器。
(3)在包底下寫一個HelloWorldController的類,其內容如下:
@Controller
public class HelloWorldController {
@RequestMapping(value="/hello")
public String sayHello(ModelMap modelMap){
modelMap.put("sayHello", "hello world");
return "/hello";
}
}
在這裡簡單介紹下上面的配置,後面我們會詳細講解各個引數:
Ø Controller即宣告這個類是一個控制器,上面第二部分我們說明了,只要加了@Controller標示的,spring會通過自動掃描機制,將這個類納入spring容器管理中。
Ø @RequestMapping(value="/hello"),這個定義的就是一個請求路徑,只要符合/hello路徑的多會交給這個控制器的sayhello方法來處理。
Ø 最後我們返回/hello的檢視給客戶端。
(4)好了,大功告成,我們再在web-info/jsp/目錄下新增一個hello.jsp檔案,就可以啟動執行我們的第一個程式了。hello.jsp的內容如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!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>Insert title here</title>
</head>
<body>
<span>${sayHello}</span>
</body>
</html>
執行後訪問ip;port/project/hello.html就可以看到我們預期的結果了。
四、springMVC引數傳遞mvc結構中,v層不斷有資料和c層互動,所以弄明白在springMVC中如何與後臺進行資料互動是極其重要的,當然在下面我不會介紹每一個方法,只是對常用的方法,對於我這裡沒有涉及的方法大家可以參考spring官方的文件中springMVC這個章節。下面我們來看一幅圖。
當我們向springMVC發起請求到檢視返回前,spring MVC幫我們做了主要是上面幾個步驟,通過資料繫結、資料型別轉換、驗證、結果繫結這幾個步驟。
讓我們看下例項:
@RequestMapping("/user/find")
public String findUserById(@RequestParam("userId") int userId,ModelMap modelMap){
modelMap.put("userId", userId);
return "/user";
}
Ø @RequestMapping("/user/find"),是對請求對映的說明,這個註解中主要包含四個屬性,分別value、method、params、header,這四個引數分別表示:
Value:指定路徑
Method:請求方式
Params:引數
Headers:請求頭
後面三個就是對請求路徑的一個限制條件。
SpringMVC對於路徑的定義非常的靈活
以下URL都是合法的:
l /usercreateUser
匹配/user/createUser、/user/aaa/bbb/createUser等URL。
l /user/createUser??
匹配/user/createUseraa、/user/createUserbb等URL。
l /user/{userId}
匹配user/123、user/abc等URL。
l /user{userId}
匹配user/aaa/bbb/123、user/aaa/456等URL。l company/{companyId}/user/{userId}/detail
匹配company/123/user/456/detail等的URL。
Ø 對RequestParam的介紹
@RequestParam有以下三個引數。
l value:引數名。
l required:是否必需,預設為true,表示請求中必須包含對應的引數名,如果不存在將丟擲異常。
l defaultValue:預設引數名,設定該引數時,自動將required設為false。極少情況需要使用該引數,也不推薦使用該引數。
當傳送請求時,請求引數中必須要包含userId這個引數,當不包含這個引數,請求將找不到這個對映。當屬性required=true時,不包含這個引數將會拋異常,如果不能確定是否需要這個引數是我們可以寫成,@RequestParam(value = "userId", required = false) 。
Ø 直接將屬性對映到物件中
@RequestMapping("/user/find2")
public String find2UserById(User user,ModelMap modelMap){
modelMap.put("user", user);
return "/user";
}
Spring MVC按:
“HTTP請求引數名 = 命令/表單物件的屬性名”
的規則,自動繫結請求資料,支援“級聯屬性名”,自動進行基本型別資料轉換。
如:發起下面這個請求,springMVC會自動將id、name、password屬性的值填充到user物件中。
Ø SpringMVC以rest技術向springMVC傳遞引數
通過 REST 風格體系架構,請求和響應都是基於資源表示的傳輸來構建的。資源是通過全域性 ID 來標識的,這些 ID 一般使用的是一個統一資源識別符號(URI)。客戶端應用使用 HTTP 方法(如,GET、POST、PUT 或 DELETE)來操作一個或多個資源。通常,GET 是用於獲取或列出一個或多個資源,POST 用於建立,PUT 用於更新或替換,而 DELETE 則用於刪除資源。
例如,GET http://host/context/employees/12345
將獲取 ID 為 12345 的員工的表示。這個響應表示可以是包含詳細的員工資訊的 XML 或 ATOM,或者是具有更好 UI 的 JSP/HTML 頁面。您看到哪種表示方式取決於伺服器端實現和您的客戶端請求的 MIME 型別。
RESTful Web Service 是一個使用 HTTP 和 REST 原理實現的 Web Service。通常,一個 RESTful Web Service 將定義基本資源 URI、它所支援的表示/響應 MIME,以及它所支援的操作。
Spring 3.0之後引入了對rest風格的支援。我們看例項
@RequestMapping("/user/find/{id}")
public String rest(@PathVariable int id,ModelMap modelMap){
User user = new User();user.setName("marcle");
user.setPassword("123");
user.setId(id);
modelMap.put("user", user);
return "/user";
}
這裡需要注意的地方時@RequestMapping("/user/find/{id}")和@PathVariable int id名稱必須一樣,否則會出現異常。
Ø 簡單介紹返回檢視的方式
u ModelAndView 形式返回
@RequestMapping("/user/save2")
public ModelAndView save2(User user,ModelMap modelMap){
ModelAndView mav = new ModelAndView();
mav.setViewName("/user");
mav.addObject("user", user);
return mav;
}
ModelAndView就是對返回到頁面的值和檢視進行封裝。
u 直接字串的形式返回,如”return “/user””,再把屬性通過modelMap進行封裝,modelMap儲存的值屬於request範圍內,如果要傳送伺服器端請求,springMVC非常方便,你只要這樣寫即可return ”direct:user”.
還有一種傳遞引數的方法,我放在springMVC中的rest技術介紹
下面我們看看springMVC返回的過程SpringMVC簡單沒幾個標籤,用起來還是非常好用的,在使用springMVC中的標籤之前需要向每個jsp的頭部引入標籤支援<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
1) form標籤
這個標籤會生成一個HTML的form標籤,同時為內部標籤的繫結暴露了一個繫結路徑,它把命令物件(command object)放在pageContext中,這樣內部的標籤就可以訪問這個物件,這個庫中的其它標籤都是這個標籤的巢狀標籤。
如我們有個user的域物件,包含id、name、password屬性,我們將把它當作返回index.jsp表單控制器的物件,如下面的程式碼:
<form:form action="${ctx}/user/save.${ext}" method="post" commandName="user">
<table>
<tr>
<td>Id:</td>
<td><form:input path ="id" /></td>
</tr>
<tr>
<td>name:</td>
<td><form:input path ="name" /></td>
</tr>
<tr>
<td>password:</td>
<td><form:input path ="password" /></td>
</tr>
<tr>
<td colspan="2"><input type ="submit" value="Save" /></td>
</tr>
</table>
</form:form>
上述的id、name、password由頁面控制器放置在pageContext中,即在內部控制器方法中需要做這樣的宣告:
@RequestMapping(value="/user/save",method=RequestMethod.GET)
public String forSave(@ModelAttribute User user){
return "/index";
}
後臺控制器中必須繫結這個@ModelAttribute User user命令列物件,而form下面的屬性需要於這個user中的屬性對應起來,否則將會拋異常。標籤經過解析後生成的程式碼如下:
<form id="user" action="/springTag/user/save.html" method="post">
<table>
<tr>
<td>Id:</td>
<td><input id="id" name="id" type="text" value="0"/></td>
</tr>
<tr>
<td>name:</td>
<td><input id="name" name="name" type="text" value=""/></td>
</tr>
<tr>
<td>password:</td>
<td><input id="password" name="password" type="text" value=""/></td>
</tr>
<tr>
<td colspan="2"><input type ="submit" value="Save Changes" /></td>
</tr>
</table>
</form>
使用時如上面的表示式,<form:input path ="id" />解析後會變成<input id="name" name="name" type="text" value=""/>可見用spring標籤比傳統的html簡潔很多。
(3)checkbox 標籤
這個標籤解析之後會變成html’中的type為checkbox的input元素,我們假設我們的使用者有很多的參考東西,如資訊的訂閱、愛好、格言等,即如下面的域模型:
public class Preferences {
private boolean receiveNewsletter;
private String[] interests;
private String favouriteWord;
public boolean isReceiveNewsletter() {
return receiveNewsletter;
}
public void setReceiveNewsletter(boolean receiveNewsletter) {
this.receiveNewsletter = receiveNewsletter;
}
public String[] getInterests() {
return interests;
}
public void setInterests(String[] interests) {
this.interests = interests;
}
public String getFavouriteWord() {
return favouriteWord;
}
public void setFavouriteWord(String favouriteWord) {
this.favouriteWord = favouriteWord;
}
}
我們的相應的jsp檔案可以寫成:
<form:form action="${ctx}/pre/save.${ext}" method="post" commandName="preferences">
<table>
<tr>
<td>Subscribe to newsletter?:</td>
<%-- Approach 1: Property is of type java.lang.Boolean --%>
<td><form:checkbox path="receiveNewsletter"/></td>
<td> </td>
</tr>
<tr>
<td>Interests:</td>
<td>
<%-- Approach 2: Property is of an array or of type java.util.Collection --%>
Quidditch: <form:checkbox path="interests" value="Quidditch"/>
Herbology: <form:checkbox path="interests" value="Herbology"/>
Defence Against the Dark Arts: <form:checkbox path="interests"
value="Defence Against the Dark Arts"/>
</td>
<td> </td>
</tr>
<tr>
<td>Favourite Word:</td>
<td>
<%-- Approach 3: Property is of type java.lang.Object --%>
Magic: <form:checkbox path="favouriteWord" value="Magic"/>
</td>
<td> </td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="submit"/>
</td>
</tr>
</table>
</form:form>
如果有多個供選擇的,在後臺我們以陣列的形式儲存。
(4)radiobutton 標籤
解析後會變成html元素中type為radio的input元素
如下面的情況:
<tr>
<td>Sex:</td>
<td>Male: <form:radiobutton path="sex" value="M"/> <br/>
Female: <form:radiobutton path="sex" value="F"/> </td>
<td> </td>
</tr>
(5)password標籤
解析後變成html元素中type為password的input元素,即為密碼框。
<tr>
<td>Password:</td>
<td>
<form:password path="password" />
</td>
</tr>
(6)select標籤
這個標籤對應於html元素中的下拉框,即為select元素。
<tr>
<td>Skills:</td>
<td><form:select path="skills" items="${skills}" /></td>
<td></td>
</tr>
(7)option標籤
<form:select path="house">
<form:option value="Gryffindor"/>
<form:option value="Hufflepuff"/>
<form:option value="Ravenclaw"/>
<form:option value="Slytherin"/>
</form:select>
(8)options標籤
<form:select path="country">
<form:option value="-" label="--Please Select"/>
<form:options items="${countryList}" itemValue="code" itemLabel="name"/>
</form:select>
(9)textarea標籤
<td><form:textarea path="notes" rows="3" cols="20" /></td>
(10)hidden標籤
<form:hidden path="house" />
(11)errors標籤
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName" /></td>
<%-- Show errors for firstName field --%>
<td><form:errors path="firstName" /></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName" /></td>
<%-- Show errors for lastName field --%>
<td><form:errors path="lastName" /></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes" />
</td>
</tr>
</table>
</form:form>
和Struts2一樣,Spring MVC也可以使用攔截器對請求進行攔截處理,使用者可以自定義攔截器來實現特定的功能,自定義的攔截器必須實現HandlerInterceptor介面。這個介面中定義了三個方法:preHandle()、postHandle()、afterCompletion()。
下面對程式碼中的三個方法進行解釋。
preHandle():這個方法在業務處理器處理請求之前被呼叫,在該方法中對使用者請求request進行處理。如果程式設計師決定該攔截器對請求進行攔截處理後還要呼叫其他的攔截器,或者是業務處理器去進行處理,則返回true;如果程式設計師決定不需要再呼叫其他的元件去處理請求,則返回false。
postHandle():這個方法在業務處理器處理完請求後,但是DispatcherServlet向客戶端返回請求前被呼叫,在該方法中對使用者請求request進行處理。
afterCompletion():這個方法在DispatcherServlet完全處理完請求後被呼叫,可以在該方法中進行一些資源清理的操作。
下面通過一個例子來說明如何使用Spring MVC框架的攔截器。
要求編寫一個攔截器,攔截所有不在工作時間的請求,把這些請求轉發到一個特定的靜態頁面,而不對它們的請求進行處理。
首先編寫TimeInterceptor.Java,程式碼如下:
public class TimeInterceptor extends HandlerInterceptorAdapter {
private int openingTime; // openingTime 屬性指定上班時間
private int closingTime; // closingTime屬性指定下班時間
private String outsideOfficeHoursPage; // outsideOfficeHoursPage屬性指定錯誤
public void setOpeningTime(int openingTime) {
this.openingTime = openingTime;
}
public void setClosingTime(int closingTime) {
this.closingTime = closingTime;
}
public void setOutsideOfficeHoursPage(String outsideOfficeHoursPage) {
this.outsideOfficeHoursPage = outsideOfficeHoursPage;
}
// 重寫 preHandle()方法,在業務處理器處理請求之前對該請求進行攔截處理
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
Calendar cal = Calendar.getInstance();
int hour = cal.get(Calendar.HOUR_OF_DAY); // 獲取當前時間
if (openingTime <= hour && hour < closingTime) { // 判斷當前是否處於工作 時間段內
return true;
} else {
response.sendRedirect(outsideOfficeHoursPage); // 返回提示頁面
return false;
}
}
}
可以看出,上面的程式碼過載了preHandle()方法,該方法在業務處理器處理請求之前被呼叫。在該方法中,首先獲得當前的時間,判斷其是否在 openingTime和closingTime之間,如果在,返回true,這樣才會呼叫業務控制器去處理該請求;否則直接轉向一個靜態頁面,返回 false,這樣該請求就不會被處理。
下面是在dispatcherServlet-servlet.xml中對攔截器進行的配置,程式碼如下:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/user/*" />
<bean class="com.pango.spring.interceptor.TimeInterceptor">
<property name="openingTime" value="12"></property>
<property name="closingTime" value="24"></property>
<property name="outsideOfficeHoursPage" value="outTime.html"></property>
</bean>
</mvc:interceptor>
</mvc:interceptors>
可以看出,上面程式碼用bean標籤去定義TimeInterceptor,令其id為officeHoursInterceptor,並給它的3個屬性賦值。在urlMapping中通過<property name="interceptors">去指定officeHoursInterceptor為一個攔截器,讀者可以在<list> 和</list>之間定義多個攔截器
outsideOfficeHours.html的程式碼很簡單,只是輸出一句提示語。
執行程式,在瀏覽器中隨便訪問一個頁面,如果請求的時間在9點~18點之間,則該請求可以被處理;否則,返回一句提示語,如圖23-5所示
說 明:在第22章中介紹過控制反轉是Spring框架的核心思想,即用一個介面去定義一些操作,在介面的實現類中去重寫這些操作,然後在Spring的配置檔案中去把該介面的實現類注入到應有框架中,這樣就可以通過呼叫介面去呼叫介面的實現類。本節講的攔截器就體現了這種思想,即實現 HandlerInterceptorAdapter介面,重寫preHandle()方法並在配置檔案中實現TimeInterceptor的注入。這 樣當框架呼叫HandlerInterceptorAdapter時,就可以呼叫到TimeInterceptor類的preHandle()方法
七、spring3 MVC 型別轉換 Servlet中的輸入引數為都是string型別,而spring mvc通過data bind機制將這些string 型別的輸入引數轉換為相應的command object(根據view和controller之間傳輸資料的具體邏輯,也可稱為model attributes, domain model objects)。在這個轉換過程中,spring實際是先利用java.beans.PropertyEditor中的 setAdText方法來把string格式的輸入轉換為bean屬性,亦可通過繼承java.beans.PropertyEditorSupport來實現自定義的PropertyEditors。
自定義完畢propertyEditor後,有以下幾種方式來註冊自定義的customer propertyEditor. (我只實現了第二種轉換方式,至於其它方法大家可以自己嘗試)
Ø 直接將自定義的propertyEditor放到需要處理的java bean相同的目錄下
名稱和java Bean相同但後面帶Editor字尾。
例如需要轉換的java bean 名為User,則在相同的包中存在UserEditor類可實現customer propertyEditor的自動註冊。
Ø 利用@InitBinder來註冊customer propertyEditor
這個在之前的筆記中已經介紹過了,即在controller類中增加一個使用@InitBinder標註的方法,在其中註冊customer Editor
Java程式碼
public class BaseController {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class, new CustomDateEditor(true));
}
}
Ø 繼承 WebBindingInitializer 介面來實現全域性註冊
使用@InitBinder只能對特定的controller類生效,為註冊一個全域性的customer Editor,可以實現介面WebBindingInitializer 。
Java程式碼
public class CustomerBinding implements WebBindingInitializer {
public void initBinder(WebDataBinder binder, WebRequest request) {
// TODO Auto-generated method stub
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
}
並修改 spring-servlet xml配置檔案
Xml程式碼
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="webBindingInitializer">
<bean
class="net.zhepu.web.customerBinding.CustomerBinding" />
</property>
</bean>
但這樣一來就無法使用mvc:annotation-driven 了。
使用conversion-service來註冊自定義的converter
DataBinder實現了PropertyEditorRegistry, TypeConverter這兩個interface,而在spring mvc實際處理時,返回值都是return binder.convertIfNecessary(見HandlerMethodInvoker中的具體處理邏輯)。因此可以使用customer conversionService來實現自定義的型別轉換。
Xml程式碼
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<list>
<bean class="net.zhepu.web.customerBinding.CustomerConverter" />
</list>
</property>
</bean>
需要修改spring-servlet xml配置檔案中的annotation-driven,增加屬性conversion-service指向新增的conversionService bean。
Xml程式碼
<mvc:annotation-driven validator="validator"
conversion-service="conversionService" />
對於第二種方式實現如下
Date型別編輯器
public class CustomDateEditor extends PropertyEditorSupport {
private static final Map<String, String> dateMap;
static {
dateMap = new HashMap<String, String>();
dateMap.put("yyyy-MM-dd", "\\d{4}-\\d{2}-\\d{2}");
dateMap.put("yyyy-MM-dd hh:mm:ss", "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}");
dateMap.put("yyyy年MM月dd日", "\\d{4}年\\d{2}月\\d{2}日");
}
private final boolean allowEmpty;
public CustomDateEditor(boolean allowEmpty) {
this.allowEmpty = allowEmpty;
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
if (this.allowEmpty && !StringUtils.hasText(text)) {
// Treat empty String as null value.
setValue(null);
} else {
try {
boolean flag = false;
for (String dateFormatStr : dateMap.keySet()) {
if (text.matches(dateMap.get(dateFormatStr))) {
flag = true;
System.out.println(text);
DateFormat dateFormat = new SimpleDateFormat(dateFormatStr);
setValue(dateFormat.parse(text));
break;
}
}
if (!flag) {
//throw new IllegalArgumentException("Could not parse date: " + text);
}
} catch (ParseException ex) {
//throw new IllegalArgumentException("Could not parse date: " + ex.getMessage(), ex);
}
}
}
@Override
public String getAsText() {
Date value = (Date) getValue();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
return value != null ? dateFormat.format(value) : "";
}
}
@InitBinder來註冊customer propertyEditor
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class, new CustomDateEditor(true));
}
最後講講對於requestBody或httpEntity中資料的型別轉換
Spring MVC中對於requestBody中傳送的資料轉換不是通過databind來實現,而是使用HttpMessageConverter來實現具體的型別轉換。
例如,之前提到的json格式的輸入,在將json格式的輸入轉換為具體的model的過程中,spring mvc首先找出request header中的contenttype,再遍歷當前所註冊的所有的HttpMessageConverter子類,根據子類中的canRead()方法來決定呼叫哪個具體的子類來實現對requestBody中的資料的解析。如果當前所註冊的 httpMessageConverter中都無法解析對應contexttype型別,則丟擲 HttpMediaTypeNotSupportedException (http 415錯誤)。
那麼需要如何註冊自定義的messageConverter呢,很不幸,在spring 3.0.5中如果使用annotation-driven的配置方式的話,無法實現自定義的messageConverter的配置,必須老老實實的自己定義AnnotationMethodHandlerAdapter的bean定義,再設定其messageConverters以註冊自定義的 messageConverter。
Xml程式碼
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
<bean class="org.springframework.http.converter.ResourceHttpMessageConverter"/>
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
八、json格式資料的輸入和輸出
Spring mvc處理json需要使用jackson的類庫,因此為支援json格式的輸入輸出需要先修改pom.xml增加jackson包的引用
Xml程式碼
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-lgpl</artifactId>
<version>1.8.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.8.0</version>
</dependency>
在spring-servlet.xml中必須加入這段程式碼:<mvc:annotation-driven />
根據前面的分析,在spring mvc中解析輸入為json格式的資料有兩種方式
1:使用@RequestBody來設定輸入
Java程式碼
@RequestMapping("/json1")
@ResponseBody
public JsonResult testJson1(@RequestBody User u){
log.info("get json input from request body annotation");
log.info(u.getUserName());
return new JsonResult(true,"return ok");
}
2:使用HttpEntity來實現輸入繫結
Java程式碼
@RequestMapping("/json2")
public ResponseEntity<JsonResult> testJson2(HttpEntity<User> u){
log.info("get json input from HttpEntity annotation");
log.info(u.getBody().getUserName());
ResponseEntity<JsonResult> responseResult = new ResponseEntity<JsonResult>( new JsonResult(true,"return ok"),HttpStatus.OK);
return responseResult;
對應Json格式的輸出也對應有兩種方式
1:使用@responseBody來設定輸出內容為context body
@RequestMapping(value="/kfc/brands/{name}", method = RequestMethod.GET)
public @ResponseBody List<Shop> getShopInJSON(@PathVariable String name) {
List<Shop> shops = new ArrayList<Shop>();
Shop shop = new Shop();
shop.setName(name);
shop.setStaffName(new String[]{"mkyong1", "mkyong2"});
shops.add(shop);
Shop shop2 = new Shop();
shop2.setName(name);
shop2.setStaffName(new String[]{"mktong1", "mktong2"});
shops.add(shop2);
return shops;
}
伺服器端會返回給我們jason格式的資料,這樣我們就可以省去手工繁瑣的組並了
2:返回值設定為ResponseEntity<?>型別,以返回context body
@RequestMapping("/json2")
public ResponseEntity<JsonResult> testJson2(HttpEntity<User> u){
log.info("get json input from HttpEntity annotation");
log.info(u.getBody().getUserName());
ResponseEntity<JsonResult> responseResult = new ResponseEntity<JsonResult>( new JsonResult(true,"return ok"),HttpStatus.OK);
return responseResult;
}
Spring mvc使用jakarta的commons fileupload來支援檔案上傳,因此我們需要在pom.xml中匯入所依賴的兩個包:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
在spring-servlet.xml中加入以下這段程式碼:
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- one of the properties available; the maximum file size in bytes -->
<property name="maxUploadSize" value="100000" />
</bean>
其中的property中可以限制最大和最小檔案上傳。
在客戶端的程式碼如下:
<form method="post" action="${ctx}/user/upload.${ext}" enctype="multipart/form-data">
<input type="text" name="name"/>
<input type="file" name="file"/>
<input type="submit"/>
</form>
伺服器端的程式碼如下:
@RequestMapping(value = "/user/upload", method = RequestMethod.POST)
public String handleFormUpload(@RequestParam("name") String name,
@RequestParam("file") MultipartFile file, HttpServletRequest request)
throws IOException {
String filePath = request.getRealPath("/");
if (!file.isEmpty()) {
String fileName = file.getOriginalFilename();
System.out.println(filePath + "/" + fileName);
byte[] bytes = file.getBytes();
FileOutputStream output = new FileOutputStream(new File(filePath
+ fileName));
output.write(bytes);
output.close();
return "redirect:/success.jsp";
} else {
return "redirect:/failure.jsp";
}
}
何為國際化,簡單來說就是在那個國家顯示哪個國家的語言,在計算機中,國際化和本地化意味著計算機軟體要適應不同的語言和地區的差異。國際化就是設計為了適應不同地區和語言的差異而工程不需要做任何改動。
這一節的目的就是在springMVC中增加國際化和本地化的應用,我們將在這一節實現三種語言可以相互切換的國際化和本地化。
(1)我們在resources下面新增三個property檔案,分別為:messages_de.properties、messages_en.properties、messages_zh.properties,檔案的命名規則:messages_語言.properties
三個檔案的內容如下:
Ø messages_de.properties
label.firstname=Vorname
label.lastname=Familiename
label.email=Email
label.telephone=Telefon
label.addcontact=Addieren Kontakt
label.title=spring mvc Internationalization (i18n) / Localization
Ø messages_en.properties
label.firstname=First Name
label.lastname=Last Name
label.email=Email
label.telephone=Telephone
label.addcontact=Add Contact
label.title=spring mvc Internationalization (i18n) / Localization
Ø messages_zh.properties(經過轉換後的中文)
label.firstname=\u59D3
label.lastname=\u540D\u5B57
label.email=\u7535\u5B50\u90AE\u4EF6
label.telephone=\u7535\u8BDD
label.addcontact=\u8054\u7CFB\u65B9\u5F0F
label.title=spring mvc \u56FD\u9645\u5316\u548C\u672C\u5730\u5316\u652F\u6301
(2)spring-servet.xml檔案的配置
<!-- 為了使用國際化資訊源,Spring MVC 必須實現MessageSource介面。當然框架內部有許多內建的實現類。我們需要做的是註冊一個MessageSource型別的Bean。Bean 的名稱必須為messageSource,從而方便DispatcherServlet自動檢測它。每個DispatcherServlet只能註冊一個資訊源-->
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:messages" />
<property name="defaultEncoding" value="UTF-8" />
</bean>
<!—session 解析區域 -->
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<!-- property name="defaultLocale" value="en"/> -->
</bean>
<!-- 修改使用者的區域需要呼叫區域修改攔截器 LocaleChangeInterceptor。如下所設定設定paramName屬性來設定攔截請求中的特定引數(這裡是language)確定區域。既然是攔截器那就需要註冊到攔截器 Bean 中,這裡是註冊到了DefaultAnnotationHandlerMapping Bean中 -->
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="lang" />
</bean>
<!--
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="defaultLocale" value="en" />
</bean>
-->
<bean id="handlerMapping"
class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name="interceptors">
<ref bean="localeChangeInterceptor" />
</property>
</bean>
(3)在jsp目錄下面建立一個contact.jsp檔案
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="ctx" value="${pageContext.request.contextPath}" />
<c:set var="ext" value="html" />
<html>
<head>
<title>Spring 3 MVC Series - Contact Manager</title>
</head>
<body>
<h3><spring:message code="label.title"/></h3>
<span style="float: right">
<a href="${ctx}/language.${ext}?local=en">英文</a>
|
<a href="${ctx}/language.${ext}?local=de">德文</a>
<a href="${ctx}/language.${ext}?local=zh">中文</a>
</span>
<form:form method="post" action="addContact.html" commandName="contact">
<table>
<tr>
<td><form:label path="firstname"><spring:message code="label.firstname"/></form:label></td>
<td><form:input path="firstname" /></td>
</tr>
<tr>
<td><form:label path="lastname"><spring:message code="label.lastname"/></form:label></td>
<td><form:input path="lastname" /></td>
</tr>
<tr>
<td><form:label path="lastname"><spring:message code="label.email"/></form:label></td>
<td><form:input path="email" /></td>
</tr>
<tr>
<td><form:label path="lastname"><spring:message code="label.telephone"/></form:label></td>
<td><form:input path="telephone" /></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="<spring:message code="label.addcontact"/>"/>
</td>
</tr>
</table>
</form:form>
</body>
</html>
其中<spring:message>標籤結合 ResourceBundleMessageSource 的功能,在網頁上顯示 messages.properties 中的文字訊息。
(4)建立LanguageController
@Controller
public class LanguageController {
@Autowired
private SessionLocaleResolver localeResolver;
@RequestMapping("/forLanguage")
public String forLanguage(@ModelAttribute Contact contact){
return "/contact";
}
@RequestMapping(value="/language",method=RequestMethod.GET)
public ModelAndView changeLocal(@ModelAttribute Contact contact,HttpServletRequest request,@RequestParam String local,HttpServletResponse response){
if("zh".equals(local)){
localeResolver.setLocale(request, response, Locale.CHINA);
}else if("en".equals(local)) {
localeResolver.setLocale(request, response, Locale.ENGLISH);
}else if("de".equals(local)){
localeResolver.setLocale(request, response, Locale.GERMAN);
}
return new ModelAndView("/contact");
}
}
其中紅色部分就是對語言的設定
效果如下圖:
JSR 303 – Bean Validation 是一個數據驗證的規範,2009 年 11 月確定最終方案。2009 年 12 月 Java EE 6 釋出,Bean Validation 作為一個重要特性被包含其中,Spring MVC在使用了<mvc:annotation-driven> 後,如果路徑中有jsr 303的實現,將自動提供對jsr 303驗證方式的支援。
Ø 引入hibernate-validator,hibernate-validator對jsr 303做了實現
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.2.0.Final</version>
</dependency>
Ø 新增一個pojo bean ,增加jsr 303格式的驗證annotation
public class Contact {
private Long id = 0L;
@Size(min = 1)
private String firstName;
@NotNull
@Size(min = 1)
private String lastName;
@Past
private Date dateOfBirth;
private boolean married;
@Min(0)
@Max(20)
private int children;
}
Ø 在controller 類中的handler method中,對需要驗證的物件前增加@Valid 標誌
@RequestMapping(value="/add",method=RequestMethod.POST)
public String addContact(@ModelAttribute @Valid Contact contact,BindingResult result){
if(result.hasErrors()){
return "/contact";
}
return "/contact";
}
Ø 在jsp頁面中新增contact.jsp
<form:form action="${ctx }/contact/add.${ext}" method="post"
commandName="contact">
<table border="1">
<tr>
<th> </th>
<th><spring:message code="editcontact.heading" /></th>
</tr>
<tr>
<td bgcolor="cyan"><spring:message
code="editcontact.label.firstname" /></td>
<td><form:input path="firstName" size="40" /><font
color="#FF0000"><form:errors path="firstName*" /></font></td>
</tr>
<tr>
<td bgcolor="cyan"><spring:message
code="editcontact.label.lastname" /></td>
<td><form:input path="lastName" size="40" /><font
color="#FF0000"><form:errors path="lastName*" /></font></td>
</tr>
<tr>
<td bgcolor="cyan"><spring:message code="editcontact.label.dob" /></td>
<td><form:input path="dateOfBirth" size="40" /><font
color="#FF0000"><form:errors path="dateOfBirth*" /></font></td>
</tr>
<tr>
<td bgcolor="cyan"><spring:message
code="editcontact.label.married" /></td>
<td><form:checkbox path="married" /><font color="#FF0000"><form:errors
path="married" /></font></td>
</tr>
<tr>
<td bgcolor="cyan"><spring:message
code="editcontact.label.children" /></td>
<td><form:input path="children" size="5" /><font
color="#FF0000"><form:errors path="children*" /></font></td>
</tr>
<tr>
<td><input type="submit"
value="<spring:message code="editcontact.button.save"/>" /></td>
<td><input type="reset"
value="<spring:message code="editcontact.button.reset"/>" /></td>
</tr>
</table>
</form:form>
Ø 結果
使用jsr 303非常簡單吧,有些人就問了,可以不可以自定義錯誤資訊,當然是可以的,下面我就通過自定義錯誤來實現對contact的校驗。
@Size(min = 1, message = "Contact first name is required.")
private String firstName;
@NotNull(message = "Contact cannot be left empty.")
@Size(min = 1, message = "Contact last name is required.")
private String lastName;
@Past(message = "Contact date of birth must be a date in the past.