第六章 註解式控制器詳解

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代碼:
  1. package cn.javass.chapter6.web.controller;  
  2. //省略import  
  3. @Controller         // 或 @RequestMapping               //①將一個POJO類聲明爲處理器  
  4. public class HelloWorldController {  
  5.     @RequestMapping(value = "/hello")                  //②請求URL到處理器功能處理方法的映射  
  6.     public ModelAndView helloWorld() {  
  7.         //1、收集參數  
  8.         //2、綁定參數到命令對象  
  9.         //3、調用業務對象  
  10.         //4、選擇下一個頁面  
  11.         ModelAndView mv = new ModelAndView();  
  12.         //添加模型數據 可以是任意的POJO對象  
  13.         mv.addObject("message""Hello World!");  
  14.         //設置邏輯視圖名,視圖解析器會根據該名字解析到具體的視圖頁面  
  15.         mv.setViewName("hello");  
  16.         return mv;                                         //○3 模型數據和邏輯視圖名  
  17.     }  
  18. }  


1 可以通過在一個POJO類上放置@Controller或@RequestMapping,即可把一個POJO類變身爲處理器;

@RequestMapping(value = "/hello") 請求URL(/hello) 到 處理器的功能處理方法的映射;

3 模型數據和邏輯視圖名的返回。

 

現在的處理器無需實現/繼承任何接口/類,只需要在相應的類/方法上放置相應的註解說明下即可,非常方便。

 

