在前面我們搭建了基本的Spring Web MVC環(huán)境,并配置了一個控制器。下面我們來詳細學習一下控制器。控制器的主要作用就是處理特定URL發(fā)過來的HTTP請求,然后進行業(yè)務邏輯處理,將結(jié)果返回給某個特定的視圖。
處理請求
我們在前面定義了如下一個控制器。在Spring中定義控制器非常簡單,新建一個類然后應用@Controller注解即可,當然一般習慣上將控制器類也命名為XXController。每個控制器可以有若干方法,分別處理不同的請求。要指定處理請求的URL,使用@RequestMapping注解。控制器方法處理之后,返回一個字符串,指定要使用的視圖名稱,然后該名稱交給視圖解析器轉(zhuǎn)換成真正的視圖,然后返回給客戶端。@RequestMapping還可以注解到控制器類上,這樣一來每個方法處理的URL就是控制器和方法上URL的組合。
@Controller
public class MainController {
@RequestMapping("/hello")
public String hello(@RequestParam(defaultValue = "茍") String name, Model model) {
model.addAttribute("name", name);
return "hello";
}
@RequestMapping("/index")
public String index() {
return "index";
}
}
默認情況下@RequestMapping會處理所有請求,如果希望只處理GET或者POST等請求,可以使用@RequestMapping的method屬性。
@RequestMapping(value = "/index", method = {RequestMethod.GET})
public String index() {
return "index";
}
當然也可以直接使用Spring定義的幾個Mapping注解,包括了GET、POST、DELETE、PUT等。需要注意這幾個注解只能應用于方法上。
@GetMapping(value = "/index")
public String index() {
return "index";
}
路徑參數(shù)
細心的同學可能會發(fā)現(xiàn)有些網(wǎng)站的URL很特別,類似http://mysite.com/list/yitian
這樣的。Spring也支持這樣的路徑參數(shù)。這時候路徑模式中相應部分需要用花括號括起來,然后在方法中使用@PathVariable注解(注解中的名稱需要和花括號中的參數(shù)相同)。這樣對應的路徑參數(shù)就會由Spring自動賦給方法中的參數(shù),我們直接在方法中使用即可。
@RequestMapping("/hello/{name}")
public String hey(@PathVariable("name") String username, Model model) {
model.addAttribute("name", username);
return "hello";
}
如果方法參數(shù)和路徑中花括號部分相同,那么@PathVariable中的名稱可以省略。
@RequestMapping("/hello/{name}")
public String hey(@PathVariable String name, Model model) {
model.addAttribute("name", name);
return "hello";
}
另外,方法中可以有多個路徑參數(shù)。而且路徑參數(shù)并不一定只能是字符串,也可以是int
、long
、Date
這樣的簡單類型,Spring會自動進行轉(zhuǎn)換,如果轉(zhuǎn)換失敗,就會拋出TypeMismatchException
。
正則表達式匹配
有時候可能需要匹配一個比較復雜的路徑,這時候可以使用正則表達式,語法是{varName:regex}
。例如為了匹配"/spring-web/spring-web-3.0.5.jar"
,我們需要這樣一個方法。
@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String extension) {
// ...
}
另外路徑模式中還支持通配符,例如/myPath/*.do
。
最后一個問題就是這些路徑優(yōu)先級的問題。如果一個請求匹配了多個路徑模式,那么最具體的那個會被使用。規(guī)則如下:
- 路徑中路徑參數(shù)和通配符越少,路徑越具體。
- 路徑參數(shù)和通配符個數(shù)相同的話,路徑越長越具體。
- 個數(shù)和長度都相同的話,通配符個數(shù)越少路徑越具體。
- 默認匹配
/**
優(yōu)先級最低。 - 前綴模式例如
/public/**
比其他兩個通配符的模式優(yōu)先級更低。
矩陣變量Matrix Variables
RFC 3986定義了可以在路徑中添加鍵值對,這樣的鍵值對叫做矩陣變量。Spring默認沒有啟用矩陣變量。要啟用它,在dispatcher-servlet.xml
中添加或修改如下一行。
<mvc:annotation-driven enable-matrix-variables="true"/>
矩陣變量可以用在路徑的任何部分,需要和路徑之間使用分號;
分隔開,每個矩陣變量之間也是用分號分隔。如果一個矩陣變量有多個值,使用逗號,
分隔,例如"/matrix/42;colors=red,blue,yellow;year=2012"
。
對應的控制器方法如下。
// 處理請求 /matrix/42;colors=red,blue,yellow;year=2012
@RequestMapping("/matrix/{count}")
public String matrix(@PathVariable int count, @MatrixVariable String[] colors, @MatrixVariable int year, Model model) {
model.addAttribute("colors", colors);
model.addAttribute("year", year);
return "matrix";
}
還可以將所有矩陣變量映射成一個Map。
// 處理請求 /matrix2/42;colors=red,blue,yellow;year=2012
@RequestMapping("/matrix2/{count}")
public String matrix2(@PathVariable int count, @MatrixVariable MultiValueMap<String, String> map, Model model) {
model.addAttribute("map", map);
return "matrix";
}
對應的視圖文件是matrix.jsp
。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>矩陣變量</title>
<meta charset="utf-8"/>
</head>
<body>
<p>顏色是:
<c:forEach var="color" items="${colors}">
${color},
</c:forEach>
</p>
<p>年份是:${year}</p>
<p>矩陣變量:${map}</p>
</body>
</html>
其他詳細用法參見Spring參考文檔-matrix-variables。
媒體類型
通過使用@RequestMapping的consumes屬性,還可以指定某個處理方法只處理某個或某些媒體類型的請求。下面的請求方法只處理Content-Type
是application/json
的請求。
@RequestMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet, Model model) {
// implementation omitted
}
另外comsumes
部分還可以寫為非的形式,表示匹配不是某種類型的請求。例如comsumes="!text/html"
表示處理Content-Type
不是text/html
的請求。除了直接指定字符串,還可以指定org.springframework.http.MediaType
提供的一組常量。
另外@RequestMapping還有一個produces屬性,指定匹配Accept
是某種類型的請求,并且使用指定的類型來編碼返回的響應。下面是一個例子。
@GetMapping(path = "/pets/{petId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {
// implementation omitted
}
定義處理方法
前面我們通過@RequestMapping和另外幾個注解,來匹配特定的請求。下面來學習一下如何定義處理方法。
方法參數(shù)
處理方法的參數(shù)并不是任意的,Spring處理方法支持的參數(shù)列表很長,可以參考Spring文檔。這些參數(shù)可以分為幾類:一是Servlet相關(guān)的類,例如HttpServletRequest
、HttpServletResponse
、HttpSession
等;二是應用程序相關(guān)的,例如Timezone、Locale等;三是Spring提供的各類注解;四是輸入輸出流,用于直接操作HTTP請求和響應
返回類型
處理方法的返回類型也不是任意的。詳細的返回類型參見Spring官方文檔。常用的一些返回類型如下:
-
String
,表示要返回視圖的名稱。 -
View
,會由RequestToViewNameTranslator
翻譯實際視圖的名稱。 -
void
,表示方法會自己生成響應,不需要視圖支持。 -
Callable<?>
,表示異步請求的返回。
綁定請求參數(shù)
我們還記得直接使用Servlet API中g(shù)etParameter方法的恐懼吧,對于每個Servlet我們都要調(diào)用多次getParameter方法獲取參數(shù),而且獲取到的是字符串,我們需要手動轉(zhuǎn)換類型。
在Spring中就非常簡單了,我們可以將請求參數(shù)綁定到方法參數(shù)上,使用@RequestParam即可。該注解有三個屬性,name表示請求參數(shù)的名稱;defaultValue表示請求參數(shù)的默認值;required表示請求參數(shù)是否是必需的。如果請求參數(shù)的名稱和方法參數(shù)相同,那么name還可以省略。
// GET /hello?name=yitian
@RequestMapping("/hello")
public String hello(@RequestParam(defaultValue = "茍") String name, Model model) {
model.addAttribute("name", name);
return "hello";
}
向視圖傳遞數(shù)據(jù)
如果處理方法的擁有一個org.springframework.ui.Model
類型參數(shù),那么我們就可以調(diào)用該參數(shù)的addAttribute方法添加屬性,然后在視圖中就可以訪問這些屬性了。例子見上。
綁定請求體和響應體
綁定請求體使用@RequestBody注解。下面的例子將請求體直接返回給響應。這里的處理方法用到了Writer參數(shù)直接輸出HTTP響應,不需要視圖,因此這里返回空。為了運行這個例子,需要一個表單,發(fā)送到該控制器上,然后我們就可以看到表單對應的請求體了。
@PostMapping("/requestBody")
public void handle(@RequestBody String body, Writer writer) throws IOException {
writer.write(body);
}
綁定響應體類似,我們需要使用@ResponseBody注解到方法上,這會告訴Spring直接將該方法的返回結(jié)果作為響應返回給客戶端。
@GetMapping("/something")
@ResponseBody
public String helloWorld() {
return "Hello World";
}
在底層,Spring會使用HttpMessageConverter
來將請求信息轉(zhuǎn)換成我們需要的類型。Spring Web MVC為我們自動注冊了一些HttpMessageConverter
,詳細情況參見Spring 參考文檔 Section 22.16.1, “Enabling the MVC Java Config or the MVC XML Namespace”。
Rest控制器
@RestController會向所有@RequestMapping方法添加@ResponseBody注解。如果控制器需要實現(xiàn)REST API,那么這時候就很方便。
使用HttpEntity
HttpEntity和請求體、響應體這兩個類似,可以在一個地方同時處理請求和響應。下面是Spring官方的一個例子,獲取了請求HttpEntity,處理之后返回一個響應HttpEntity。Spring會使用HttpMessageConverter
做必要的轉(zhuǎn)換。
@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) throws UnsupportedEncodingException {
String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader"));
byte[] requestBody = requestEntity.getBody();
// do something with request header and body
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("MyResponseHeader", "MyValue");
return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}
使用ModelAttribute
@ModelAttribute注解用于向模型添加屬性??梢宰饔玫椒椒ǎ@時候該方法會在該控制器的所有處理方法前執(zhí)行。在方法中可以接受多個參數(shù)和一個模型參數(shù),然后將這些參數(shù)處理之后添加到模型中。這樣每次處理方法執(zhí)行前都會先執(zhí)行一次該方法。因此如果控制器中有多個處理方法要小心使用這個注解。
@ModelAttribute
public void addModel(@RequestParam String name, Model model) {
model.addAttribute("name",name);
// add more ...
}
@ModelAttribute還可以作用到方法參數(shù)上。這種情況更常見也更加有用。這時候Spring會先從model中尋找@ModelAttribute參數(shù),如果沒找到則實例化一個(因此這個類必須有無參構(gòu)造函數(shù)),然后添加到model中。然后將請求參數(shù)(下面例子中是name=易天&age=24&gender=男
)添加到模型中。這樣當我們查看視圖的時候,一個完整的實體類已經(jīng)準備就緒了。
// 請求 /modelAttribute?name=易天&age=24&gender=男
@RequestMapping("/modelAttribute")
public String modelAttribute(@ModelAttribute User user, Model model) {
model.addAttribute("user", user);
return "modelAttribute";
}
User類的內(nèi)容如下, 各種方法已省略。
public class User {
private String name;
private int age;
private String gender;
}
使用SessionAttribute
@SessionAttribute可以用于控制器上,這時候它會將上面介紹的ModelAttribute保存到Session中,方便多個方法間使用。
@Controller
@SessionAttributes("user")
public class MainController {
// 請求 /modelAttribute?name=易天&age=24&gender=男
@RequestMapping("/modelAttribute")
public String modelAttribute(@ModelAttribute User user, Model model) {
model.addAttribute("user", user);
return "modelAttribute";
}
}
@SessionAttributes還可以用到處理方法的參數(shù)上,這時候可以獲取到Session中相應名稱的屬性,需要注意這個屬性必須是已存在的。如果改屬性不存在Spring就會拋出異常,然后將我們導向400頁面。
@RequestMapping("/accessSession")
public String accessSession(@SessionAttribute User user, Model model) {
model.addAttribute("user", user);
return "accessSession";
}
如果需要還可以直接使用HttpSession,方法很簡單,將方法參數(shù)定義為HttpSession,然后Spring就會將session對象注入到方法中。我們可以直接進行操作。
@RequestMapping("/httpSession")
public String httpSession(HttpSession session, Model model) {
User user = new User("林妹妹", 24, "女");
session.setAttribute("user", user);
model.addAttribute("session", session);
return "accessSession";
}
使用@RequestAttribute
@RequestAttribute用于獲取RequestAttribute,這些請求屬性可能是由過濾器或攔截器產(chǎn)生的。
@RequestMapping("/")
public String handleInfo(@RequestAttribute String info) {
// ...
}
處理application/x-www-form-urlencoded數(shù)據(jù)
瀏覽器會使用GET或者POST方法發(fā)送數(shù)據(jù),非瀏覽器客戶端可以使用PUT方法發(fā)送數(shù)據(jù)。但是PUT發(fā)送過來的數(shù)據(jù),不能被Servlet系列方法ServletRequest.getParameter*()
獲取到。Spring提供了一個過濾器HttpPutFormContentFilter
,用于支持非瀏覽器的PUT信息發(fā)送。
HttpPutFormContentFilter
需要在web.xml
中配置。<servlet-name>
配置的是Spring的DispatcherServlet的名稱。
<filter>
<filter-name>httpPutFormFilter</filter-name>
<filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpPutFormFilter</filter-name>
<servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>
這個過濾器會攔截Content-Type是application/x-www-form-urlencoded
的PUT請求,讀取其請求體然后包裝到ServletRequest中,以便ServletRequest.getParameter*()
可以獲取到參數(shù)。
使用@CookieValue
@CookieValue可以獲取某個Cookie的值。如果該cookie不存在,就會拋出異常,可以使用required和defaultValue指定是否必須和默認值。
@RequestMapping("/cookie")
public void cookie(@CookieValue("JSESSIONID") String cookie) {
//...
}
@RequestHeader
@RequestHeader注解可以獲取RequestHeader的信息,可以使用required和defaultValue指定是否必須和默認值。
@RequestMapping("/header")
public String headerInfo(@RequestHeader("Accept-Encoding") String encoding, Model model) {
model.addAttribute("encoding", encoding);
return "info";
}
控制器通知
先來介紹一下@InitBinder注解,它可以放到控制器的一個方法上,這個方法有一個WebDataBinder參數(shù),用它可以對控制器進行定制,添加格式轉(zhuǎn)換、驗證等功能。
@InitBinder
protected void initBinder(WebDataBinder binder) {
//添加功能
}
然后就可以介紹@ControllerAdvice
和@RestControllerAdvice
這兩個注解了。它們可以定義控制器通知,這個AOP中的Advice概念是一樣的。這些注解需要應用到類上,這些類可以包括@ExceptionHandler
, @InitBinder
和 @ModelAttribute
注解的方法,然后這些方法就會在恰當?shù)臅r機來執(zhí)行。
// 注解了RestController的控制器
@ControllerAdvice(annotations = RestController.class)
public class AnnotationAdvice {}
// 特定包下的控制器
@ControllerAdvice("org.example.controllers")
public class BasePackageAdvice {}
// 特定類型的控制器
@ControllerAdvice(assignableTypes = {MainController.class})
public class AssignableTypesAdvice {}
攔截請求
我們可以使用攔截器攔截請求并進行處理,這一點有點像Servlet的過濾器。我們需要實現(xiàn)org.springframework.web.servlet.HandlerInterceptor
接口,不過更好的方法是繼承HandlerInterceptorAdapter
類,這個類將幾個攔截方法進行了默認實現(xiàn)。我們只需要重寫需要的方法即可。
下面定義了一個簡單的攔截器,作用僅僅是輸出攔截時間。我們可以看到有四個攔截時機,處理請求前,處理請求后,完成請求后和異步處理開始后,這些攔截方法的參數(shù)是Http請求和響應,使用很方便。
public class LogInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle:" + LocalTime.now());
return super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle:" + LocalTime.now());
super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion:" + LocalTime.now());
super.afterCompletion(request, response, handler, ex);
}
@Override
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("afterConcurrentHandlingStarted:" + LocalTime.now());
super.afterConcurrentHandlingStarted(request, response, handler);
}
}
有了攔截器之后,我們還需要注冊它。首先將其注冊為一個Spring Bean。然后定義一個RequestMappingHandlerMapping
并將攔截器傳遞給它。
<beans>
<bean id="handlerMapping"
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="interceptors">
<list>
<ref bean="logInterceptor"/>
</list>
</property>
</bean>
<bean id="logInterceptor"
class="samples.LogInInterceptor"/>
</beans>
定義攔截器的這部分配置可以使用mvc命名空間簡化。
<mvc:interceptors>
<ref bean="logInterceptor"/>
</mvc:interceptors>
默認情況下攔截器針對所有處理方法。如果希望只匹配某些URL,可以定義一個org.springframework.web.servlet.handler.MappedInterceptor
,使用它的構(gòu)造方法設置映射。
<bean class="org.springframework.web.servlet.handler.MappedInterceptor">
<constructor-arg index="0">
<list>
<value>/</value>
</list>
</constructor-arg>
<constructor-arg index="1" ref="logInterceptor"/>
</bean>
也可以直接使用mvc命名空間。
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/"/>
<ref bean="logInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
攔截器可能不適用@ResponseBody
和ResponseEntity
方法,因為這些方法會使用HttpMessageConverter
來輸出響應。這時候我們可以實現(xiàn)ResponseBodyAdvice
接口,然后使用@ControllerAdvice注解或者直接在RequestMappingHandlerAdapter.
配置它們來攔截這些方法。