SpringMVC Controller 介紹
一、簡介
在SpringMVC 中,控制器Controller 負責處理由DispatcherServlet 分發的請求,它把用戶請求的數據經過業務處理層處理之後封裝成一個Model ,然後再把該Model 返回給對應的View 進行展示。在SpringMVC 中提供了一個非常簡便的定義Controller 的方法,你無需繼承特定的類或實現特定的接口,只需使用@Controller 標記一個類是Controller ,然後使用@RequestMapping 和@RequestParam 等一些註解用以定義URL 請求和Controller 方法之間的映射,這樣的Controller 就能被外界訪問到。此外Controller 不會直接依賴於HttpServletRequest 和HttpServletResponse 等HttpServlet 對象,它們可以通過Controller 的方法參數靈活的獲取到。爲了先對Controller 有一個初步的印象,以下先定義一個簡單的Controller :
- @Controller
- public class MyController {
- @RequestMapping ( "/showView" )
- public ModelAndView showView() {
- ModelAndView modelAndView = new ModelAndView();
- modelAndView.setViewName( "viewName" );
- modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " );
- return modelAndView;
- }
- }
@Controllerpublic class MyController { @RequestMapping ( "/showView" ) public ModelAndView showView() { ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName( "viewName" ); modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " ); return modelAndView; }}
在上面的示例中,@Controller 是標記在類MyController 上面的,所以類MyController 就是一個SpringMVC Controller 對象了,然後使用@RequestMapping(“/showView”) 標記在Controller 方法上,表示當請求/showView.do 的時候訪問的是MyController 的showView 方法,該方法返回了一個包括Model 和View 的ModelAndView 對象。這些在後續都將會詳細介紹。
二、使用 @Controller 定義一個 Controller 控制器
@Controller 用於標記在一個類上,使用它標記的類就是一個SpringMVC Controller 對象。分發處理器將會掃描使用了該註解的類的方法,並檢測該方法是否使用了@RequestMapping 註解。@Controller 只是定義了一個控制器類,而使用@RequestMapping 註解的方法纔是真正處理請求的處理器,這個接下來就會講到。
單單使用@Controller 標記在一個類上還不能真正意義上的說它就是SpringMVC 的一個控制器類,因爲這個時候Spring 還不認識它。那麼要如何做Spring 才能認識它呢?這個時候就需要我們把這個控制器類交給Spring 來管理。拿MyController 來舉一個例子
- @Controller
- public class MyController {
- @RequestMapping ( "/showView" )
- public ModelAndView showView() {
- ModelAndView modelAndView = new ModelAndView();
- modelAndView.setViewName( "viewName" );
- modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " );
- return modelAndView;
- }
- }
@Controllerpublic class MyController { @RequestMapping ( "/showView" ) public ModelAndView showView() { ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName( "viewName" ); modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " ); return modelAndView; }}
這個時候有兩種方式可以把MyController 交給Spring 管理,好讓它能夠識別我們標記的@Controller 。
第一種方式是在SpringMVC 的配置文件中定義MyController 的bean 對象。
<bean class="com.host.app.web.controller.MyController"/>
第二種方式是在SpringMVC 的配置文件中告訴Spring 該到哪裏去找標記爲@Controller 的Controller 控制器。
- < context:component-scan base-package = "com.host.app.web.controller" >
- < context:exclude-filter type = "annotation"
- expression = "org.springframework.stereotype.Service" />
- </ context:component-scan >
< context:component-scan base-package = "com.host.app.web.controller" > < context:exclude-filter type = "annotation" expression = "org.springframework.stereotype.Service" /> </ context:component-scan >
注:
上面 context:exclude-filter 標註的是不掃描 @Service 標註的類
三、使用 @RequestMapping 來映射 Request 請求與處理器
可以使用@RequestMapping 來映射URL 到控制器類,或者是到Controller 控制器的處理方法上。當@RequestMapping 標記在Controller 類上的時候,裏面使用@RequestMapping 標記的方法的請求地址都是相對於類上的@RequestMapping 而言的;當Controller 類上沒有標記@RequestMapping 註解時,方法上的@RequestMapping 都是絕對路徑。這種絕對路徑和相對路徑所組合成的最終路徑都是相對於根路徑“/ ”而言的。
- @Controller
- public class MyController {
- @RequestMapping ( "/showView" )
- public ModelAndView showView() {
- ModelAndView modelAndView = new ModelAndView();
- modelAndView.setViewName( "viewName" );
- modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " );
- return modelAndView;
- }
- }
@Controllerpublic class MyController { @RequestMapping ( "/showView" ) public ModelAndView showView() { ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName( "viewName" ); modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " ); return modelAndView; }}
在這個控制器中,因爲MyController 沒有被@RequestMapping 標記,所以當需要訪問到裏面使用了@RequestMapping 標記的showView 方法時,就是使用的絕對路徑/showView.do 請求就可以了。
- @Controller
- @RequestMapping ( "/test" )
- public class MyController {
- @RequestMapping ( "/showView" )
- public ModelAndView showView() {
- ModelAndView modelAndView = new ModelAndView();
- modelAndView.setViewName( "viewName" );
- modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " );
- return modelAndView;
- }
- }
@Controller@RequestMapping ( "/test" )public class MyController { @RequestMapping ( "/showView" ) public ModelAndView showView() { ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName( "viewName" ); modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " ); return modelAndView; }}
這種情況是在控制器上加了@RequestMapping 註解,所以當需要訪問到裏面使用了@RequestMapping 標記的方法showView() 的時候就需要使用showView 方法上@RequestMapping 相對於控制器MyController 上@RequestMapping 的地址,即/test/showView.do 。
(一)使用 URI 模板
URI 模板就是在URI 中給定一個變量,然後在映射的時候動態的給該變量賦值。如URI 模板http://localhost/app/{variable1}/index.html ,這個模板裏面包含一個變量variable1 ,那麼當我們請求http://localhost/app/hello/index.html 的時候,該URL 就跟模板相匹配,只是把模板中的variable1 用hello 來取代。在SpringMVC 中,這種取代模板中定義的變量的值也可以給處理器方法使用,這樣我們就可以非常方便的實現URL 的RestFul 風格。這個變量在SpringMVC 中是使用@PathVariable 來標記的。
在SpringMVC 中,我們可以使用@PathVariable 來標記一個Controller 的處理方法參數,表示該參數的值將使用URI 模板中對應的變量的值來賦值。
- @Controller
- @RequestMapping ( "/test/{variable1}" )
- public class MyController {
- @RequestMapping ( "/showView/{variable2}" )
- public ModelAndView showView( @PathVariable String variable1, @PathVariable ( "variable2" ) int variable2) {
- ModelAndView modelAndView = new ModelAndView();
- modelAndView.setViewName( "viewName" );
- modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " );
- return modelAndView;
- }
- }
@Controller@RequestMapping ( "/test/{variable1}" )public class MyController { @RequestMapping ( "/showView/{variable2}" ) public ModelAndView showView( @PathVariable String variable1, @PathVariable ( "variable2" ) int variable2) { ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName( "viewName" ); modelAndView.addObject( " 需要放到 model 中的屬性名稱 " , " 對應的屬性值,它是一個對象 " ); return modelAndView; }}
在上面的代碼中我們定義了兩個URI 變量,一個是控制器類上的variable1 ,一個是showView 方法上的variable2 ,然後在showView 方法的參數裏面使用@PathVariable 標記使用了這兩個變量。所以當我們使用/test/hello/showView/2.do 來請求的時候就可以訪問到MyController 的showView 方法,這個時候variable1 就被賦予值hello ,variable2 就被賦予值2 ,然後我們在showView 方法參數裏面標註了參數variable1 和variable2 是來自訪問路徑的path 變量,這樣方法參數variable1 和variable2 就被分別賦予hello 和2 。方法參數variable1 是定義爲String 類型,variable2 是定義爲int 類型,像這種簡單類型在進行賦值的時候Spring 是會幫我們自動轉換的,關於複雜類型該如何來轉換在後續內容中將會講到。
在上面的代碼中我們可以看到在標記variable1 爲path 變量的時候我們使用的是@PathVariable ,而在標記variable2 的時候使用的是@PathVariable(“variable2”) 。這兩者有什麼區別呢?第一種情況就默認去URI 模板中找跟參數名相同的變量,但是這種情況只有在使用debug 模式進行編譯的時候纔可以,而第二種情況是明確規定使用的就是URI 模板中的variable2 變量。當不是使用debug 模式進行編譯,或者是所需要使用的變量名跟參數名不相同的時候,就要使用第二種方式明確指出使用的是URI 模板中的哪個變量。
除了在請求路徑中使用URI 模板,定義變量之外,@RequestMapping 中還支持通配符“* ”。如下面的代碼我就可以使用/myTest/whatever/wildcard.do 訪問到Controller 的testWildcard 方法。
- @Controller
- @RequestMapping ( "/myTest" )
- public class MyController {
- @RequestMapping ( "*/wildcard" )
- public String testWildcard() {
- System. out .println( "wildcard------------" );
- return "wildcard" ;
- }
- }
@Controller@RequestMapping ( "/myTest" )public class MyController { @RequestMapping ( "*/wildcard" ) public String testWildcard() { System. out .println( "wildcard------------" ); return "wildcard" ; } }
(二)使用 @RequestParam 綁定 HttpServletRequest 請求參數到控制器方法參數
- @RequestMapping ( "requestParam" )
- ublic String testRequestParam( @RequestParam(required=false) String name, @RequestParam ( "age" ) int age) {
- return "requestParam" ;
- }
@RequestMapping ( "requestParam" ) public String testRequestParam( @RequestParam(required=false) String name, @RequestParam ( "age" ) int age) { return "requestParam" ; }
在上面代碼中利用@RequestParam 從HttpServletRequest 中綁定了參數name 到控制器方法參數name ,綁定了參數age 到控制器方法參數age 。值得注意的是和@PathVariable 一樣,當你沒有明確指定從request 中取哪個參數時,Spring 在代碼是debug 編譯的情況下會默認取更方法參數同名的參數,如果不是debug 編譯的就會報錯。此外,當需要從request 中綁定的參數和方法的參數名不相同的時候,也需要在@RequestParam 中明確指出是要綁定哪個參數。在上面的代碼中如果我訪問/requestParam.do?name=hello&age=1 則Spring 將會把request 請求參數name 的值hello 賦給對應的處理方法參數name ,把參數age 的值1 賦給對應的處理方法參數age 。
在@RequestParam 中除了指定綁定哪個參數的屬性value 之外,還有一個屬性required ,它表示所指定的參數是否必須在request 屬性中存在,默認是true ,表示必須存在,當不存在時就會報錯。在上面代碼中我們指定了參數name 的required 的屬性爲false ,而沒有指定age 的required 屬性,這時候如果我們訪問/requestParam.do 而沒有傳遞參數的時候,系統就會拋出異常,因爲age 參數是必須存在的,而我們沒有指定。而如果我們訪問/requestParam.do?age=1 的時候就可以正常訪問,因爲我們傳遞了必須的參數age ,而參數name 是非必須的,不傳遞也可以。
(三)使用 @CookieValue 綁定 cookie 的值到 Controller 方法參數
- @RequestMapping ( "cookieValue" )
- public String testCookieValue( @CookieValue ( "hello" ) String cookieValue, @CookieValue String hello) {
- System. out .println(cookieValue + "-----------" + hello);
- return "cookieValue" ;
- }
@RequestMapping ( "cookieValue" ) public String testCookieValue( @CookieValue ( "hello" ) String cookieValue, @CookieValue String hello) { System. out .println(cookieValue + "-----------" + hello); return "cookieValue" ; }
在上面的代碼中我們使用@CookieValue 綁定了cookie 的值到方法參數上。上面一共綁定了兩個參數,一個是明確指定要綁定的是名稱爲hello 的cookie 的值,一個是沒有指定。使用沒有指定的形式的規則和@PathVariable 、@RequestParam 的規則是一樣的,即在debug 編譯模式下將自動獲取跟方法參數名同名的cookie 值。
(四)使用 @RequestHeader 註解綁定 HttpServletRequest 頭信息到 Controller 方法參數
- @RequestMapping ( "testRequestHeader" )
- public String testRequestHeader( @RequestHeader ( "Host" ) String hostAddr, @RequestHeader String Host, @RequestHeader String host ) {
- System. out .println(hostAddr + "-----" + Host + "-----" + host );
- return "requestHeader" ;
- }
@RequestMapping ( "testRequestHeader" )public String testRequestHeader( @RequestHeader ( "Host" ) String hostAddr, @RequestHeader String Host, @RequestHeader String host ) { System. out .println(hostAddr + "-----" + Host + "-----" + host ); return "requestHeader" ;}
在上面的代碼中我們使用了 @RequestHeader 綁定了 HttpServletRequest 請求頭 host 到 Controller 的方法參數。上面方法的三個參數都將會賦予同一個值,由此我們可以知道在綁定請求頭參數到方法參數的時候規則和 @PathVariable 、 @RequestParam 以及 @CookieValue 是一樣的,即沒有指定綁定哪個參數到方法參數的時候,在 debug 編譯模式下將使用方法參數名作爲需要綁定的參數。但是有一點 @RequestHeader 跟另外三種綁定方式是不一樣的,那就是在使用 @RequestHeader 的時候是大小寫不敏感的,即 @RequestHeader(“Host”) 和 @RequestHeader(“host”) 綁定的都是 Host 頭信息。記住在 @PathVariable 、 @RequestParam 和 @CookieValue 中都是大小寫敏感的。
(五) @RequestMapping 的一些高級應用
在RequestMapping 中除了指定請求路徑value 屬性外,還有其他的屬性可以指定,如params 、method 和headers 。這樣屬性都可以用於縮小請求的映射範圍。
1.params屬性
params 屬性用於指定請求參數的,先看以下代碼。
- @RequestMapping (value= "testParams" , params={ "param1=value1" , "param2" , "!param3" })
- public String testParams() {
- System. out .println( "test Params..........." );
- return "testParams" ;
- }
@RequestMapping (value= "testParams" , params={ "param1=value1" , "param2" , "!param3" }) public String testParams() { System. out .println( "test Params..........." ); return "testParams" ; }
在上面的代碼中我們用@RequestMapping 的params 屬性指定了三個參數,這些參數都是針對請求參數而言的,它們分別表示參數param1 的值必須等於value1 ,參數param2 必須存在,值無所謂,參數param3 必須不存在,只有當請求/testParams.do 並且滿足指定的三個參數條件的時候才能訪問到該方法。所以當請求/testParams.do?param1=value1¶m2=value2 的時候能夠正確訪問到該testParams 方法,當請求/testParams.do?param1=value1¶m2=value2¶m3=value3 的時候就不能夠正常的訪問到該方法,因爲在@RequestMapping 的params 參數裏面指定了參數param3 是不能存在的。
2.method屬性
method 屬性主要是用於限制能夠訪問的方法類型的。
- @RequestMapping (value= "testMethod" , method={RequestMethod. GET , RequestMethod. DELETE })
- public String testMethod() {
- return "method" ;
- }
@RequestMapping (value= "testMethod" , method={RequestMethod. GET , RequestMethod. DELETE }) public String testMethod() { return "method" ; }
在上面的代碼中就使用method 參數限制了以GET 或DELETE 方法請求/testMethod.do 的時候才能訪問到該Controller 的testMethod 方法。
3.headers屬性
使用headers 屬性可以通過請求頭信息來縮小@RequestMapping 的映射範圍。
- @RequestMapping (value= "testHeaders" , headers={ "host=localhost" , "Accept" })
- public String testHeaders() {
- return "headers" ;
- }
@RequestMapping (value= "testHeaders" , headers={ "host=localhost" , "Accept" }) public String testHeaders() { return "headers" ; }
headers 屬性的用法和功能與params 屬性相似。在上面的代碼中當請求/testHeaders.do 的時候只有當請求頭包含Accept 信息,且請求的host 爲localhost 的時候才能正確的訪問到testHeaders 方法。
(六)以 @RequestMapping 標記的處理器方法支持的方法參數和返回類型
1. 支持的方法參數類型
(1 )HttpServlet 對象,主要包括HttpServletRequest 、HttpServletResponse 和HttpSession 對象。 這些參數Spring 在調用處理器方法的時候會自動給它們賦值,所以當在處理器方法中需要使用到這些對象的時候,可以直接在方法上給定一個方法參數的申明,然後在方法體裏面直接用就可以了。但是有一點需要注意的是在使用HttpSession 對象的時候,如果此時HttpSession 對象還沒有建立起來的話就會有問題。
(2 )Spring 自己的WebRequest 對象。 使用該對象可以訪問到存放在HttpServletRequest 和HttpSession 中的屬性值。
(3 )InputStream 、OutputStream 、Reader 和Writer 。 InputStream 和Reader 是針對HttpServletRequest 而言的,可以從裏面取數據;OutputStream 和Writer 是針對HttpServletResponse 而言的,可以往裏面寫數據。
(4 )使用@PathVariable 、@RequestParam 、@CookieValue 和@RequestHeader 標記的參數。
(5 )使用@ModelAttribute 標記的參數。
(6 )java.util.Map 、Spring 封裝的Model 和ModelMap 。 這些都可以用來封裝模型數據,用來給視圖做展示。
(7 )實體類。 可以用來接收上傳的參數。
(8 )Spring 封裝的MultipartFile 。 用來接收上傳文件的。
(9 )Spring 封裝的Errors 和BindingResult 對象。 這兩個對象參數必須緊接在需要驗證的實體對象參數之後,它裏面包含了實體對象的驗證結果。
2. 支持的返回類型
(1 )一個包含模型和視圖的ModelAndView 對象。
(2 )一個模型對象,這主要包括Spring 封裝好的Model 和ModelMap ,以及java.util.Map ,當沒有視圖返回的時候視圖名稱將由RequestToViewNameTranslator 來決定。
(3 )一個View 對象。這個時候如果在渲染視圖的過程中模型的話就可以給處理器方法定義一個模型參數,然後在方法體裏面往模型中添加值。
(4 )一個String 字符串。這往往代表的是一個視圖名稱。這個時候如果需要在渲染視圖的過程中需要模型的話就可以給處理器方法一個模型參數,然後在方法體裏面往模型中添加值就可以了。
(5 )返回值是void 。這種情況一般是我們直接把返回結果寫到HttpServletResponse 中了,如果沒有寫的話,那麼Spring 將會利用RequestToViewNameTranslator 來返回一個對應的視圖名稱。如果視圖中需要模型的話,處理方法與返回字符串的情況相同。
(6 )如果處理器方法被註解@ResponseBody 標記的話,那麼處理器方法的任何返回類型都會通過HttpMessageConverters 轉換之後寫到HttpServletResponse 中,而不會像上面的那些情況一樣當做視圖或者模型來處理。
(7 )除以上幾種情況之外的其他任何返回類型都會被當做模型中的一個屬性來處理,而返回的視圖還是由RequestToViewNameTranslator 來決定,添加到模型中的屬性名稱可以在該方法上用@ModelAttribute(“attributeName”) 來定義,否則將使用返回類型的類名稱的首字母小寫形式來表示。使用@ModelAttribute 標記的方法會在@RequestMapping 標記的方法執行之前執行。
(七)使用 @ModelAttribute 和 @SessionAttributes 傳遞和保存數據
SpringMVC 支持使用 @ModelAttribute 和 @SessionAttributes 在不同的模型和控制器之間共享數據。 @ModelAttribute 主要有兩種使用方式,一種是標註在方法上,一種是標註在 Controller 方法參數上。
當 @ModelAttribute 標記在方法上的時候,該方法將在處理器方法執行之前執行,然後把返回的對象存放在 session 或模型屬性中,屬性名稱可以使用 @ModelAttribute(“attributeName”) 在標記方法的時候指定,若未指定,則使用返回類型的類名稱(首字母小寫)作爲屬性名稱。關於 @ModelAttribute 標記在方法上時對應的屬性是存放在 session 中還是存放在模型中,我們來做一個實驗,看下面一段代碼。
- @Controller
- @RequestMapping ( "/myTest" )
- public class MyController {
- @ModelAttribute ( "hello" )
- public String getModel() {
- System. out .println( "-------------Hello---------" );
- return "world" ;
- }
- @ModelAttribute ( "intValue" )
- public int getInteger() {
- System. out .println( "-------------intValue---------------" );
- return 10;
- }
- @RequestMapping ( "sayHello" )
- public void sayHello( @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpSession session) throws IOException {
- writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num);
- writer.write( "\r" );
- Enumeration enume = session.getAttributeNames();
- while (enume.hasMoreElements())
- writer.write(enume.nextElement() + "\r" );
- }
- @ModelAttribute ( "user2" )
- public User getUser() {
- System. out .println( "---------getUser-------------" );
- return new User(3, "user2" );
- }
- }
@Controller@RequestMapping ( "/myTest" )public class MyController { @ModelAttribute ( "hello" ) public String getModel() { System. out .println( "-------------Hello---------" ); return "world" ; } @ModelAttribute ( "intValue" ) public int getInteger() { System. out .println( "-------------intValue---------------" ); return 10; } @RequestMapping ( "sayHello" ) public void sayHello( @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpSession session) throws IOException { writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num); writer.write( "\r" ); Enumeration enume = session.getAttributeNames(); while (enume.hasMoreElements()) writer.write(enume.nextElement() + "\r" ); } @ModelAttribute ( "user2" ) public User getUser() { System. out .println( "---------getUser-------------" ); return new User(3, "user2" ); }}
當我們請求 /myTest/sayHello.do 的時候使用 @ModelAttribute 標記的方法會先執行,然後把它們返回的對象存放到模型中。最終訪問到 sayHello 方法的時候,使用 @ModelAttribute 標記的方法參數都能被正確的注入值。執行結果如下圖所示:
由執行結果我們可以看出來,此時 session 中沒有包含任何屬性,也就是說上面的那些對象都是存放在模型屬性中,而不是存放在 session 屬性中。那要如何才能存放在 session 屬性中呢?這個時候我們先引入一個新的概念 @SessionAttributes ,它的用法會在講完 @ModelAttribute 之後介紹,這裏我們就先拿來用一下。我們在 MyController 類上加上 @SessionAttributes 屬性標記哪些是需要存放到 session 中的。看下面的代碼:
- @Controller
- @RequestMapping ( "/myTest" )
- @SessionAttributes (value={ "intValue" , "stringValue" }, types={User. class })
- public class MyController {
- @ModelAttribute ( "hello" )
- public String getModel() {
- System. out .println( "-------------Hello---------" );
- return "world" ;
- }
- @ModelAttribute ( "intValue" )
- public int getInteger() {
- System. out .println( "-------------intValue---------------" );
- return 10;
- }
- @RequestMapping ( "sayHello" )
- public void sayHello(Map<String, Object> map, @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpServletRequest request) throws IOException {
- map.put( "stringValue" , "String" );
- writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num);
- writer.write( "\r" );
- HttpSession session = request.getSession();
- Enumeration enume = session.getAttributeNames();
- while (enume.hasMoreElements())
- writer.write(enume.nextElement() + "\r" );
- System. out .println(session);
- }
- @ModelAttribute ( "user2" )
- public User getUser() {
- System. out .println( "---------getUser-------------" );
- return new User(3, "user2" );
- }
- }
@Controller@RequestMapping ( "/myTest" )@SessionAttributes (value={ "intValue" , "stringValue" }, types={User. class })public class MyController { @ModelAttribute ( "hello" ) public String getModel() { System. out .println( "-------------Hello---------" ); return "world" ; } @ModelAttribute ( "intValue" ) public int getInteger() { System. out .println( "-------------intValue---------------" ); return 10; } @RequestMapping ( "sayHello" ) public void sayHello(Map<String, Object> map, @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpServletRequest request) throws IOException { map.put( "stringValue" , "String" ); writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num); writer.write( "\r" ); HttpSession session = request.getSession(); Enumeration enume = session.getAttributeNames(); while (enume.hasMoreElements()) writer.write(enume.nextElement() + "\r" ); System. out .println(session); } @ModelAttribute ( "user2" ) public User getUser() { System. out .println( "---------getUser-------------" ); return new User(3, "user2" ); }}
在上面代碼中我們指定了屬性爲 intValue 或 stringValue 或者類型爲 User 的都會放到 Session 中,利用上面的代碼當我們訪問 /myTest/sayHello.do 的時候,結果如下:
仍然沒有打印出任何 session 屬性,這是怎麼回事呢?怎麼定義了把模型中屬性名爲 intValue 的對象和類型爲 User 的對象存到 session 中,而實際上沒有加進去呢?難道我們錯啦?我們當然沒有錯,只是在第一次訪問 /myTest/sayHello.do 的時候 @SessionAttributes 定義了需要存放到 session 中的屬性,而且這個模型中也有對應的屬性,但是這個時候還沒有加到 session 中,所以 session 中不會有任何屬性,等處理器方法執行完成後 Spring 纔會把模型中對應的屬性添加到 session 中。所以當請求第二次的時候就會出現如下結果:
當 @ModelAttribute 標記在處理器方法參數上的時候,表示該參數的值將從模型或者 Session 中取對應名稱的屬性值,該名稱可以通過 @ModelAttribute(“attributeName”) 來指定,若未指定,則使用參數類型的類名稱(首字母小寫)作爲屬性名稱。
- @Controller
- @RequestMapping ( "/myTest" )
- public class MyController {
- @ModelAttribute ( "hello" )
- public String getModel() {
- return "world" ;
- }
- @RequestMapping ( "sayHello" )
- public void sayHello( @ModelAttribute ( "hello" ) String hello, Writer writer) throws IOException {
- writer.write( "Hello " + hello);
- }
- }
@Controller@RequestMapping ( "/myTest" )public class MyController { @ModelAttribute ( "hello" ) public String getModel() { return "world" ; } @RequestMapping ( "sayHello" ) public void sayHello( @ModelAttribute ( "hello" ) String hello, Writer writer) throws IOException { writer.write( "Hello " + hello); } }
在上面代碼中,當我們請求/myTest/sayHello.do 的時候,由於MyController 中的方法getModel 使用了註解@ModelAttribute 進行標記,所以在執行請求方法sayHello 之前會先執行getModel 方法,這個時候getModel 方法返回一個字符串world 並把它以屬性名hello 保存在模型中,接下來訪問請求方法sayHello 的時候,該方法的hello 參數使用@ModelAttribute(“hello”) 進行標記,這意味着將從session 或者模型中取屬性名稱爲hello 的屬性值賦給hello 參數,所以這裏hello 參數將被賦予值world ,所以請求完成後將會在頁面上看到Hello world 字符串。
@SessionAttributes 用於標記需要在Session 中使用到的數據,包括從Session 中取數據和存數據。@SessionAttributes 一般是標記在Controller 類上的,可以通過名稱、類型或者名稱加類型的形式來指定哪些屬性是需要存放在session 中的。
- @Controller
- @RequestMapping ( "/myTest" )
- @SessionAttributes (value={ "user1" , "blog1" }, types={User. class , Blog. class })
- public class MyController {
- @RequestMapping ( "setSessionAttribute" )
- public void setSessionAttribute(Map<String, Object> map, Writer writer) throws IOException {
- User user = new User(1, "user" );
- User user1 = new User(2, "user1" );
- Blog blog = new Blog(1, "blog" );
- Blog blog1 = new Blog(2, "blog1" );
- map.put( "user" , user);
- map.put( "user1" , user1);
- map.put( "blog" , blog);
- map.put( "blog1" , blog1);
- writer.write( "over." );
- }
- @RequestMapping ( "useSessionAttribute" )
- public void useSessionAttribute(Writer writer, @ModelAttribute ( "user1" ) User user1, @ModelAttribute ( "blog1" ) Blog blog1) throws IOException {
- writer.write(user1.getId() + "--------" + user1.getUsername());
- writer.write( "\r" );
- writer.write(blog1.getId() + "--------" + blog1.getTitle());
- }
- @RequestMapping ( "useSessionAttribute2" )
- public void useSessionAttribute(Writer writer, @ModelAttribute ( "user1" ) User user1, @ModelAttribute ( "blog1" ) Blog blog1, @ModelAttribute User user, HttpSession session) throws IOException {
- writer.write(user1.getId() + "--------" + user1.getUsername());
- writer.write( "\r" );
- writer.write(blog1.getId() + "--------" + blog1.getTitle());
- writer.write( "\r" );
- writer.write(user.getId() + "---------" + user.getUsername());
- writer.write( "\r" );
- Enumeration enume = session.getAttributeNames();
- while (enume.hasMoreElements())
- writer.write(enume.nextElement() + " \r" );
- }
- @RequestMapping ( "useSessionAttribute3" )
- public void useSessionAttribute( @ModelAttribute ( "user2" ) User user) {
- }
- }
@Controller@RequestMapping ( "/myTest" )@SessionAttributes (value={ "user1" , "blog1" }, types={User. class , Blog. class })public class MyController { @RequestMapping ( "setSessionAttribute" ) public void setSessionAttribute(Map<String, Object> map, Writer writer) throws IOException { User user = new User(1, "user" ); User user1 = new User(2, "user1" ); Blog blog = new Blog(1, "blog" ); Blog blog1 = new Blog(2, "blog1" ); map.put( "user" , user); map.put( "user1" , user1); map.put( "blog" , blog); map.put( "blog1" , blog1); writer.write( "over." ); } @RequestMapping ( "useSessionAttribute" ) public void useSessionAttribute(Writer writer, @ModelAttribute ( "user1" ) User user1, @ModelAttribute ( "blog1" ) Blog blog1) throws IOException { writer.write(user1.getId() + "--------" + user1.getUsername()); writer.write( "\r" ); writer.write(blog1.getId() + "--------" + blog1.getTitle()); } @RequestMapping ( "useSessionAttribute2" ) public void useSessionAttribute(Writer writer, @ModelAttribute ( "user1" ) User user1, @ModelAttribute ( "blog1" ) Blog blog1, @ModelAttribute User user, HttpSession session) throws IOException { writer.write(user1.getId() + "--------" + user1.getUsername()); writer.write( "\r" ); writer.write(blog1.getId() + "--------" + blog1.getTitle()); writer.write( "\r" ); writer.write(user.getId() + "---------" + user.getUsername()); writer.write( "\r" ); Enumeration enume = session.getAttributeNames(); while (enume.hasMoreElements()) writer.write(enume.nextElement() + " \r" ); } @RequestMapping ( "useSessionAttribute3" ) public void useSessionAttribute( @ModelAttribute ( "user2" ) User user) { }}
在上面代碼中我們可以看到在MyController 上面使用了@SessionAttributes 標記了需要使用到的Session 屬性。可以通過名稱和類型指定需要存放到Session 中的屬性,對應@SessionAttributes 註解的value 和types 屬性。當使用的是types 屬性的時候,那麼使用的Session 屬性名稱將會是對應類型的名稱(首字母小寫)。當value 和types 兩個屬性都使用到了的時候,這時候取的是它們的並集,而不是交集,所以上面代碼中指定要存放在Session 中的屬性有名稱爲user1 或blog1 的對象,或類型爲User 或Blog 的對象。在上面代碼中我們首先訪問/myTest/setSessionAttribute.do ,該請求將會請求到MyController 的setSessionAttribute 方法,在該方法中,我們往模型裏面添加了user 、user1 、blog 和blog1 四個屬性,因爲它們或跟類上的@SessionAttributes 定義的需要存到session 中的屬性名稱相同或類型相同,所以在請求完成後這四個屬性都將添加到session 屬性中。接下來訪問/myTest/useSessionAttribute.do ,該請求將會請求MyController 的useSessionAttribute(Writer writer, @ModelAttribute(“user1”) User user1, @ModelAttribute(“blog1”) Blog blog) 方法,該方法參數中用@ModelAttribute 指定了參數user1 和參數blog1 是需要從session 或模型中綁定的,恰好這個時候session 中已經有了這兩個屬性,所以這個時候在方法執行之前會先綁定這兩個參數。執行結果如下圖所示:
接下來訪問/myTest/useSessionAttribute2.do ,這個時候請求的是上面代碼中對應的第二個useSessionAttribute 方法,方法參數user 、user1 和blog1 用@ModelAttribute 聲明瞭需要session 或模型屬性注入,我們知道在請求/myTest/setSessionAttribute.do 的時候這些屬性都已經添加到了session 中,所以該請求的結果會如下圖所示:
接下來訪問/myTest/useSessionAttribute3.do ,這個時候請求的是上面代碼中對應的第三個useSessionAttribute 方法,我們可以看到該方法的方法參數user 使用了@ModelAttribute(“user2”) 進行標記,表示user 需要session 中的user2 屬性來注入,但是這個時候我們知道session 中是不存在user2 屬性的,所以這個時候就會報錯了。執行結果如圖所示:
(八)定製自己的類型轉換器
在通過處理器方法參數接收 request 請求參數綁定數據的時候,對於一些簡單的數據類型 Spring 會幫我們自動進行類型轉換,而對於一些複雜的類型由於 Spring 沒法識別,所以也就不能幫助我們進行自動轉換了,這個時候如果我們需要 Spring 來幫我們自動轉換的話就需要我們給 Spring 註冊一個對特定類型的識別轉換器。 Spring 允許我們提供兩種類型的識別轉換器,一種是註冊在 Controller 中的,一種是註冊在 SpringMVC 的配置文件中。聰明的讀者看到這裏應該可以想到它們的區別了,定義在 Controller 中的是局部的,只在當前 Controller 中有效,而放在 SpringMVC 配置文件中的是全局的,所有 Controller 都可以拿來使用。
1. 在 @InitBinder 標記的方法中定義局部的類型轉換器
我們可以使用 @InitBinder 註解標註在 Controller 方法上,然後在方法體裏面註冊數據綁定的轉換器,這主要是通過 WebDataBinder 進行的。我們可以給需要註冊數據綁定的轉換器的方法一個 WebDataBinder 參數,然後給該方法加上 @InitBinder 註解,這樣當該 Controller 中在處理請求方法時如果發現有不能解析的對象的時候,就會看該類中是否有使用 @InitBinder 標記的方法,如果有就會執行該方法,然後看裏面定義的類型轉換器是否與當前需要的類型匹配。
- @Controller
- @RequestMapping ( "/myTest" )
- public class MyController {
- @InitBinder
- public void dataBinder(WebDataBinder binder) {
- DateFormat dateFormat = new SimpleDateFormat( "yyyyMMdd" );
- PropertyEditor propertyEditor = new CustomDateEditor(dateFormat, true ); // 第二個參數表示是否允許爲空
- binder.registerCustomEditor(Date. class , propertyEditor);
- }
- @RequestMapping ( "dataBinder/{date}" )
- public void testDate( @PathVariable Date date, Writer writer) throws IOException {
- writer.write(String.valueOf (date.getTime()));
- }
- }
@Controller@RequestMapping ( "/myTest" )public class MyController { @InitBinder public void dataBinder(WebDataBinder binder) { DateFormat dateFormat = new SimpleDateFormat( "yyyyMMdd" ); PropertyEditor propertyEditor = new CustomDateEditor(dateFormat, true ); // 第二個參數表示是否允許爲空 binder.registerCustomEditor(Date. class , propertyEditor); } @RequestMapping ( "dataBinder/{date}" ) public void testDate( @PathVariable Date date, Writer writer) throws IOException { writer.write(String.valueOf (date.getTime())); }}
在上面的代碼中當我們請求 /myTest/dataBinder/20121212.do 的時候, Spring 就會利用 @InitBinder 標記的方法裏面定義的類型轉換器把字符串 20121212 轉換爲一個 Date 對象。這樣定義的類型轉換器是局部的類型轉換器,一旦出了這個 Controller 就不會再起作用。類型轉換器是通過 WebDataBinder 對象的 registerCustomEditor 方法來註冊的,要實現自己的類型轉換器就要實現自己的 PropertyEditor 對象。 Spring 已經給我們提供了一些常用的屬性編輯器,如 CustomDateEditor 、 CustomBooleanEditor 等。
PropertyEditor 是一個接口,要實現自己的 PropertyEditor 類我們可以實現這個接口,然後實現裏面的方法。但是 PropertyEditor 裏面定義的方法太多了,這樣做比較麻煩。在 java 中有一個封裝類是實現了 PropertyEditor 接口的,它是 PropertyEditorSupport 類。所以如果需要實現自己的 PropertyEditor 的時候只需要繼承 PropertyEditorSupport 類,然後重寫其中的一些方法。一般就是重寫 setAsText 和 getAsText 方法就可以了, setAsText 方法是用於把字符串類型的值轉換爲對應的對象的,而 getAsText 方法是用於把對象當做字符串來返回的。在 setAsText 中我們一般先把字符串類型的對象轉爲特定的對象,然後利用 PropertyEditor 的 setValue 方法設定轉換後的值。在 getAsText 方法中一般先使用 getValue 方法取代當前的對象,然後把它轉換爲字符串後再返回給 getAsText 方法。下面是一個示例:
- @InitBinder
- public void dataBinder(WebDataBinder binder) {
- // 定義一個 User 屬性編輯器
- PropertyEditor userEditor = new PropertyEditorSupport() {
- @Override
- public String getAsText() {
- // TODO Auto-generated method stub
- User user = (User) getValue();
- return user.getUsername();
- }
- @Override
- public void setAsText(String userStr) throws IllegalArgumentException {
- // TODO Auto-generated method stub
- User user = new User(1, userStr);
- setValue(user);
- }
- };
- // 使用 WebDataBinder 註冊 User 類型的屬性編輯器
- binder.registerCustomEditor(User. class , userEditor);
- }
@InitBinder public void dataBinder(WebDataBinder binder) { // 定義一個 User 屬性編輯器 PropertyEditor userEditor = new PropertyEditorSupport() { @Override public String getAsText() { // TODO Auto-generated method stub User user = (User) getValue(); return user.getUsername(); } @Override public void setAsText(String userStr) throws IllegalArgumentException { // TODO Auto-generated method stub User user = new User(1, userStr); setValue(user); } }; // 使用 WebDataBinder 註冊 User 類型的屬性編輯器 binder.registerCustomEditor(User. class , userEditor); }
2. 實現 WebBindingInitializer 接口定義全局的類型轉換器
如果需要定義全局的類型轉換器就需要實現自己的 WebBindingInitializer 對象,然後把該對象注入到 AnnotationMethodHandlerAdapter 中,這樣 Spring 在遇到自己不能解析的對象的時候就會到全局的 WebBindingInitializer 的 initBinder 方法中去找,每次遇到不認識的對象時, initBinder 方法都會被執行一遍。
- public class MyWebBindingInitializer implements WebBindingInitializer {
- @Override
- public void initBinder(WebDataBinder binder, WebRequest request) {
- // TODO Auto-generated method stub
- DateFormat dateFormat = new SimpleDateFormat( "yyyyMMdd" );
- PropertyEditor propertyEditor = new CustomDateEditor(dateFormat, true );
- binder.registerCustomEditor(Date. class , propertyEditor);
- }
- }
public class MyWebBindingInitializer implements WebBindingInitializer { @Override public void initBinder(WebDataBinder binder, WebRequest request) { // TODO Auto-generated method stub DateFormat dateFormat = new SimpleDateFormat( "yyyyMMdd" ); PropertyEditor propertyEditor = new CustomDateEditor(dateFormat, true ); binder.registerCustomEditor(Date. class , propertyEditor); }}
定義了這麼一個 WebBindingInitializer 對象之後 Spring 還是不能識別其中指定的對象,這是因爲我們只是定義了 WebBindingInitializer 對象,還沒有把它交給 Spring , Spring 不知道該去哪裏找解析器。要讓 Spring 能夠識別還需要我們在 SpringMVC 的配置文件中定義一個 AnnotationMethodHandlerAdapter 類型的 bean 對象,然後利用自己定義的 WebBindingInitializer 覆蓋它的默認屬性 webBindingInitializer 。
- < bean class = "org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" >
- < property name = "webBindingInitializer" >
- < bean class = "com.host.app.web.util.MyWebBindingInitializer" />
- </ property >
- </ bean >
< bean class = "org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" > < property name = "webBindingInitializer" > < bean class = "com.host.app.web.util.MyWebBindingInitializer" /> </ property > </ bean >
3.觸發數據綁定方法的時間
當Controller處理器方法參數使用@RequestParam、@PathVariable、@RequestHeader、@CookieValue和@ModelAttribute標記的時候都會觸發initBinder方法的執行,這包括使用WebBindingInitializer定義的全局方法和在Controller中使用@InitBinder標記的局部方法。而且每個使用了這幾個註解標記的參數都會觸發一次initBinder方法的執行,這也意味着有幾個參數使用了上述註解就會觸發幾次initBinder方法的執行。