(2、Spring配置文件chapter6-servlet.xml

(2.1、HandlerMapping和HandlerAdapter的配置

如果您使用的是Spring3.1之前版本,開啓註解式處理器支持的配置爲:DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter。


java代碼:
  1. <!—Spring3.1之前的註解 HandlerMapping -->  
  2. <bean   
  3. class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>  
  4.   
  5. <!—Spring3.1之前的註解 HandlerAdapter -->  
  6. <bean   
  7. class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>  
  8.       

 

如果您使用的Spring3.1開始的版本,建議使用RequestMappingHandlerMapping和RequestMappingHandlerAdapter。

 

 

java代碼:
  1. <!--Spring3.1開始的註解 HandlerMapping -->  
  2. <bean   
  3. class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>  
  4. <!--Spring3.1開始的註解 HandlerAdapter -->  
  5. <bean  
  6. class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>  

下一章我們介紹DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter 與RequestMappingHandlerMapping和RequestMappingHandlerAdapter 的區別。

 

(2.2、視圖解析器的配置

還是使用之前的org.springframework.web.servlet.view.InternalResourceViewResolver。

(2.3、處理器的配置

 

java代碼:
  1. <!-- 處理器 -->  
  2. <bean class="cn.javass.chapter6.web.controller.HelloWorldController"/>  

 

只需要將處理器實現類註冊到spring配置文件即可,spring的DefaultAnnotationHandlerMapping或RequestMappingHandlerMapping能根據註解@Controller或@RequestMapping自動發現。

 

(2.3、視圖頁面(/WEB-INF/jsp/hello.jsp)

 

java代碼:
  1. <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>  
  2. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">  
  3. <html>  
  4. <head>  
  5. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">  
  6. <title>Hello World</title>  
  7. </head>  
  8. <body>  
  9. ${message}  
  10. </body>  
  11. </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實現:使用AnnotationMethodHandlerAdapter(spring3.1之前)或RequestMappingHandlerAdapter(spring3.1)替換之前的SimpleControllerHandlerAdapter。

註解式處理器適配器會通過反射調用相應的功能處理方法(方法上擁有@RequestMapping註解)。

 

好了到此我們知道Spring如何發現處理器、如何調用處理的功能處理方法了,接下來我們詳細學習下如何定義處理器、如何進行請求到功能處理方法的定義。

 

6.4、處理器定義

6.4.1、@Controller

 

java代碼:
  1. @Controller  
  2. public class HelloWorldController {  
  3. ……  
  4. }  

推薦使用這種方式聲明處理器,它和我們的@Service、@Repository很好的對應了我們常見的三層開發架構的組件。

6.4.2、@RequestMapping

 

java代碼:
  1. @RequestMapping  
  2. public class HelloWorldController {  
  3. ……  
  4. }  

這種方式也是可以工作的,但如果在類上使用@ 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代碼:
  1. package cn.javass.chapter6.web.controller;  
  2. @Controller  
  3. @RequestMapping(value="/user")                 //①處理器的通用映射前綴  
  4. public class HelloWorldController2 {  
  5.     @RequestMapping(value = "/hello2")        //②相對於①處的映射進行窄化  
  6.     public ModelAndView helloWorld() {  
  7.          //省略實現  
  8.     }  
  9. }  

①類上的@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”的請求。

 

接下來看看具體如何映射吧。


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”的請求。

 

接下來看看具體如何映射吧。

 

6.5.1、URL路徑映射

6.5.1.1、普通URL路徑映射

@RequestMapping(value={"/test1", "/user/create"}):多個URL路徑可以映射到同一個處理器的功能處理方法。

6.5.1.2、URI模板模式映射

@RequestMapping(value="/users/{userId}"):{×××}佔位符, 請求的URL可以是 “/users/123456”或

“/users/abcd”,通過6.6.5講的通過@PathVariable可以提取URI模板模式中的{×××}中的×××變量。

@RequestMapping(value="/users/{userId}/create"):這樣也是可以的,請求的URL可以是“/users/123/create”。

@RequestMapping(value="/users/{userId}/topics/{topicId}"):這樣也是可以的,請求的URL可以是“/users/123/topics/123”。

6.5.1.3、Ant風格的URL路徑映射

@RequestMapping(value="/users/**"):可以匹配“/users/abc/abc”,但“/users/123”將會被【URI模板模式映射中的“/users/{userId}”模式優先映射到】【詳見4.14的最長匹配優先】。

 

@RequestMapping(value="/product?"):可匹配“/product1”或“/producta”,但不匹配“/product”或“/productaa”;

@RequestMapping(value="/product*"):可匹配“/productabc”或“/product”,但不匹配“/productabc/abc”;

@RequestMapping(value="/product/*"):可匹配“/product/abc”,但不匹配“/productabc”;

@RequestMapping(value="/products/**/{productId}"):可匹配“/products/abc/abc/123”或“/products/123”,也就是Ant風格和URI模板變量風格可混用;

 

此處需要注意的是【4.14中提到的最長匹配優先】,Ant風格的模式請參考4.14。

6.5.1.4、正則表達式風格的URL路徑映射

從Spring3.0開始支持正則表達式風格的URL路徑映射,格式爲{變量名:正則表達式},這樣我們就可以通過6.6.5講的通過@PathVariable提取模式中的{×××:正則表達式匹配的值}中的×××變量了。

 

@RequestMapping(value="/products/{categoryCode:\\d+}-{pageNumber:\\d+}"):可以匹配“/products/123-1”,但不能匹配“/products/abc-1”,這樣可以設計更加嚴格的規則。

 

正則表達式風格的URL路徑映射是一種特殊的URI模板模式映射:

URI模板模式映射是{userId},不能指定模板變量的數據類型,如是數字還是字符串;

正則表達式風格的URL路徑映射,可以指定模板變量的數據類型,可以將規則寫的相當複雜。

 

6.5.1.5、組合使用是“或”的關係

如 @RequestMapping(value={"/test1", "/user/create"}) 組合使用是或的關係,即“/test1”或“/user/create”請求URL路徑都可以映射到@RequestMapping指定的功能處理方法。

 

以上URL映射的測試類爲:cn.javass.chapter6.web.controller.mapping.MappingController.java。

 

到此,我們學習了Spring Web MVC提供的強大的URL路徑映射,而且可以實現非常複雜的URL規則。Spring Web MVC不僅僅提供URL路徑映射,還提供了其他強大的映射規則。接下來我們看一下請求方法映射限定吧。

 

 

6.5.2、請求方法映射限定

一般我們熟悉的表單一般分爲兩步:第一步展示,第二步提交,如4.9、SimpleFormController那樣,那如何通過@RequestMapping來實現呢?

6.5.2.1、請求方法映射限定

我們熟知的,展示表單一般爲GET請求方法;提交表單一般爲POST請求方法。但6.5.1節講的URL路徑映射方式對任意請求方法是全盤接受的,因此我們需要某種方式來告訴相應的功能處理方法只處理如GET請求方法的請求或POST請求方法的請求。

 

接下來我們使用@RequestMapping來實現SimpleFormController的功能吧。

 

package cn.javass.chapter6.web.controller.method;
//省略import
@Controller
@RequestMapping("/customers/**")                                     //①處理器的通用映射前綴
public class RequestMethodController {
    @RequestMapping(value="/create", method = RequestMethod.GET)//②類級別的@RequestMapping窄化
    public String showForm() {
        System.out.println("===============GET");
        return "customer/create";  
    }
    @RequestMapping(value="/create", method = RequestMethod.POST)//③類級別的@RequestMapping窄化
    public String submit() {
        System.out.println("================POST");
        return "redirect:/success";        
    }
}

 ①處理器的通用映射前綴(父路徑):表示該處理器只處理匹配“/customers/**”的請求;

②對類級別的@RequestMapping進行窄化,表示showForm可處理匹配“/customers/**/create”且請求方法爲“GET”的請求;

③對類級別的@RequestMapping進行窄化,表示submit可處理匹配“/customers/**/create”且請求方法爲“POST”的請求。

6.5.2.2、組合使用是“或”的關係

@RequestMapping(value="/methodOr", method = {RequestMethod.POST, RequestMethod.GET}):即請求方法可以是 GET 或 POST。

 

提示:

1、一般瀏覽器只支持GET、POST請求方法,如想瀏覽器支持PUT、DELETE等請求方法只能模擬,稍候章節介紹。

2、除了GET、POST,還有HEAD、OPTIONS、PUT、DELETE、TRACE。

3、DispatcherServlet默認開啓對 GET、POST、PUT、DELETE、HEAD的支持;

4、如果需要支持OPTIONS、TRACE,請添加DispatcherServlet在web.xml的初始化參數:dispatchOptionsRequest 和 dispatchTraceRequest 爲true。

 

請求方法的詳細使用請參考RESTful架構風格一章。

 

以上請求方法映射限定測試類爲:cn.javass.chapter6.web.controller.method.RequestMethodController。

 

 

6.5.3、請求參數數據映射限定

6.5.3.1、請求數據中有指定參數名

package cn.javass.chapter6.web.controller.parameter;
//省略import
@Controller
@RequestMapping("/parameter1")                                      //①處理器的通用映射前綴
public class RequestParameterController1 {
    //②進行類級別的@RequestMapping窄化
    @RequestMapping(params="create", method=RequestMethod.GET) 
    public String showForm() {
        System.out.println("===============showForm");
        return "parameter/create";        
    }
    //③進行類級別的@RequestMapping窄化
    @RequestMapping(params="create", method=RequestMethod.POST)  
    public String submit() {
        System.out.println("================submit");
        return "redirect:/success";        
    }
}

 

 

 ②@RequestMapping(params="create", method=RequestMethod.GET) :表示請求中有“create”的參數名且請求方法爲“GET”即可匹配,如可匹配的請求URL“http://×××/parameter1?create”;

③@RequestMapping(params="create", method=RequestMethod.POST):表示請求中有“create”的參數名且請求方法爲“POST”即可匹配;

 

此處的create請求參數名錶示你請求的動作,即你想要的功能的一個標識,常見的CRUD(增刪改查)我們可以使用如下請求參數名來表達:

(create請求參數名 且 GET請求方法) 新增頁面展示、(create請求參數名 且 POST請求方法)新增提交;

(update請求參數名 且 GET請求方法) 新增頁面展示、(update請求參數名 且 POST請求方法)新增提交;

(delete請求參數名 且 GET請求方法) 新增頁面展示、(delete請求參數名 且 POST請求方法)新增提交;

(query請求參數名 且 GET請求方法) 新增頁面展示、(query請求參數名 且 POST請求方法) 新增提交;

(list請求參數名 且 GET請求方法) 列表頁面展示;

(view請求參數名 且 GET請求方法) 查看單條記錄頁面展示。

 

6.5.3.2、請求數據中沒有指定參數名

//請求參數不包含 create參數名
@RequestMapping(params="!create", method=RequestMethod.GET)//進行類級別的@RequestMapping窄化

 @RequestMapping(params="!create", method=RequestMethod.GET):表示請求中沒有“create”參數名且請求方法爲“GET”即可匹配,如可匹配的請求URL“http://×××/parameter1?abc”。

6.5.3.3、請求數據中指定參數名=值

package cn.javass.chapter6.web.controller.parameter;
//省略import
@Controller
@RequestMapping("/parameter2")                      //①處理器的通用映射前綴
public class RequestParameterController2 {
    //②進行類級別的@RequestMapping窄化
    @RequestMapping(params="submitFlag=create", method=RequestMethod.GET)  
    public String showForm() {
        System.out.println("===============showForm");
        return "parameter/create";        
    }
    //③進行類級別的@RequestMapping窄化
    @RequestMapping(params="submitFlag=create", method=RequestMethod.POST)   
    public String submit() {
        System.out.println("===============submit");
        return "redirect:/success";        
    }
}

 ②@RequestMapping(params="submitFlag=create", method=RequestMethod.GET):表示請求中有“submitFlag=create”請求參數且請求方法爲“GET”即可匹配,如請求URL爲http://×××/parameter2?submitFlag=create

 

③@RequestMapping(params="submitFlag=create", method=RequestMethod.POST):表示請求中有“submitFlag=create”請求參數且請求方法爲“POST”即可匹配;

 

此處的submitFlag=create請求參數表示你請求的動作,即你想要的功能的一個標識,常見的CRUD(增刪改查)我們可以使用如下請求參數名來表達:

(submitFlag=create請求參數名 且 GET請求方法) 新增頁面展示、(submitFlag=create請求參數名 且 POST請求方法) 新增提交;

(submitFlag=update請求參數名 且 GET請求方法) 新增頁面展示、(submitFlag=update請求參數名 且 POST請求方法) 新增提交;

(submitFlag=delete請求參數名 且 GET請求方法) 新增頁面展示、(submitFlag=delete請求參數名 且 POST請求方法) 新增提交;

(submitFlag=query請求參數名 且 GET請求方法) 新增頁面展示、(submitFlag=query請求參數名 且 POST請求方法) 新增提交;

(submitFlag=list請求參數名 且 GET請求方法) 列表頁面展示;

(submitFlag=view請求參數名 且 GET請求方法) 查看單條記錄頁面展示。

6.5.3.4、請求數據中指定參數名!=值

//請求參數submitFlag 不等於 create
@RequestMapping(params="submitFlag!=create", method=RequestMethod.GET)  

 @RequestMapping(params="submitFlag!=create", method=RequestMethod.GET):表示請求中的參數“submitFlag!=create”且請求方法爲“GET”即可匹配,如可匹配的請求URL“http://×××/parameter1?submitFlag=abc”。

 

6.5.3.5、組合使用是“且”的關係

@RequestMapping(params={"test1", "test2=create"})  //②進行類級別的@RequestMapping窄化

 @RequestMapping(params={"test1", "test2=create"}):表示請求中的有“test1”參數名 且 有“test2=create”參數即可匹配,如可匹配的請求URL“http://×××/parameter3?test1&test2=create。

 

以上請求參數數據映射限定測試類爲:cn.javass.chapter6.web.controller.method包下的RequestParameterController1、RequestParameterController2、RequestParameterController3。

 

 

6.5.4、請求頭數據映射限定

6.5.4.1、準備環境

瀏覽器:建議chrome最新版本;

插件:ModHeader

安裝地址:https://chrome.google.com/webstore/detail/idgpnmonknjnojddfkpgkljpfnnfcklj

 

插件安裝步驟:

1、打開https://chrome.google.com/webstore/detail/idgpnmonknjnojddfkpgkljpfnnfcklj,如圖6-2

 

圖6-2

2、點擊“添加至chrome”後彈出“確認安裝”對話框,點擊“安裝”按鈕即可,如圖6-3:

 

圖6-3

3、安裝成功後,在瀏覽器右上角出現如圖6-4的圖標表示安裝成功:

 

圖6-4

4、鼠標右擊右上角的“Modify Header”圖標,選擇選項,打開如圖6-5:

 

圖6-5

7、修改完成後,輸入URL請求,你可以在chrome的“開發人員工具的”網絡選項卡下,看到如圖6-7的信息表示添加請求頭成功了:

 

圖6-7

到此我們的工具安裝完畢,接下來看看如何使用請求頭數據進行映射限定。

6.5.4.2、請求頭數據中有指定參數名

@RequestMapping(value="/header/test1", headers = "Accept"):表示請求的URL必須爲“/header/test1”

且 請求頭中必須有Accept參數才能匹配。

 

@RequestMapping(value="/header/test1", headers = "abc"):表示請求的URL必須爲“/header/test1”

且 請求頭中必須有abc參數才能匹配,如圖6-8時可匹配。

 

圖6-8

6.5.4.3、請求頭數據中沒有指定參數名

@RequestMapping(value="/header/test2", headers = "!abc"):表示請求的URL必須爲“/header/test2”

且 請求頭中必須沒有abc參數才能匹配。(將Modify Header的abc參數值刪除即可)。

 

6.5.4.4、請求頭數據中指定參數名=值

@RequestMapping(value="/header/test3", headers = "Content-Type=application/json"):表示請求的URL必須爲“/header/test3” 且 請求頭中必須有“Content-Type=application/json”參數即可匹配。(將Modify Header的Content-Type參數值改爲“application/json”即可);

 

當你請求的URL爲“/header/test3” 但 如果請求頭中沒有或不是“Content-Type=application/json”參數(如“text/html”其他參數),將返回“HTTP Status 415”狀態碼【表示不支持的媒體類型(Media Type),也就是MIME類型】,即我們的功能處理方法只能處理application/json的媒體類型。

 

@RequestMapping(value="/header/test4", headers = "Accept=application/json"):表示請求的URL必須爲“/header/test4” 且 請求頭中必須有“Accept =application/json”參數即可匹配。(將Modify Header的Accept參數值改爲“application/json”即可);

 

當你請求的URL爲“/header/test4” 但 如果請求頭中沒有“Accept=application/json”參數(如“text/html”其他參數),將返回“HTTP Status 406”狀態碼【不可接受,服務器無法根據Accept頭的媒體類型爲客戶端生成響應】,即客戶只接受“application/json”媒體類型的數據,即我們的功能處理方法的響應只能返回“application/json”媒體類型的數據。

 

@RequestMapping(value="/header/test5", headers = "Accept=text/*") :表示請求的URL必須爲“/header/test5” 且 請求頭中必須有如“Accept=text/plain”參數即可匹配。(將Modify Header的Accept參數值改爲“text/plain”即可);

 

Accept=text/*:表示主類型爲text,子類型任意,如“text/plain”、“text/html”等都可以匹配。

 

@RequestMapping(value="/header/test6", headers = "Accept=*/*") :表示請求的URL必須爲“/header/test6” 且 請求頭中必須有任意Accept參數即可匹配。(將Modify Header的Accept參數值改爲“text/html”或“application/xml”等都可以)。

 

Accept=*/*:表示主類型任意,子類型任意,如“text/plain”、“application/xml”等都可以匹配。

6.5.4.5、請求頭數據中指定參數名!=值

@RequestMapping(value="/header/test7", headers = "Accept!=text/vnd.wap.wml"):表示請求的URL必須爲“/header/test7” 且 請求頭中必須有“Accept”參數但值不等於“text/vnd.wap.wml”即可匹配。

6.5.4.6、組合使用是“且”的關係

@RequestMapping(value="/header/test8", headers = {"Accept!=text/vnd.wap.wml","abc=123"}):表示請求的URL必須爲“/header/test8” 且 請求頭中必須有“Accept”參數但值不等於“text/vnd.wap.wml”且 請求中必須有參數“abc=123”即可匹配。

 

注:Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

如果您的請求中含有Accept:“*/*”,則可以匹配功能處理方法上的如“text/html”、“text/*”,“application/xml”等。

 

6.6.5、生產者、消費者限定

6.6.5.1、基本概念

首先讓我們看一下通過HTTP協議傳輸的媒體類型及如何表示媒體類型:

 

一、Media Type:

互聯網媒體類型,一般就是我們所說的MIME類型,用來確定請求的內容類型或響應的內容類型。

 寫道
媒體類型格式:type/subtype(;parameter)? 
type主類型,任意的字符串,如text,如果是*號代表所有; 
subtype 子類型,任意的字符串,如html,如果是*號代表所有; 
parameter 可選,一些參數,如Accept請求頭的q參數, Content-Type的 charset參數。 

詳見http://tools.ietf.org/html/rfc2616#section-3.7

 常見媒體類型:

 

text/html : HTML格式          text/plain :純文本格式             text/xml :XML格式

image/gif :gif圖片格式          image/jpeg :jpg圖片格式          image/png:png圖片格式

 

application/x-www-form-urlencoded : <form encType=””>中默認的encType,form表單數據被編碼爲key/value格式發送到服務器(表單默認的提交數據的格式)。

multipart/form-data : 當你需要在表單中進行文件上傳時,就需要使用該格式;

 

application/xhtml+xml :XHTML格式               application/xml     : XML數據格式 

application/atom+xml  :Atom XML聚合格式    application/json    : JSON數據格式

application/pdf       :pdf格式                        application/msword  : Word文檔格式

application/octet-stream : 二進制流數據(如常見的文件下載)。

 

在如tomcat服務器的 “conf/web.xml”中指定了擴展名到媒體類型的映射,在此我們可以看到服務器支持的媒體類型。

 

 

二、Content-Type:內容類型,即請求/響應的內容區數據的媒體類型;

2.1、請求頭的內容類型,表示發送到服務器的內容數據的媒體類型;

request中設置請求頭“Content-Type: application/x-www-form-urlencoded”表示請求的數據爲key/value數據;

(1、控制器cn.javass.chapter6.web.controller.consumesproduces.contenttype.RequestContentTypeController

 

    @RequestMapping(value = "/ContentType", method = RequestMethod.GET)
    public String showForm() throws IOException {
        //form表單,使用application/x-www-form-urlencoded編碼方式提交表單
        return "consumesproduces/Content-Type";
    }
    
    @RequestMapping(value = "/ContentType", method = RequestMethod.POST, 
headers = "Content-Type=application/x-www-form-urlencoded")
    public String request1(HttpServletRequest request) throws IOException {
        //①得到請求的內容區數據的類型
        String contentType = request.getContentType(); 
        System.out.println("========ContentType:" + contentType);
        //②得到請求的內容區數據的編碼方式,如果請求中沒有指定則爲null
        //注意,我們的CharacterEncodingFilter這個過濾器設置了編碼(UTF-8)
        //編碼只能被指定一次,即如果客戶端設置了編碼,則過濾器不會再設置
        String characterEncoding = request.getCharacterEncoding();
        System.out.println("========CharacterEncoding:" + characterEncoding);
        
        //③表示請求的內容區數據爲form表單提交的參數,此時我們可以通過request.getParameter得到數據(key=value)
        System.out.println(request.getParameter("realname"));
        System.out.println(request.getParameter("username"));
        return "success";
    }

 showForm功能處理方式:展示表單,且form的enctype="application/x-www-form-urlencoded",在提交時請求的內容類型頭爲“Content-Type:application/x-www-form-urlencoded”;

 

 

request1功能處理方法:只對請求頭爲“Content-Type:application/x-www-form-urlencoded”的請求進行處理(即消費請求內容區數據);

      request.getContentType():可以得到請求頭的內容區數據類型(即Content-Type頭的值)

      request.getCharacterEncoding():如“Content-Type:application/json;charset=GBK”,則得到的編碼爲“GBK”,否則如果你設置過濾器(CharacterEncodingFilter)則得到它設置的編碼,否則返回null。

      request.getParameter():因爲請求的內容區數據爲application/x-www-form-urlencoded格式的數據,因此我們可以通過request.getParameter()得到相應參數數據。

 

request中設置請求頭“Content-Type:application/json;charset=GBK”表示請求的內容區數據爲json類型數據,且內容區的數據以GBK進行編碼;

 

(1、控制器cn.javass.chapter6.web.controller.consumesproduces.contenttype.RequestContentTypeController

@RequestMapping(value = "/request/ContentType", method = RequestMethod.POST, 
headers = "Content-Type=application/json")
    public String request2(HttpServletRequest request) throws IOException {        
        //①表示請求的內容區數據爲json數據
        InputStream is = request.getInputStream();
        byte bytes[] = new byte[request.getContentLength()];
        is.read(bytes);
        //②得到請求中的內容區數據(以CharacterEncoding解碼)
        //此處得到數據後你可以通過如json-lib轉換爲其他對象
        String jsonStr = new String(bytes, request.getCharacterEncoding());
        System.out.println("json data:" + jsonStr);
        return "success";
    } 

  request2功能處理方法:只對請求頭爲“Content-Type:application/json”的進行請求處理(即消費請求內容區數據);

      request.getContentLength():可以得到請求頭的內容區數據的長度;

      request.getCharacterEncoding():如“Content-Type:application/json;charset=GBK”,則得到的編碼爲“GBK”,否則如果你設置過濾器(CharacterEncodingFilter)則得到它設置的編碼,否則返回null。

     

      我們得到json的字符串形式後就能很簡單的轉換爲JSON相關的對象。

 

(2、客戶端發送json數據請求

 

        //請求的地址
        String url = "http://localhost:9080/springmvc-chapter6/request/ContentType";
        //①創建Http Request(內部使用HttpURLConnection)
        ClientHttpRequest request = 
            new SimpleClientHttpRequestFactory().   
                createRequest(new URI(url), HttpMethod.POST);
        //②設置請求頭的內容類型頭和內容編碼(GBK)
        request.getHeaders().set("Content-Type", "application/json;charset=gbk");
        //③以GBK編碼寫出請求內容體
        String jsonData = "{\"username\":\"zhang\", \"password\":\"123\"}";
        request.getBody().write(jsonData.getBytes("gbk"));
        //④發送請求並得到響應
        ClientHttpResponse response = request.execute();
        System.out.println(response.getStatusCode());

 此處我們使用Spring提供的Http客戶端API SimpleClientHttpRequestFactory創建了請求並設置了請求的Content-Type和編碼並在響應體中寫回了json數據(即生產json類型的數據),此處是硬編碼,實際工作可以使用json-lib等工具進行轉換。

 

具體代碼在cn.javass.chapter6.web.controller.consumesproduces.contenttype.RequestContentTypeClient。

 

2.2、響應頭的內容類型,表示發送到客戶端的內容數據類型,和請求頭的內容類型類似,只是方向相反。

    @RequestMapping("/response/ContentType")
    public void response1(HttpServletResponse response) throws IOException {
        //①表示響應的內容區數據的媒體類型爲html格式,且編碼爲utf-8(客戶端應該以utf-8解碼)
        response.setContentType("text/html;charset=utf-8");
        //②寫出響應體內容
        response.getWriter().write("<font style='color:red'>hello</font>");
    }

  <!--[endif]-->

如上所示,通過response.setContentType("text/html;charset=utf-8") 告訴客戶端響應體媒體類型爲html,編碼爲utf-8,大家可以通過chrome工具查看響應頭爲“Content-Type:text/html;charset=utf-8”,還一個“Content-Length:36”表示響應體大小。

 

代碼在cn.javass.chapter6.web.controller.consumesproduces.contenttype.ResponseContentTypeController。

 

 

如上代碼可以看出Content-Type可以指定請求/響應的內容體的媒體格式和可選的編碼方式。如圖6-9 

 

 

①客戶端—發送請求—服務器:客戶端通過請求頭Content-Type指定內容體的媒體類型(即客戶端此時是生產者),服務器根據Content-Type消費內容體數據(即服務器此時是消費者);

②服務器—發送請求—客戶端:服務器生產響應頭Content-Type指定的響應體數據(即服務器此時是生產者),客戶端根據Content-Type消費內容體數據(即客戶端此時是消費者)。

 

問題:

①服務器端可以通過指定【headers = "Content-Type=application/json"】來聲明可處理(可消費)的媒體類型,即只消費Content-Type指定的請求內容體數據;

②客戶端如何告訴服務器端它只消費什麼媒體類型的數據呢?即客戶端接受(需要)什麼類型的數據呢?服務器應該生產什麼類型的數據?此時我們可以請求的Accept請求頭來實現這個功能。

 

三、Accept:用來指定什麼媒體類型的響應是可接受的,即告訴服務器我需要什麼媒體類型的數據,此時服務器應該根據Accept請求頭生產指定媒體類型的數據。

 2.1、json數據

(1、服務器端控制器

 

    @RequestMapping(value = "/response/ContentType", headers = "Accept=application/json")
    public void response2(HttpServletResponse response) throws IOException {
        //①表示響應的內容區數據的媒體類型爲json格式,且編碼爲utf-8(客戶端應該以utf-8解碼)
        response.setContentType("application/json;charset=utf-8");
        //②寫出響應體內容
        String jsonData = "{\"username\":\"zhang\", \"password\":\"123\"}";
        response.getWriter().write(jsonData);
    }

  服務器根據請求頭“Accept=application/json”生產json數據。

 

(2、客戶端端接收服務器端json數據響應

 

使用瀏覽器測試(Ajax場景使用該方式)

請求地址爲:http://localhost:9080/springmvc-chapter6/response/ContentType,且把修改請求頭Accept改爲“Accept=application/json”:

 

大家可以下載chrome的JSONView插件來以更好看的方式查看json數據,安裝地址:https://chrome.google.com/webstore/detail/chklaanhfefbnpoihckbnefhakgolnmc

 

 

使用普通客戶端測試(服務器之間通信可使用該方式)

 

    private static void jsonRequest() throws IOException, URISyntaxException {
        //請求的地址
        String url = "http://localhost:9080/springmvc-chapter6/response/ContentType";
        //①創建Http Request(內部使用HttpURLConnection)
        ClientHttpRequest request = 
            new SimpleClientHttpRequestFactory().   
                createRequest(new URI(url), HttpMethod.POST);
        //②設置客戶端可接受的媒體類型(即需要什麼類型的響應體數據)
        request.getHeaders().set("Accept", "application/json");        
        //③發送請求並得到響應
        ClientHttpResponse response = request.execute();
        //④得到響應體的編碼方式
        Charset charset = response.getHeaders().getContentType().getCharSet();        
        //⑤得到響應體的內容        
        InputStream is = response.getBody();
        byte bytes[] = new byte[(int)response.getHeaders().getContentLength()];
        is.read(bytes);
        String jsonData = new String(bytes, charset);
        System.out.println("charset : " + charset + ", json data : " + jsonData);
    }

 request.getHeaders().set("Accept", "application/json"):表示客戶端只接受(即只消費)json格式的響應數據;

response.getHeaders():可以得到響應頭,從而可以得到響應體的內容類型和編碼、內容長度。

2.2、xml數據

(1、服務器端控制器

 

    @RequestMapping(value = "/response/ContentType", headers = "Accept=application/xml")
    public void response3(HttpServletResponse response) throws IOException {
        //①表示響應的內容區數據的媒體類型爲xml格式,且編碼爲utf-8(客戶端應該以utf-8解碼)
        response.setContentType("application/xml;charset=utf-8");
        //②寫出響應體內容
        String xmlData = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
        xmlData += "<user><username>zhang</username><password>123</password></user>";
        response.getWriter().write(xmlData);
    }

  和生產json數據唯一不同的兩點:請求頭爲“Accept=application/xml”,響應體數據爲xml。

 

(2、客戶端端接收服務器端xml數據響應

 

使用瀏覽器測試(Ajax場景使用該方式)

請求地址爲:http://localhost:9080/springmvc-chapter6/response/ContentType,且把修改請求頭Accept改爲“Accept=application/xml”,和json方式類似,此處不再重複。

 

使用普通客戶端測試(服務器之間通信可使用該方式)

 

    private static void xmlRequest() throws IOException, URISyntaxException {
        //請求的地址
        String url = "http://localhost:9080/springmvc-chapter6/response/ContentType";
        //①創建Http Request(內部使用HttpURLConnection)
        ClientHttpRequest request = 
            new SimpleClientHttpRequestFactory().   
                createRequest(new URI(url), HttpMethod.POST);
        //②設置客戶端可接受的媒體類型(即需要什麼類型的響應體數據)
        request.getHeaders().set("Accept", "application/xml");
        //③發送請求並得到響應
        ClientHttpResponse response = request.execute();        
        //④得到響應體的編碼方式
        Charset charset = response.getHeaders().getContentType().getCharSet();
        //⑤得到響應體的內容        
        InputStream is = response.getBody();
        byte bytes[] = new byte[(int)response.getHeaders().getContentLength()];
        is.read(bytes);
        String xmlData = new String(bytes, charset);
        System.out.println("charset : " + charset + ", xml data : " + xmlData);
    }

 request.getHeaders().set("Accept", "application/xml"):表示客戶端只接受(即只消費)xml格式的響應數據;

response.getHeaders():可以得到響應頭,從而可以得到響應體的內容類型和編碼、內容長度。

 

許多開放平臺,都提供了同一種數據的多種不同的表現形式,此時我們可以根據Accept請求頭告訴它們我們需要什麼類型的數據,他們根據我們的Accept來判斷需要返回什麼類型的數據。

 

實際項目使用Accept請求頭是比較麻煩的,現在大多數開放平臺(國內的新浪微博、淘寶、騰訊等開放平臺)使用如下兩種方式:

擴展名:如response/ContentType.json response/ContentType.xml方式,使用擴展名錶示需要什麼類型的數據;

參數:如response/ContentType?format=json response/ContentType?format=xml,使用參數表示需要什麼類型的數據;

 

也就是說,目前我們可以使用如上三種方式實現來告訴服務器我們需要什麼類型的數據,但麻煩的是現在有三種實現方式,難道我們爲了支持三種類型的數據就要分別進行三種實現嗎?當然不要這麼麻煩,後續我們會學ContentNegotiatingViewResolver,它能幫助我們做到這一點。

 

6.6.5.2、生產者消費者流程圖

生產者消費者流程,如圖6-10:

 

 

從圖6-10可以看出:

請求階段:客戶端是生產者【生產Content-Type媒體類型的請求內容區數據】,服務器是消費者【消費客戶端生產的Content-Type媒體類型的請求內容區數據】;

響應階段:服務器是生產者【生產客戶端請求頭參數Accept指定的響應體數據】,客戶端是消費者【消費服務器根據Accept請求頭生產的響應體數據】。

 

 

如上生產者/消費者寫法無法很好的體現我們分析的生產者/消費者模式,Spring3.1爲生產者/消費者模式提供了簡化支持,接下來我們學習一下如何在Spring3.1中來實現生產者/消費者模式吧。

 

 

6.6.5.3、生產者、消費者限定

Spring3.1開始支持消費者、生產者限定,而且必須使用如下HandlerMapping和HandlerAdapter才支持:

 

<!--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"/> 

 一、功能處理方法是消費者

@RequestMapping(value = "/consumes", consumes = {"application/json"}):此處使用consumes來指定功能處理方法能消費的媒體類型,其通過請求頭的“Content-Type”來判斷。

 

此種方式相對使用@RequestMapping的“headers = "Content-Type=application/json"”更能表明你的目的。

 

服務器控制器代碼詳解cn.javass.chapter6.web.controller.consumesproduces.ConsumesController;

客戶端代碼類似於之前的Content-Type中的客戶端,詳見ConsumesClient.java代碼。

 

 

二、功能處理方法是生產者

@RequestMapping(value = "/produces", produces = "application/json"):表示將功能處理方法將生產json格式的數據,此時根據請求頭中的Accept進行匹配,如請求頭“Accept:application/json”時即可匹配;

@RequestMapping(value = "/produces", produces = "application/xml"):表示將功能處理方法將生產xml格式的數據,此時根據請求頭中的Accept進行匹配,如請求頭“Accept:application/xml”時即可匹配。

   

此種方式相對使用@RequestMapping的“headers = "Accept=application/json"”更能表明你的目的。

 

服務器控制器代碼詳解cn.javass.chapter6.web.controller.consumesproduces.ProducesController;

客戶端代碼類似於之前的Content-Type中的客戶端,詳見ProducesController.java代碼。

 

當你有如下Accept頭:

①Accept:text/html,application/xml,application/json

      將按照如下順序進行produces的匹配 ①text/html ②application/xml ③application/json

②Accept:application/xml;q=0.5,application/json;q=0.9,text/html

      將按照如下順序進行produces的匹配 ①text/html ②application/json ③application/xml

      q參數爲媒體類型的質量因子,越大則優先權越高(從0到1)

③Accept:*/*,text/*,text/html

      將按照如下順序進行produces的匹配 ①text/html ②text/* ③*/*

 

即匹配規則爲:最明確的優先匹配。

 

代碼詳見ProducesPrecedenceController1、ProducesPrecedenceController2、ProducesPrecedenceController3。

Accept詳細信息,請參考http://tools.ietf.org/html/rfc2616#section-14.1

 

 

三、窄化時是覆蓋 而 非繼承

如類級別的映射爲 @RequestMapping(value="/narrow", produces="text/html"),方法級別的爲@RequestMapping(produces="application/xml"),此時方法級別的映射將覆蓋類級別的,因此請求頭“Accept:application/xml”是成功的,而“text/html”將報406錯誤碼,表示不支持的請求媒體類型。

 

詳見cn.javass.chapter6.web.controller.consumesproduces.NarrowController。

 

只有生產者/消費者 模式 是 覆蓋,其他的使用方法是繼承,如headers、params等都是繼承。

 

四、組合使用是“或”的關係

@RequestMapping(produces={"text/html", "application/json"}) :將匹配“Accept:text/html”或“Accept:application/json”。

 

五、問題

消費的數據,如JSON數據、XML數據都是由我們讀取請求的InputStream並根據需要自己轉換爲相應的模型數據,比較麻煩;

生產的數據,如JSON數據、XML數據都是由我們自己先把模型數據轉換爲json/xml等數據,然後輸出響應流,也是比較麻煩的。

 

Spring提供了一組註解(@RequestBody、@ResponseBody)和一組轉換類(HttpMessageConverter)來完成我們遇到的問題,詳見6.6.8節。


到目前爲止,請求已經能交給我們的處理器進行處理了,接下來的事情是要進行收集數據啦,接下來我們看看我們能從請求中收集到哪些數據,如圖6-11:


 圖6-11

1、@RequestParam綁定單個請求參數值;

2、@PathVariable綁定URI模板變量值;

3、@CookieValue綁定Cookie數據值

4、@RequestHeader綁定請求頭數據;

5、@ModelValue綁定參數到命令對象;

6、@SessionAttributes綁定命令對象到session;

7、@RequestBody綁定請求的內容區數據並能進行自動類型轉換等。

8、@RequestPart綁定“multipart/data”數據,除了能綁定@RequestParam能做到的請求參數外,還能綁定上傳的文件等。

 

除了上邊提到的註解,我們還可以通過如HttpServletRequest等API得到請求數據,但推薦使用註解方式,因爲使用起來更簡單。

 

接下來先看一下功能處理方法支持的參數類型吧。

6.6.1、功能處理方法支持的參數類型

在繼續學習之前,我們需要首先看看功能處理方法支持哪些類型的形式參數,以及他們的具體含義。

 一、ServletRequest/HttpServletRequest 和 ServletResponse/HttpServletResponse

 

public String requestOrResponse (
        ServletRequest servletRequest, HttpServletRequest httpServletRequest,
        ServletResponse servletResponse, HttpServletResponse httpServletResponse
    )

 Spring Web MVC框架會自動幫助我們把相應的Servlet請求/響應(Servlet API)作爲參數傳遞過來。

 

二、InputStream/OutputStream 和 Reader/Writer

 

public void inputOrOutBody(InputStream requestBodyIn, OutputStream responseBodyOut)
        throws IOException {
responseBodyOut.write("success".getBytes());
}

requestBodyIn:獲取請求的內容區字節流,等價於request.getInputStream();

responseBodyOut:獲取相應的內容區字節流,等價於response.getOutputStream()。

 

 

public void readerOrWriteBody(Reader reader, Writer writer)
        throws IOException {
    writer.write("hello");
}

 reader:獲取請求的內容區字符流,等價於request.getReader();

writer:獲取相應的內容區字符流,等價於response.getWriter()。

 

InputStream/OutputStream 和 Reader/Writer兩組不能同時使用,只能使用其中的一組。

 

三、WebRequest/NativeWebRequest

WebRequest是Spring Web MVC提供的統一請求訪問接口,不僅僅可以訪問請求相關數據(如參數區數據、請求頭數據,但訪問不到Cookie區數據),還可以訪問會話和上下文中的數據;NativeWebRequest繼承了WebRequest,並提供訪問本地Servlet API的方法。

 

public String webRequest(WebRequest webRequest, NativeWebRequest nativeWebRequest) {
    System.out.println(webRequest.getParameter("test"));//①得到請求參數test的值
    webRequest.setAttribute("name", "value", WebRequest.SCOPE_REQUEST);//②
    System.out.println(webRequest.getAttribute("name", WebRequest.SCOPE_REQUEST));
    HttpServletRequest request = 
        nativeWebRequest.getNativeRequest(HttpServletRequest.class);//③
    HttpServletResponse response = 
        nativeWebRequest.getNativeResponse(HttpServletResponse.class);
        return "success";
    }

 ① webRequest.getParameter:訪問請求參數區的數據,可以通過getHeader()訪問請求頭數據;

② webRequest.setAttribute/getAttribute:到指定的作用範圍內取/放屬性數據,Servlet定義的三個作用範圍分別使用如下常量代表:

            SCOPE_REQUEST :代表請求作用範圍;

           SCOPE_SESSION :代表會話作用範圍;

           SCOPE_GLOBAL_SESSION :代表全局會話作用範圍,即ServletContext上下文作用範圍。 

③ nativeWebRequest.getNativeRequest/nativeWebRequest.getNativeResponse:得到本地的Servlet API。

 

四、HttpSession

 

public String session(HttpSession session) {
    System.out.println(session);
    return "success";
}

 此處的session永遠不爲null。

 

注意:session訪問不是線程安全的,如果需要線程安全,需要設置AnnotationMethodHandlerAdapter或RequestMappingHandlerAdapter的synchronizeOnSession屬性爲true,即可線程安全的訪問session。

 

 

五、命令/表單對象

Spring Web MVC能夠自動將請求參數綁定到功能處理方法的命令/表單對象上。

 

 

@RequestMapping(value = "/commandObject", method = RequestMethod.GET)
public String toCreateUser(HttpServletRequest request, UserModel user) {
    return "customer/create";
}
@RequestMapping(value = "/commandObject", method = RequestMethod.POST)
public String createUser(HttpServletRequest request, UserModel user) {
    System.out.println(user);
    return "success";
}

 如果提交的表單(包含username和password文本域),將自動將請求參數綁定到命令對象user中去。

 

六、Model、Map、ModelMap

Spring Web MVC 提供Model、Map或ModelMap讓我們能去暴露渲染視圖需要的模型數據。

 

@RequestMapping(value = "/model")
public String createUser(Model model, Map model2, ModelMap model3) {
    model.addAttribute("a", "a");
    model2.put("b", "b");
    model3.put("c", "c");
    System.out.println(model == model2);
    System.out.println(model2 == model3);
    return "success";}

 雖然此處注入的是三個不同的類型(Model model, Map model2, ModelMap model3),但三者是同一個對象,如圖6-12所示:


圖6-11

AnnotationMethodHandlerAdapter和RequestMappingHandlerAdapter將使用BindingAwareModelMap作爲模型對象的實現,即此處我們的形參(Model model, Map model2, ModelMap model3)都是同一個BindingAwareModelMap實例。

 

此處還有一點需要我們注意:

 

@RequestMapping(value = "/mergeModel")
public ModelAndView mergeModel(Model model) {
    model.addAttribute("a", "a");//①添加模型數據
    ModelAndView mv = new ModelAndView("success");
    mv.addObject("a", "update");//②在視圖渲染之前更新③處同名模型數據
    model.addAttribute("a", "new");//③修改①處同名模型數據
    //視圖頁面的a將顯示爲"update" 而不是"new"
    return mv;
}

 從代碼中我們可以總結出功能處理方法的返回值中的模型數據(如ModelAndView)會 合併 功能處理方法形式參數中的模型數據(如Model),但如果兩者之間有同名的,返回值中的模型數據會覆蓋形式參數中的模型數據。

 

七、Errors/BindingResult

 

@RequestMapping(value = "/error1")
public String error1(UserModel user, BindingResult result)

 

 

@RequestMapping(value = "/error2")
public String error2(UserModel user, BindingResult result, Model model) {
    

 

 

@RequestMapping(value = "/error3")
public String error3(UserModel user, Errors errors) 

 

 

以上代碼都能獲取錯誤對象。

 

Spring3.1之前(使用AnnotationMethodHandlerAdapter)錯誤對象必須緊跟在命令對象/表單對象之後,如下定義是錯誤的:

 

@RequestMapping(value = "/error4")
public String error4(UserModel user, Model model, Errors errors)
    }

如上代碼從Spring3.1開始(使用RequestMappingHandlerAdapter)將能正常工作,但還是推薦“錯誤對象緊跟在命令對象/表單對象之後”,這樣是萬無一失的。

 

Errors及BindingResult的詳細使用請參考4.16.2數據驗證。

 

 

八、其他雜項

 

public String other(Locale locale, Principal principal)

 java.util.Locale:得到當前請求的本地化信息,默認等價於ServletRequest.getLocale(),如果配置LocaleResolver解析器則由它決定Locale,後續介紹;

java.security.Principal:該主體對象包含了驗證通過的用戶信息,等價於HttpServletRequest.getUserPrincipal()。

 

以上測試在cn.javass.chapter6.web.controller.paramtype.MethodParamTypeController中。

 

其他功能處理方法的形式參數類型(如HttpEntity、UriComponentsBuilder、SessionStatus、RedirectAttributes)將在後續章節詳細講解。

 

第二部分會介紹註解方式的數據綁定。



6.6.2、@RequestParam綁定單個請求參數值

@RequestParam用於將請求參數區數據映射到功能處理方法的參數上。

public String requestparam1(@RequestParam String username)

請求中包含username參數(如/requestparam1?username=zhang),則自動傳入。

 

此處要特別注意:右擊項目,選擇“屬性”,打開“屬性對話框”,選擇“Java Compiler”然後再打開的選項卡將“Add variable attributes to generated class files”取消勾選,意思是不將局部變量信息添加到類文件中,如圖6-12所示:


 圖6-12

當你在瀏覽器輸入URL,如“requestparam1?username=123”時會報如下錯誤

Name for argument type [java.lang.String] not available, and parameter name information not found in class file either,表示得不到功能處理方法的參數名,此時我們需要如下方法進行入參:

 

public String requestparam2(@RequestParam("username") String username)

 即通過@RequestParam("username")明確告訴Spring Web MVC使用username進行入參。

 

 

接下來我們看一下@RequestParam註解主要有哪些參數:

value:參數名字,即入參的請求參數名字,如username表示請求的參數區中的名字爲username的參數的值將傳入;

required:是否必須,默認是true,表示請求中一定要有相應的參數,否則將報404錯誤碼;

defaultValue:默認值,表示如果請求中沒有同名參數時的默認值,默認值可以是SpEL表達式,如“#{systemProperties['java.vm.version']}”。

 

public String requestparam4(@RequestParam(value="username",required=false) String username) 

 表示請求中可以沒有名字爲username的參數,如果沒有默認爲null,此處需要注意如下幾點:

 

     原子類型:必須有值,否則拋出異常,如果允許空值請使用包裝類代替。

     Boolean包裝類型類型:默認Boolean.FALSE,其他引用類型默認爲null。

 

public String requestparam5(
@RequestParam(value="username", required=true, defaultValue="zhang") String username) 
        

表示如果請求中沒有名字爲username的參數,默認值爲“zhang”。

 

 

如果請求中有多個同名的應該如何接收呢?如給用戶授權時,可能授予多個權限,首先看下如下代碼:

public String requestparam7(@RequestParam(value="role") String roleList)

如果請求參數類似於url?role=admin&rule=user,則實際roleList參數入參的數據爲“admin,user”,即多個數據之間使用“,”分割;我們應該使用如下方式來接收多個請求參數:

public String requestparam7(@RequestParam(value="role") String[] roleList)   

 

public String requestparam8(@RequestParam(value="list") List<String> list)    

 到此@RequestParam我們就介紹完了,以上測試代碼在cn.javass.chapter6.web.controller. paramtype.RequestParamTypeController中。

 

6.6.3、@PathVariable綁定URI模板變量值

@PathVariable用於將請求URL中的模板變量映射到功能處理方法的參數上。

 

@RequestMapping(value="/users/{userId}/topics/{topicId}")
public String test(
       @PathVariable(value="userId") int userId, 
       @PathVariable(value="topicId") int topicId)      

 如請求的URL爲“控制器URL/users/123/topics/456”,則自動將URL中模板變量{userId}和{topicId}綁定到通過@PathVariable註解的同名參數上,即入參後userId=123、topicId=456。代碼在PathVariableTypeController中。

 

6.6.4、@CookieValue綁定Cookie數據值

@CookieValue用於將請求的Cookie數據映射到功能處理方法的參數上。

 

public String test(@CookieValue(value="JSESSIONID", defaultValue="") String sessionId) 

 

如上配置將自動將JSESSIONID值入參到sessionId參數上,defaultValue表示Cookie中沒有JSESSIONID時默認爲空。

public String test2(@CookieValue(value="JSESSIONID", defaultValue="") Cookie sessionId)       

傳入參數類型也可以是javax.servlet.http.Cookie類型。

 

測試代碼在CookieValueTypeController中。@CookieValue也擁有和@RequestParam相同的三個參數,含義一樣。

6.6.5、@RequestHeader綁定請求頭數據

@RequestHeader用於將請求的頭信息區數據映射到功能處理方法的參數上。

@RequestMapping(value="/header")
public String test(
       @RequestHeader("User-Agent") String userAgent,
       @RequestHeader(value="Accept") String[] accepts)
        

如上配置將自動將請求頭“User-Agent”值入參到userAgent參數上,並將“Accept”請求頭值入參到accepts參數上。測試代碼在HeaderValueTypeController中。

 

@RequestHeader也擁有和@RequestParam相同的三個參數,含義一樣。

6.6.6、@ModelAttribute綁定請求參數到命令對象

@ModelAttribute一個具有如下三個作用:

①綁定請求參數到命令對象:放在功能處理方法的入參上時,用於將多個請求參數綁定到一個命令對象,從而簡化綁定流程,而且自動暴露爲模型數據用於視圖頁面展示時使用;

②暴露表單引用對象爲模型數據:放在處理器的一般方法(非功能處理方法)上時,是爲表單準備要展示的表單引用對象,如註冊時需要選擇的所在城市等,而且在執行功能處理方法(@RequestMapping註解的方法)之前,自動添加到模型對象中,用於視圖頁面展示時使用;

③暴露@RequestMapping方法返回值爲模型數據:放在功能處理方法的返回值上時,是暴露功能處理方法的返回值爲模型數據,用於視圖頁面展示時使用。

 

一、綁定請求參數到命令對象

如用戶登錄,我們需要捕獲用戶登錄的請求參數(用戶名、密碼)並封裝爲用戶對象,此時我們可以使用@ModelAttribute綁定多個請求參數到我們的命令對象。

 

public String test1(@ModelAttribute("user") UserModel user)

和6.6.1一節中的五、命令/表單對象功能一樣。只是此處多了一個註解@ModelAttribute("user"),它的作用是將該綁定的命令對象以“user”爲名稱添加到模型對象中供視圖頁面展示使用。我們此時可以在視圖頁面使用${user.username}來獲取綁定的命令對象的屬性。

 

綁定請求參數到命令對象支持對象圖導航式的綁定,如請求參數包含“?username=zhang&password=123&workInfo.city=bj”自動綁定到user中的workInfo屬性的city屬性中。

@RequestMapping(value="/model2/{username}")
public String test2(@ModelAttribute("model") DataBinderTestModel model) { 

DataBinderTestModel相關模型請從第三章拷貝過來,請求參數到命令對象的綁定規則詳見【4.16.1、數據綁定】一節,URI模板變量也能自動綁定到命令對象中,當你請求的URL中包含“bool=yes&schooInfo.specialty=computer&hobbyList[0]=program&hobbyList[1]=music&map[key1]=value1&map[key2]=value2&state=blocked”會自動綁定到命令對象上。

 

當URI模板變量和請求參數同名時,URI模板變量具有高優先權。

 

二、暴露表單引用對象爲模型數據

@ModelAttribute("cityList")
public List<String> cityList() {
    return Arrays.asList("北京", "山東");
} 

如上代碼會在執行功能處理方法之前執行,並將其自動添加到模型對象中,在功能處理方法中調用Model 入參的containsAttribute("cityList")將會返回true。

@ModelAttribute("user")  //①
public UserModel getUser(@RequestParam(value="username", defaultValue="") String username) {
//TODO 去數據庫根據用戶名查找用戶對象
UserModel user = new UserModel();
user.setRealname("zhang");
     return user;
} 

如你要修改用戶資料時一般需要根據用戶的編號/用戶名查找用戶來進行編輯,此時可以通過如上代碼查找要編輯的用戶。

也可以進行一些默認值的處理。

@RequestMapping(value="/model1") //②
public String test1(@ModelAttribute("user") UserModel user, Model model) 

此處我們看到①和②有同名的命令對象,那Spring Web MVC內部如何處理的呢:

(1、首先執行@ModelAttribute註解的方法,準備視圖展示時所需要的模型數據;@ModelAttribute註解方法形式參數規則和@RequestMapping規則一樣,如可以有@RequestParam等;

(2、執行@RequestMapping註解方法,進行模型綁定時首先查找模型數據中是否含有同名對象,如果有直接使用,如果沒有通過反射創建一個,因此②處的user將使用①處返回的命令對象。即②處的user等於①處的user。

 

三、暴露@RequestMapping方法返回值爲模型數據

public @ModelAttribute("user2") UserModel test3(@ModelAttribute("user2") UserModel user)

大家可以看到返回值類型是命令對象類型,而且通過@ModelAttribute("user2")註解,此時會暴露返回值到模型數據(名字爲user2)中供視圖展示使用。那哪個視圖應該展示呢?此時Spring Web MVC會根據RequestToViewNameTranslator進行邏輯視圖名的翻譯,詳見【4.15.5、RequestToViewNameTranslator】一節。

 

此時又有問題了,@RequestMapping註解方法的入參user暴露到模型數據中的名字也是user2,其實我們能猜到:

(3、@ModelAttribute註解的返回值會覆蓋@RequestMapping註解方法中的@ModelAttribute註解的同名命令對象。

 

四、匿名綁定命令參數

public String test4(@ModelAttribute UserModel user, Model model)
或
public String test5(UserModel user, Model model) 

此時我們沒有爲命令對象提供暴露到模型數據中的名字,此時的名字是什麼呢?Spring Web MVC自動將簡單類名(首字母小寫)作爲名字暴露,如“cn.javass.chapter6.model.UserModel”暴露的名字爲“userModel”。

public @ModelAttribute List<String> test6()
或
public @ModelAttribute List<UserModel> test7() 

對於集合類型(Collection接口的實現者們,包括數組),生成的模型對象屬性名爲“簡單類名(首字母小寫)”+“List”,如List<String>生成的模型對象屬性名爲“stringList”,List<UserModel>生成的模型對象屬性名爲“userModelList”。

 

其他情況一律都是使用簡單類名(首字母小寫)作爲模型對象屬性名,如Map<String, UserModel>類型的模型對象屬性名爲“map”。

6.6.7、@SessionAttributes綁定命令對象到session

有時候我們需要在多次請求之間保持數據,一般情況需要我們明確的調用HttpSession的API來存取會話數據,如多步驟提交的表單。Spring Web MVC提供了@SessionAttributes進行請求間透明的存取會話數據。

//1、在控制器類頭上添加@SessionAttributes註解
@SessionAttributes(value = {"user"})    //①
public class SessionAttributeController 

//2、@ModelAttribute註解的方法進行表單引用對象的創建
@ModelAttribute("user")    //②
public UserModel initUser() 

//3、@RequestMapping註解方法的@ModelAttribute註解的參數進行命令對象的綁定
@RequestMapping("/session1")   //③
public String session1(@ModelAttribute("user") UserModel user)

//4、通過SessionStatus的setComplete()方法清除@SessionAttributes指定的會話數據
@RequestMapping("/session2")   //③
public String session(@ModelAttribute("user") UserModel user, SessionStatus status) {
    if(true) { //④
        status.setComplete();
    }
    return "success";
} 

@SessionAttributes(value = {"user"})含義:

@SessionAttributes(value = {"user"}) 標識將模型數據中的名字爲“user” 的對象存儲到會話中(默認HttpSession),此處value指定將模型數據中的哪些數據(名字進行匹配)存儲到會話中,此外還有一個types屬性表示模型數據中的哪些類型的對象存儲到會話範圍內,如果同時指定value和types屬性則那些名字和類型都匹配的對象才能存儲到會話範圍內。

 

包含@SessionAttributes的執行流程如下所示:

① 首先根據@SessionAttributes註解信息查找會話內的對象放入到模型數據中;

② 執行@ModelAttribute註解的方法:如果模型數據中包含同名的數據,則不執行@ModelAttribute註解方法進行準備表單引用數據,而是使用①步驟中的會話數據;如果模型數據中不包含同名的數據,執行@ModelAttribute註解的方法並將返回值添加到模型數據中;

③ 執行@RequestMapping方法,綁定@ModelAttribute註解的參數:查找模型數據中是否有@ModelAttribute註解的同名對象,如果有直接使用,否則通過反射創建一個;並將請求參數綁定到該命令對象;

此處需要注意:如果使用@SessionAttributes註解控制器類之後,③步驟一定是從模型對象中取得同名的命令對象,如果模型數據中不存在將拋出HttpSessionRequiredException Expected session attribute ‘user’(Spring3.1)

或HttpSessionRequiredException Session attribute ‘user’ required - not found in session(Spring3.0)異常。

④ 如果會話可以銷燬了,如多步驟提交表單的最後一步,此時可以調用SessionStatus對象的setComplete()標識當前會話的@SessionAttributes指定的數據可以清理了,此時當@RequestMapping功能處理方法執行完畢會進行清理會話數據。

 

我們通過Spring Web MVC的源代碼驗證一下吧,此處我們分析的是Spring3.1的RequestMappingHandlerAdapter,讀者可以自行驗證Spring3.0的AnnotationMethodHandlerAdapter,流程一樣:

(1、RequestMappingHandlerAdapter.invokeHandlerMethod

//1、RequestMappingHandlerAdapter首先調用ModelFactory的initModel方法準備模型數據:
modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
//2、調用@RequestMapping註解的功能處理方法
requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
//3、更新/合併模型數據
modelFactory.updateModel(webRequest, mavContainer); 

(2、ModelFactory.initModel

Map<String, ?> attributesInSession = this.sessionAttributesHandler.retrieveAttributes(request);
//1.1、將與@SessionAttributes註解相關的會話對象放入模型數據中
mavContainer.mergeAttributes(attributesInSession);
//1.2、調用@ModelAttribute方法添加表單引用對象
invokeModelAttributeMethods(request, mavContainer);
//1.3、驗證模型數據中是否包含@SessionAttributes註解相關的會話對象,不包含拋出異常
for (String name : findSessionAttributeArguments(handlerMethod)) {
	if (!mavContainer.containsAttribute(name)) {
        //1.4、此處防止在@ModelAttribute註解方法又添加了會話對象
        //如在@ModelAttribute註解方法調用session.setAttribute("user", new UserModel());
		Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
		if (value == null) {
			throw new HttpSessionRequiredException("Expected session attribute '" + name + "'");
		}
		mavContainer.addAttribute(name, value);
} 

(3、ModelFactory.invokeModelAttributeMethods

for (InvocableHandlerMethod attrMethod : this.attributeMethods) {
    String modelName = attrMethod.getMethodAnnotation(ModelAttribute.class).value(); 
    //1.2.1、如果模型數據中包含同名數據則不再添加
	if (mavContainer.containsAttribute(modelName)) {
	    continue;
	}
	//1.2.2、調用@ModelAttribute註解方法並將返回值添加到模型數據中,此處省略實現代碼
} 

(4、requestMappingMethod.invokeAndHandle 調用功能處理方法,此處省略

(5、ModelFactory.updateMode 更新模型數據

//3.1、如果會話被標識爲完成,此時從會話中清除@SessionAttributes註解相關的會話對象
if (mavContainer.getSessionStatus().isComplete()){ 
	this.sessionAttributesHandler.cleanupAttributes(request);
}
//3.2、如果會話沒有完成,將模型數據中的@SessionAttributes註解相關的對象添加到會話中
else {
	this.sessionAttributesHandler.storeAttributes(request, mavContainer.getModel());
}
//省略部分代碼 

到此@SessionAtrribute介紹完畢,測試代碼在cn.javass.chapter6.web.controller.paramtype.SessionAttributeController中。

 

另外cn.javass.chapter6.web.controller.paramtype.WizardFormController是一個類似於【4.11、AbstractWizardFormController】中介紹的多步驟表單實現,此處不再貼代碼,多步驟提交表單需要考慮會話超時問題,這種方式可能對用戶不太友好,我們可以採取隱藏表單(即當前步驟將其他步驟的表單隱藏)或表單數據存數據庫(每步驟更新下數據庫數據)等方案解決。

6.6.8、@Value綁定SpEL表示式

@Value用於將一個SpEL表達式結果映射到到功能處理方法的參數上。

public String test(@Value("#{systemProperties['java.vm.version']}") String jvmVersion)

到此數據綁定我們就介紹完了,對於沒有介紹的方法參數和註解(包括自定義註解)在後續章節進行介紹。接下來我們學習下數據類型轉換吧。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章