1 Spring MVC的職責
說明:本文中框架直接使用Spring Boot,因此除了特別說明,都使用默認配置。並且只講解相關操作,不涉及深入的原理。
我們可以將前后端開發中的各個組成部分做一個抽象,它們之間的關系如下圖所示:
在瀏覽器-服務器的交互過程中,Spring MVC起着“郵局”的作用。它一方面會從瀏覽器接收各種各樣的“來信”(HTTP請求),並把不同的請求分發給對應的服務層進行業務處理;另一方面會發送“回信”(HTTP響應),將服務器處理后的結果回應給瀏覽器。
因此,開發人員就像是“郵遞員”,主要需要完成三方面工作:
- 指定分發地址:使用
@RequestMapping
等注解指定不同業務邏輯對應的URL。 - 接收請求數據:使用
@RequestParam
等注解接收不同類型的請求數據。 - 發送響應數據:使用
@ResponseBody
等注解發送不同類型的響應數據。
本文涉及到的相關依賴:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在介紹Spring MVC這三方面的工作內容之前,我們先來看一下如何使用@Controller
或@RestController
標注XxxController
類。
@Controller
:
package com.xianhuii.controller;
import org.springframework.stereotype.Controller;
@Controller
public class StudentController {
}
最基礎的做法是使用@Controller
注解將我們的XxxController
類聲明為Spring容器管理的Controller,其源碼如下。
package org.springframework.stereotype;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(annotation = Component.class)
String value() default "";
}
@Controller
的元注解是@Component
,它們的功能相同,只不過@Controller
顯得更加有語義,便於開發人員理解。
此外,需要注意的是@Controller
頭上@Target
的值是ElementType.Type
,說明它只能標注在類上。
@Controller
有且僅有一個value
屬性,該屬性指向@Component
注解,用來指示對應的beanName
。如果沒有顯式指定該屬性,Spring的自動檢測組件會將首字母小寫的類名設置為beanName
。即上面實例代碼StudentController
類的beanName
為studentController
。
@RestController
:
package com.xianhuii.controller;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StudentController {
}
在前后端分離的開發環境下,@RestController
是開發人員更好的選擇。它除了具有上述@Controller
聲明Controller的功能外,還可以自動將類中所有方法的返回值綁定到HTTP響應體中(而不再是視圖相關信息),其源碼如下。
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Controller;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}
@RestController
的元注解包括@Controller
和@ResponseBody
,分別起着聲明Controller和綁定方法返回值的作用。
此外,需要注意的是@RestController
頭上@Target
的值也是ElementType.Type
,說明它只能標注在類上。
@Controller
有且僅有一個value
屬性,該屬性指向@Controller
注解(最終指向@Component
),用來指示對應的beanName
。如果沒有顯式指定該屬性,Spring的自動檢測組件會將首字母小寫的類名設置為beanName
。即上面實例代碼StudentController
類的beanName
為studentController
。
2 指定分發地址
映射請求分發地址的注解以@Mapping
為基礎,並有豐富的實現:
2.1 @RequestMapping
2.1.1 標注位置
@RequestMapping
是最基礎的指定分發地址的注解,它既可以標注在XxxController
類上,也可以標注在其中的方法上。理論上有三種組合方式:類、方法和類+方法。但是,實際上只有后面兩種方式能起作用。
- 僅標注在方法上:
@RestController
public class StudentController {
@RequestMapping("/getStudent")
public Student getStudent() {
// 簡單模擬獲取student流程
return new Student("Xianhuii", 18);
}
}
此時,@RequestMapping
的/getStudent
屬性值表示相對於服務端套接字的請求地址。
從瀏覽器發送GET http://localhost:8080/getStudent
請求,會得到如下響應,響應體是Student
對象的JSON字符串:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 02 May 2021 13:23:02 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"name": "Xianhuii",
"age": 18
}
- 類+方法:
@RequestMapping("/student")
@RestController
public class StudentController {
@RequestMapping("/getStudent")
public Student getStudent() {
// 簡單模擬獲取student流程
return new Student("Xianhuii", 18);
}
}
此時,標注在類上的@RequestMapping
是內部所有方法分發地址的基礎。因此,getStudent()
方法的完整分發地址應該是/student/getStudent
。
從瀏覽器發送GET http://localhost:8080/student/getStudent
請求,會得到如下響應,響應體是Student
對象的JSON字符串:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 02 May 2021 13:26:57 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"name": "Xianhuii",
"age": 18
}
- 僅標注在類上(注意:此方式不起作用):
@RequestMapping("/student")
@RestController
public class StudentController {
public Student getStudent() {
// 簡單模擬獲取student流程
return new Student("Xianhuii", 18);
}
}
我們僅將@RequestMapping
標注在StudentController
類上。需要注意的是,這種標注方式是錯誤的,服務器不能確定具體的分發方法到底是哪個(盡管我們僅定義了一個方法)。
如果從瀏覽器發送GET http://localhost:8080/student
請求,會得到如下404的響應:
HTTP/1.1 404
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 02 May 2021 13:36:56 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"timestamp": "2021-05-02T13:36:56.056+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/student"
}
以上介紹了@RequestMapping
的標注位置,在此做一個小結:
@RequestMapping
的標注方式有兩種:方法或類+方法。- 如果將
@RequestMapping
標注在類上,那么該value
屬性值是基礎,實際的分發地址是類和方法上@RequestMapping
注解value
屬性值的拼接。如果類和方法上@RequestMapping
注解value
屬性值分別為/classValue
和/methodValue
,實際分發地址為/classValue/methodValue
。 - 分發地址相對於服務器套接字。如果服務器套接字為
http://localhost:8080
,分發地址為/student
,那么對應的HTTP請求地址應該是http://localhost:8080/student
。
2.1.2 常用屬性
@RequestMapping
的屬性有很多,但是常用的只有value
、path
和method
。其中value
和path
等價,用來指定分發地址。method
則用來指定對應的HTTP請求方式。
1、value
和path
對於value
和path
屬性,它們的功能其實我們之前就見到過了:指定相對於服務器套接字的分發地址。要小心的是在類上是否標注了@RequestMapping
。
如果@RequestMapping
不顯式指定屬性名,那么默認是value
屬性:
@RequestMapping("student")
當然我們也可以顯式指定屬性名:
@RequestMapping(value = "student")
@RequestMapping(path = "student")
需要注意的是value
和path
屬性的類型是String[]
,這表示它們可以同時指定多個分發地址,即一個方法可以同時處理多個請求。如果我們指定了兩個分發地址:
@RestController
public class StudentController {
@RequestMapping(path = {"student", "/getStudent"})
public Student getStudent() {
// 簡單模擬獲取student流程
return new Student("Xianhuii", 18);
}
}
此時,無論瀏覽器發送GET http://localhost:8080/student
或GET http://localhost:8080/getStudent
哪種請求,服務器斗毆能正確調用getStudent()
方法進行處理。最終都會得到如下響應:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 02 May 2021 14:06:47 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"name": "Xianhuii",
"age": 18
}
我們對value
和path
屬性做一個小結:
- 在不顯式聲明屬性名的時候,默認為
value
屬性,如@RequestMapping("/student")
等價於@RequestMapping(value = "/student")
。 - 在聲明多個
@RequestMapping
的屬性時,必須顯式指出value
屬性名,如@RequestMapping(value = "student", method = RequestMethod.GET)
。 value
和path
等價,如@RequestMapping(value = "/student")
等價於@RequestMapping(path = "/student")
。value
和path
屬性的類型是String[]
,一般至少為其指定一個值。在指定多個值的情況下,需要用{}
將值包裹,如@RequestMapping({"/student", "/getStudent"})
,此時表示該方法可以處理的所有分發地址。- 需要注意類上是否標注了
@RequestMapping
,如果標注則為分發地址的基礎,具體方法的實際分發地址需要與之進行拼接。 - 此外,在某些情況下,
@RequestMapping
的作用不是指定分發地址,可以不指定該屬性值。
2、method
method
屬性用來指定映射的HTTP請求方法,包括GET
、POST
、HEAD
、OPTIONS
、PUT
、PATCH
、DELETE
和TRACE
,分別對應RequestMethod
枚舉類中的不同值:
package org.springframework.web.bind.annotation;
public enum RequestMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
}
method
屬性的類型是RequestMethod[]
,表明其可以聲明零個、一個或多個RequestMethod
枚舉對象。
- 零個
RequestMethod
枚舉對象:
@RestController
public class StudentController {
@RequestMapping("student")
public Student getStudent() {
// 簡單模擬獲取student流程
return new Student("Xianhuii", 18);
}
}
當沒有為method
屬性指定明確的RequestMethod
枚舉對象時(即默認情況),表明該方法可以映射所有HTTP請求方法。此時,無論是GET http://localhost:8080/student
還是POST http://localhost:8080/student
請求,都可以被getStudent()
方法處理:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 02 May 2021 15:12:44 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"name": "Xianhuii",
"age": 18
}
- 一個
RequestMethod
枚舉對象:
@RestController
public class StudentController {
@RequestMapping(value = "student", method = RequestMethod.GET)
public Student getStudent() {
// 簡單模擬獲取student流程
return new Student("Xianhuii", 18);
}
}
當顯式為method
屬性指定某個RequestMethod
枚舉類時(這個例子中是RequestMethod.GET
),表明該方法只可以處理對應的HTTP請求方法。此時,GET http://localhost:8080/student
請求可以獲得與前面例子中相同的正確響應。而POST http://localhost:8080/student
請求卻會返回405響應,並指明服務器支持的是GET方法:
HTTP/1.1 405
Allow: GET
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 02 May 2021 15:17:05 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"timestamp": "2021-05-02T15:17:05.515+00:00",
"status": 405,
"error": "Method Not Allowed",
"message": "",
"path": "/student"
}
- 多個
RequestMethod
枚舉對象:
@RestController
public class StudentController {
@RequestMapping(value = "student", method = {RequestMethod.GET, RequestMethod.POST})
public Student getStudent() {
// 簡單模擬獲取student流程
return new Student("Xianhuii", 18);
}
}
當顯式為method
屬性指定多個RequestMethod
枚舉對象時,需要使用{}
包裹起來,表明該方法支持所指定的所有方法,但是沒有指定的方法則不會支持。此時,我們指定了method = {RequestMethod.GET, RequestMethod.POST}
,說明getStudent()
方法可以支持GET
和POST
兩種HTTP請求方法。因此,發送GET http://localhost:8080/student
或POST http://localhost:8080/student
都能得到正確的響應。但是若發送其他HTTP請求方法,如PUT http://localhost:8080/student
,則同樣會返回上述405響應。
除了指定method
屬性值的個數,其標注位置也十分重要。如果在類上@RequestMapping
的method
屬性中指定了某些RequestMethod
枚舉對象,這些對象會被實際方法繼承:
@RequestMapping(method = RequestMethod.GET)
@RestController
public class StudentController {
@RequestMapping(value = "student", method = RequestMethod.POST)
public Student getStudent() {
// 簡單模擬獲取student流程
return new Student("Xianhuii", 18);
}
}
此時在StudentController
類上指定了method = RequestMethod.GET
,而getStudent()
方法上指定了method = RequestMethod.POST
。此時,getStudent()
方法會從StudentController
類上繼承該屬性,從而實際上為method = {RequestMethod.GET, RequestMethod.POST}
。因此,該方法可以接收GET http://localhost:8080/student
或POST http://localhost:8080/student
請求。當然,其他請求會響應405。
另外比較有趣的是,此時可以不必為StudentController
類上的@RequestMapping
指定value
屬性值。因為此時它的作用是類中的所有方法指定共同支持的HTTP請求方法。
3、源碼
package org.springframework.web.bind.annotation;
/**
* Annotation for mapping web requests onto methods in request-handling classes
* with flexible method signatures.
* —— 將web請求映射到方法的注解
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping // ——web映射的元注解,其中沒有任何屬性,相當於標記
public @interface RequestMapping {
/**
* Assign a name to this mapping. ——映射名
*/
String name() default "";
/**
* The primary mapping expressed by this annotation. ——映射路徑
*/
@AliasFor("path")
String[] value() default {};
/**
* The path mapping URIs (e.g. {@code "/profile"}). ——映射路徑
*/
@AliasFor("value")
String[] path() default {};
/**
* The HTTP request methods to map to, narrowing the primary mapping:
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit this
* HTTP method restriction.
* ——映射HTTP請求方法。
* ——當標記在類上時,會被所有方法級別的映射繼承。
*/
RequestMethod[] method() default {};
/**
* The parameters of the mapped request, narrowing the primary mapping.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit this
* parameter restriction.
* ——映射請求參數,如params = "myParam=myValue"或params = "myParam!=myValue"。
* ——當標記在類上時,會被所有方法級別的映射繼承。
*/
String[] params() default {};
/**
* The headers of the mapped request, narrowing the primary mapping.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit this
* header restriction.
* ——映射請求頭,如headers = "My-Headre=myValue"或headers = "My-Header!=myValue"。
* ——當標記在類上時,會被所有方法級別的映射繼承。
*/
String[] headers() default {};
/**
* Narrows the primary mapping by media types that can be consumed by the
* mapped handler. Consists of one or more media types one of which must
* match to the request {@code Content-Type} header.
* <p><b>Supported at the type level as well as at the method level!</b>
* If specified at both levels, the method level consumes condition overrides
* the type level condition.
* ——映射請求媒體類型(media types),即服務端能夠處理的媒體類型,如:
* consumes = "!text/plain"
* consumes = {"text/plain", "application/*"}
* consumes = MediaType.TEXT_PLAIN_VALUE
* ——當標記在類上時,會被所有方法級別的映射繼承。
*/
String[] consumes() default {};
/**
* Narrows the primary mapping by media types that can be produced by the
* mapped handler. Consists of one or more media types one of which must
* be chosen via content negotiation against the "acceptable" media types
* of the request.
* <p><b>Supported at the type level as well as at the method level!</b>
* If specified at both levels, the method level produces condition overrides
* the type level condition.
* ——映射響應媒體類型(media types),即客戶端能夠處理的媒體類型,如:
* produces = "text/plain"
* produces = {"text/plain", "application/*"}
* produces = MediaType.TEXT_PLAIN_VALUE
* produces = "text/plain;charset=UTF-8"
* ——當標記在類上時,會被所有方法級別的映射繼承。
*/
String[] produces() default {};
}
我們對method
屬性做一個小結:
method
屬性用來指定方法所支持的HTTP請求方法,對應為RequestMethod
枚舉對象。method
屬性的類型是RequestMethod[]
,可以指定零個至多個RequestMethod
枚舉對象。零個時(默認情況)表明支持所有HTTP請求方法,多個時則僅支持指定的HTTP請求方法。- 類上
@RequestMapping
的method
屬性所指定的RequestMethod
枚舉對象,會被具體的方法繼承。可以使用該方式為所有方法指定同一支持的HTTP請求方法。
2.2 @XxxMapping
在@RequestMapping
的基礎上,Spring根據不同的HTTP請求方法,實現了具體化的@XxxMapping
注解。如@GetMapping
、@PostMapping
、@PutMapping
、@DeleteMapping
和@PatchMapping
。
它們並沒有很神秘,只是以@RequestMapping
為元注解,因此具有之前介紹的所有屬性,用法也完全一樣。唯一特殊的是在@RequestMapping
的基礎上指定了對應的method
屬性值,例如@GetMapping
顯式指定了method = RequestMethod.GET
。
需要注意的是,@XxxMapping
只能用作方法級別,此時可以結合類級別的@RequestMapping
定制分發地址:
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
相對於@RequestMapping
,增強版@XxxMapping
顯得更加有語義,便於開發人員閱讀。我們以@GetMapping
為例,簡單看一下其源碼:
package org.springframework.web.bind.annotation;
@Target(ElementType.METHOD) // 只能用作方法級別
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET) // 以@RequestMapping為元注解,並指定了對應的method屬性
public @interface GetMapping {
@AliasFor(annotation = RequestMapping.class)
String name() default ""; // 映射名
@AliasFor(annotation = RequestMapping.class)
String[] value() default {}; // 映射路徑
@AliasFor(annotation = RequestMapping.class)
String[] path() default {}; // 映射路徑
@AliasFor(annotation = RequestMapping.class)
String[] params() default {}; // 映射參數
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {}; // 映射請求頭
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {}; // 映射服務器能接收媒體類型
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {}; // 映射客戶端能接收媒體類型
}
2.3 @PathVariable
@PathVariable
是一種十分特別的注解,從功能上來看它並不是用來指定分發地址的,而是用來接收請求數據的。但是由於它與@XxxMapping
系列注解的關系十分密切,因此放到此部分來講解。
@PathVariable
的功能是:獲取分發地址上的路徑變量。
@XxxMapping
中的路徑變量聲明形式為{}
,內部為變量名,如@RequestMapping("/student/{studentId}")
。后續我們在對應方法參數前使用@PathVariable
獲取該路徑變量的值,如pubic Student student(@PathVariable int studentId)
。該變量的類型會自動轉換,如果轉化失敗會拋出TypeMismatchException
異常。
我們也可以同時聲明和使用多個路徑變量:
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
或:
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
我們甚至可以使用{valueName:regex}
的方式指定該路徑變量的匹配規則:
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}
上述情況中,我們都沒有為@PathVariable
指定value
屬性,因此路徑變量名必須與方法形參名一致。我們也可以顯式指定value
屬性與路徑變量名一致,此時方法形參名就可以隨意:
@RestController
public class StudentController {
@PostMapping("/student/{studentId}")
public int getStudent(@PathVariable("studentId") int id) {
return id;
}
}
我們來看一下@PathVairable
的源碼:
package org.springframework.web.bind.annotation;
@Target(ElementType.PARAMETER) // 只能標注在形參上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
/**
* Alias for {@link #name}. 同name屬性,即形參綁定的路徑變量名。
*/
@AliasFor("name")
String value() default "";
/**
* The name of the path variable to bind to. 形參綁定的路徑變量名
*/
@AliasFor("value")
String name() default "";
/**
* Whether the path variable is required. 路徑變量是否是必須的。
*/
boolean required() default true;
}
最后,我們來總結一下@PathVariable
的用法:
@PathVariable
只能標注在方法形參上,用來匹配@XxxMapping()
中形如{pathVariableName}
的路徑變量。- 如果沒有顯式指定
value
或name
屬性,則形參名必須與對應的路徑變量名一致。 - 路徑變量中可以使用
{pathVariableName:regex}
方式指明匹配規則。
3 接收請求數據
我們可以直接在Controller的方法的形參中使用特定的注解,來接收HTTP請求中特定的數據,包括請求參數、請求頭、請求體和cookie等。
也可以直接聲明特定的形參,從而可以獲取框架中用於與客戶端交互的特殊對象,包括HttpServletRequest
和HttpServletResponse
等。
3.1 @RequestParam
@RequestParam
用來接收HTTP請求參數,即在分發地址之后以?
開頭的部分。
請求參數本質上是鍵值對集合,我們使用@RequestParam
來獲取某個指定的參數值,並且在這個過程中會進行自動類型轉換。
例如,對於GET http://localhost:8080/student?name=Xianhuii&age=18
請求,我們可以使用如下方式來接收其請求參數name=Xianhuii&age=18
:
@RestController
public class StudentController {
@GetMapping("/student")
public Student getStudent(@RequestParam String name, @RequestParam int age) {
// 簡單模擬獲取student流程
Student student = new Student(name, age);
return student;
}
}
上述過程沒有顯式指定@RequestParam
的value
或name
屬性,因此形參名必須與請求參數名一一對應。如果我們顯式指定了value
或name
屬性,那么形參名就可以任意了:
@RestController
public class StudentController {
@GetMapping("/student")
public Student getStudent(@RequestParam("name") String str, @RequestParam("age") int num) {
// 簡單模擬獲取student流程
Student student = new Student(str, num);
return student;
}
}
如果我們使用Map<String, String>
或MultiValueMap<String, String>
作為形參,那么會將所有請求參數納入該集合中,並且此時對value
或name
屬性沒有要求:
@RestController
public class StudentController {
@GetMapping("/student")
public Student getStudent(@RequestParam Map<String, String> params) {
params.forEach((key, val)-> System.out.println(key + ": " + val));
// 簡單模擬獲取student流程
Student student = new Student(params.get("name"), Integer.parseInt(params.get("age")));
return student;
}
}
我們來看一下@RequestParam
源碼:
package org.springframework.web.bind.annotation;
@Target(ElementType.PARAMETER) // 只能標注在形參上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
/**
* Alias for {@link #name}. 同name屬性,即綁定的請求參數名。
*/
@AliasFor("name")
String value() default "";
/**
* The name of the request parameter to bind to. 綁定的請求參數名。
*/
@AliasFor("value")
String name() default "";
/**
* Whether the parameter is required.
* <p>Defaults to {@code true}, leading to an exception being thrown
* if the parameter is missing in the request. Switch this to
* {@code false} if you prefer a {@code null} value if the parameter is
* not present in the request.
* <p>Alternatively, provide a {@link #defaultValue}, which implicitly
* sets this flag to {@code false}.
*/
boolean required() default true;
/**
* The default value to use as a fallback when the request parameter is
* not provided or has an empty value. 默認值,如果沒有提供該請求參數,則會使用該值。
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
最后,我們來總結一下@RequestParam
的用法:
@RequestParam
標注在方法形參上,用來獲取HTTP請求參數值。- 如果形參為基本類型,可以獲取對應的請求參數值。此時需要注意請求參數名是否需要與形參名一致(是否指定
value
或name
屬性)。 - 如果形參為
Map<String, String>
或MultiValueMap<String, String>
,則可以一次性獲取全部請求參數。此時請求參數名與形參名無關。 required
屬性默認為true
,此時必須保證HTTP請求中包含與形參一致的請求參數,否則會報錯。- 我們可以使用
defaultValue
屬性指定默認值,此時required
自動指定成false
,表示如果沒有提供該請求參數,則會使用該值。
3.2 @RequestHeader
@RequestHeader
用來獲取HTTP請求頭。
請求頭本質上也是鍵值對集合,只相對於請求參數,它們的鍵都具有固定的名字:
Accept-Encoding: UTF-8
Keep-Alive: 1000
例如,我們可以使用下面方式來獲取請求頭中的Accept-Encoding
和Keep-Alive
值:
@RestController
public class StudentController {
@GetMapping("/header")
public void handle(
@RequestHeader("Accept-Encoding") String encoding,
@RequestHeader("Keep-Alive") long keepAlive) {
System.out.println("Accept-Encoding: " + encoding); // Accept-Encoding: UTF-8
System.out.println("Keep-Alive: " + keepAlive); // Keep-Alive: 1000
}
}
理論上,我們也可以不顯式指定@RequestHeader
的value
或name
屬性值,而使用對應的形參名。但是由於HTTP請求頭中一般含有-
,而Java不支持此種命名方式,因此推薦還是顯式指定value
或name
屬性值。
另外,我們也可以使用Map<String, String>
或MultiValueMap<String, String>
一次性獲取所有請求頭,此時形參名與請求頭參數名沒有關系:
@RestController
public class StudentController {
@GetMapping("/header")
public void handle(@RequestHeader Map<String, String> headers) {
// headers.keySet().forEach(key->System.out.println(key));
System.out.println("Accept-Encoding: " + headers.get("accept-encoding"));
System.out.println("Keep-Alive: " + headers.get("keep-alive"));
}
}
此時我們需要注意請求頭的名為小寫形式,如accept-encoding
。我們可以遍歷headers.keySet()
進行查看。
我們來看看@RequestHeader
的源碼,可以發現與@RequestParam
十分相似:
package org.springframework.web.bind.annotation;
@Target(ElementType.PARAMETER) // 只可以標注在形參上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestHeader {
/**
* Alias for {@link #name}. 同name屬性,即綁定的請求頭名。
*/
@AliasFor("name")
String value() default "";
/**
* The name of the request header to bind to. 綁定的請求頭名
*/
@AliasFor("value")
String name() default "";
/**
* Whether the header is required.
* <p>Defaults to {@code true}, leading to an exception being thrown
* if the header is missing in the request. Switch this to
* {@code false} if you prefer a {@code null} value if the header is
* not present in the request.
* <p>Alternatively, provide a {@link #defaultValue}, which implicitly
* sets this flag to {@code false}.
*/
boolean required() default true;
/**
* The default value to use as a fallback.
* <p>Supplying a default value implicitly sets {@link #required} to
* {@code false}.
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
最后,我們來總結一下@RequestHeader
的用法:
@RequestHeader
標注在方法形參上,用來獲取HTTP請求頭,一般推薦使用value
或name
顯式指定請求頭名。- 也可以使用
Map<String, String>
或MultiValueMap<String, String>
一次性獲取所有請求頭,但是從該集合中獲取對應值時要注意其key
值的大小寫形式,如accept-encoding
。 - 我們也可以使用
required
或defaultValue
對是否必須具備該請求頭進行特殊處理。
3.3 @CookieValue
我們可以將Cookie當做特殊的請求頭,它的值是鍵值對集合,形如Cookie: cookie1=value1; cookie2 = value2
。
因此也可以使用之前的@RequestHeader
進行獲取:
@RestController
public class StudentController {
@GetMapping("/header")
public void handle(@RequestHeader("cookie") String cookie) {
System.out.println(cookie); // cookie1=value1; cookie2 = value2
}
}
但是,一般來說我們會使用@CookieValue
顯式獲取Cookie鍵值對集合中的指定值:
@RestController
public class StudentController {
@GetMapping("/cookie")
public void handle(@CookieValue("cookie1") String cookie) {
System.out.println(cookie); // value1
}
}
同樣,我們也可以不顯式指定value
或name
屬性值,此時形參名應與需要獲取的cookie鍵值對的key一致:
@RestController
public class StudentController {
@GetMapping("/cookie")
public void handle(@CookieValue String cookie1) {
System.out.println(cookie1); // value1
}
}
需要注意的是,默認情況下不能同之前的@RequestParam
或@RequestHeader
那樣使用Map
或MultiValueMap
來一次性獲取所有cookies。
我們來看一下@CookieValue
的源碼,其基本定義與@RequestParan
或@RequestHeader
完全一致,因此用法也類似:
package org.springframework.web.bind.annotation;
@Target(ElementType.PARAMETER) // 只能標注在形參上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CookieValue {
/**
* Alias for {@link #name}.
*/
@AliasFor("name")
String value() default "";
/**
* The name of the cookie to bind to.
* @since 4.2
*/
@AliasFor("value")
String name() default "";
/**
* Whether the cookie is required.
* <p>Defaults to {@code true}, leading to an exception being thrown
* if the cookie is missing in the request. Switch this to
* {@code false} if you prefer a {@code null} value if the cookie is
* not present in the request.
* <p>Alternatively, provide a {@link #defaultValue}, which implicitly
* sets this flag to {@code false}.
*/
boolean required() default true;
/**
* The default value to use as a fallback.
* <p>Supplying a default value implicitly sets {@link #required} to
* {@code false}.
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
最后,總結一下@CookieValue
的用法:
@CookieValue
標注在方法形參上,用來獲取HTTP請求中對應的cookie值。- 需要注意方法形參名是否需要與cookie鍵相對應(是否指定了
required
或defaultValue
屬性)。 - 注意:不能使用
Map
或MultiValueMap
一次性獲取所有cookies鍵值對。
3.4 @RequestBody
@RequestBody
可以接收HTTP請求體中的數據,但是必須要指定Content-Type
請求體的媒體類型為application/json
,表示接收json
類型的數據。
Spring會使用HttpMessageConverter
對象自動將對應的數據解析成指定的Java對象。例如,我們發送如下HTTP請求:
POST http://localhost:8080/student
Content-Type: application/json
{
"name": "Xianhuii",
"age": 18
}
我們可以在Controller中編寫如下代碼,接收請求體中的json
數據並轉換成Student
對象:
@RestController
public class StudentController {
@PostMapping("/student")
public void handle(@RequestBody Student student) {
System.out.println(student); // Student{name='Xianhuii', age=18}
}
}
一般來說在Controller方法中僅可聲明一個@RequestBody
注解的參數,將請求體中的所有數據轉換成對應的POJO對象。
我們來看一下@RequestBody
的源碼:
package org.springframework.web.bind.annotation;
@Target(ElementType.PARAMETER) // 只可以標注到方法形參上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestBody {
/**
* Whether body content is required.
*/
boolean required() default true;
}
可見@RequestBody
的定義十分簡單,它只有一個required
屬性。如果required
為true
,表示請求體中必須包含對應數據,否則會拋異常。如果required
為false
,表示請求體中可以沒有對應數據,此時形參值為null
。
最后,總結一下@RequestBody
用法:
@RequestBody
標注在方法形參上,用來接收HTTP請求體中的json
數據。
3.5 HttpEntity<T>
上面介紹的注解都只是獲取HTTP請求中的某個部分,比如@RequestParam
獲取請求參數、@RequestHeader
獲取請求頭、@CookieValue
獲取cookies、@RequestBody
獲取請求體。
Spring提供了一個強大的HttpEntity<T>
類,它可以同時獲取HTTP請求的請求頭和請求體。
例如,對於如下HTTP請求:
POST http://localhost:8080/student
Content-Type: application/json
Cookie: cookie1=value1; cookie2 = value2
{
"name": "Xianhuii",
"age": 18
}
我們也可以編寫如下接收方法,接收所有數據:
@RestController
public class StudentController {
@PostMapping("/student")
public void handle(HttpEntity<Student> httpEntity) {
Student student = httpEntity.getBody();
HttpHeaders headers = httpEntity.getHeaders();
System.out.println(student); // Student{name='Xianhuii', age=18}
/** [
* content-length:"37",
* host:"localhost:8080",
* connection:"Keep-Alive",
* user-agent:"Apache-HttpClient/4.5.12 (Java/11.0.8)",
* cookie:"cookie1=value1; cookie2 = value2",
* accept-encoding:"gzip,deflate",
* Content-Type:"application/json;charset=UTF-8"
* ]
*/
System.out.println(headers);
}
}
HttpEntity<T>
類中只包含三個屬性:
其中,靜態變量EMPTY
是一個空的HttpEntity
緩存(new HttpEntity<>()
),用來表示統一的沒有請求頭和請求體的HttpEntity
對象。
因此,可以認為一般HttpEntity
對象中值包含headers
和body
兩個成員變量,分別代表請求頭和請求體,對應為HttpHeaders
和泛型T
類型。我們可以調用HttpEntity
的getHeaders()
或getBody()
方法分別獲取到它們的數據。
另外,HttpHeaders
類中只有一個Map
屬性:final MultiValueMap<String, String> headers
,為各種請求頭的集合。我們可以對其進行集合相關操作,獲取到需要的請求頭。
3.6 @RequestPart
和MultipartFile
Spring提供了@RequestPart
注解和MultipartFile
接口,專門用來接收文件。
我們先來編寫一個極簡版前端的文件上傳表單:
<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
<input name="image" type="file">
<input name="text" type="file">
<button type="submit">上傳</button>
</form>
其中action
指定提交路徑,對應為處理方法的分發地址。method
指定為post
方式。enctype
指定為multipart/form-data
格式。這里我們在內部定義了兩個file
類型的<input>
標簽,表示同時上傳兩個文件,用來說明多文件上傳的情況(單文件上傳的方式也相同)。
后端處理器:
@RestController
public class FileController {
@PostMapping("/upload")
public void upload(@RequestPart("image") MultipartFile image, @RequestPart("text") MultipartFile text) {
System.out.println(image);
System.out.println(text);
}
}
在Controller的對應方法中只需要聲明MultipartFile
形參,並標注@RequestPart
注解,即可接收到對應的文件。這里我們聲明了兩個MultipartFile
形參,分別用來接收表單中定義的兩個文件。
注意到此時形參名與表單中標簽名一致,所以其實這里也可以不顯式指出@RequestPart
的value
或name
屬性(但是不一致時必須顯式指出):
public void upload(@RequestPart MultipartFile image, @RequestPart MultipartFile text)
先來看一下@RequestPart
的源碼,我保留了比較重要的文檔:
package org.springframework.web.bind.annotation;
/**
* Annotation that can be used to associate the part of a "multipart/form-data" request
* with a method argument. 此注解用來將方法形參與"multipart/form-data"請求中的某個部分相關聯。
*
* <p>Supported method argument types include {@link MultipartFile} in conjunction with
* Spring's {@link MultipartResolver} abstraction, {@code javax.servlet.http.Part} in
* conjunction with Servlet 3.0 multipart requests, or otherwise for any other method
* argument, the content of the part is passed through an {@link HttpMessageConverter}
* taking into consideration the 'Content-Type' header of the request part. This is
* analogous to what @{@link RequestBody} does to resolve an argument based on the
* content of a non-multipart regular request.
* 需要與MultipartFile結合使用。與@RequestBody類似(都解析請求體中的數據),但是它是不分段的,而RequestPart是分段的。
*
* <p>Note that @{@link RequestParam} annotation can also be used to associate the part
* of a "multipart/form-data" request with a method argument supporting the same method
* argument types. The main difference is that when the method argument is not a String
* or raw {@code MultipartFile} / {@code Part}, {@code @RequestParam} relies on type
* conversion via a registered {@link Converter} or {@link PropertyEditor} while
* {@link RequestPart} relies on {@link HttpMessageConverter HttpMessageConverters}
* taking into consideration the 'Content-Type' header of the request part.
* {@link RequestParam} is likely to be used with name-value form fields while
* {@link RequestPart} is likely to be used with parts containing more complex content
* e.g. JSON, XML).
* 在"multipart/form-data"請求情況下,@RequestParam也能以鍵值對的方式解析。而@RequestPart能解析更加復雜的內容:JSON等
*/
@Target(ElementType.PARAMETER) // 只能標注在方法形參上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestPart {
/**
* Alias for {@link #name}. 同name。
*/
@AliasFor("name")
String value() default "";
/**
* The name of the part in the {@code "multipart/form-data"} request to bind to.
* 對應"multipart/form-data"請求中某個部分的名字
*/
@AliasFor("value")
String name() default "";
/**
* Whether the part is required. 是否必須。
*/
boolean required() default true;
}
通過上述方式得到客戶端發送過來的文件后,我們就可以使用MultipartFile
中的各種方法對該文件進行操作:
我們在這里舉一個最簡單的例子,將上傳的兩個文件保存在桌面下的test
文件夾中:
@RestController
public class FileController {
@PostMapping("/upload")
public void upload(@RequestPart MultipartFile image, @RequestPart MultipartFile text) throws IOException {
String path = "C:/Users/Administrator/Desktop/test";
String originImageName = image.getOriginalFilename();
String originTextName = text.getOriginalFilename();
File img = new File(path, UUID.randomUUID() + "." + originImageName.substring(originImageName.indexOf(".")));
File txt = new File(path, UUID.randomUUID() + "." + originTextName.substring(originTextName.indexOf(".")));
image.transferTo(img);
text.transferTo(txt);
}
}
最后,我們@RequestPart
和MultipartFile
接口做一個總結:
@RequestPart
專門用來處理multipart/form-data
類型的表單文件,可以將方法形參與表單中各個文件單獨關聯。@RequestPart
需要與MultipartFile
結合使用。@RequestParam
也能進行解析multipart/form-data
類型的表單文件,但是它們原理不同。MultipartFile
表示接收到的文件對象,通過使用其各種方法,可以對文件進行操作和保存。
4 發送響應數據
對請求數據處理完成之后,最后一步是需要向客戶端返回一個結果,即發送響應數據。
4.1 @ResponseBody
@ResponseBody
可以標注在類或方法上,它的作用是將方法返回值作為HTTP響應體發回給客戶端,與@ResquestBody
剛好相反。
我們可以將它標注到方法上,表示僅有handle()
方法的返回值會被直接綁定到響應體中,注意到此時類標注成@Controller
:
@Controller
public class StudentController {
@ResponseBody
@GetMapping("/student")
public Student handle() {
return new Student("Xianhuii", 18);
}
}
我們也可以將它標注到類上,表示類中所有方法的返回值都會被直接綁定到響應體中:
@ResponseBody
@Controller
public class StudentController {
@GetMapping("/student")
public Student handle() {
return new Student("Xianhuii", 18);
}
}
此時,@ResponseBody
和@Controller
相結合,就變成了@RestController
注解,也是前后端分離中最常用的注解:
@RestController
public class StudentController {
@GetMapping("/student")
public Student handle() {
return new Student("Xianhuii", 18);
}
}
如果客戶端發送如下HTTP請求:GET http://localhost:8080/student
。此時上述代碼都會有相同的HTTP響應,表示接收到student
的json
數據:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 04 May 2021 13:04:15 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"name": "Xianhuii",
"age": 18
}
我們來看看@ResponseBody
的源碼:
package org.springframework.web.bind.annotation;
/**
* Annotation that indicates a method return value should be bound to the web
* response body. Supported for annotated handler methods.
*
* <p>As of version 4.0 this annotation can also be added on the type level in
* which case it is inherited and does not need to be added on the method level.
*/
@Target({ElementType.TYPE, ElementType.METHOD}) // 可以標注到類或方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}
最后,我們總結一下@ResponseBody
的用法:
@ResponseBody
表示將方法返回值直接綁定到web響應體中。@ResponseBody
可以標注到類或方法上。類上表示內部所有方法的返回值都直接綁定到響應體中,方法上表示僅有該方法的返回值直接綁定到響應體中。@ResponseBody
標注到類上時,與@Controller
相結合可以簡寫成@RestController
,這也是通常使用的注解。- 我們可以靈活地構造合適的返回對象,結合
@ResponseBody
,用作與實際項目最匹配的響應體返回。
4.2 ResponseEntity<T>
ResponseEntity<T>
是HttpEntity<T>
的子類,它除了擁有父類中的headers
和body
成員變量,自己還新增了一個status
成員變量。因此,ResponseEntity<T>
集合了響應體的三個最基本要素:響應頭、狀態碼和響應數據。它的層次結構如下:
status
成員變量一般使用HttpStatus
枚舉類表示,其中涵蓋了幾乎所有常用狀態碼,使用時可以直接翻看源碼。
ResponseEntity<T>
的基本使用流程如下,注意我們此時沒有使用@ResponseBody
(但是推薦直接使用@RestController
):
@Controller
public class StudentController {
@GetMapping("/student")
public ResponseEntity<Student> handle() {
// 創建返回實體:設置狀態碼、響應頭和響應數據
return ResponseEntity.ok().header("hName", "hValue").body(new Student("Xianhuii", 18));
}
}
當客戶端發送GET http://localhost:8080/student
請求時,上述代碼會返回如下結果:
HTTP/1.1 200
hName: hValue
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 04 May 2021 13:38:00 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"name": "Xianhuii",
"age": 18
}
最后,總結一下ResponseEntity<T>
的用法:
ResponseEntity<T>
直接用作方法返回值,表示將其作為HTTP響應:包括狀態碼、響應頭和響應體。ResponseEntity<T>
中包含status
、headers
和body
三個成員變量,共同組成HTTP響應。ResponseEntity
具有鏈式的靜態方法,可以很方便地構造實例對象。
4.3 @ExceptionHandler
上面介紹的都是正常返回的情況,在某些特殊情況下程序可能會拋出異常,因此不能正常返回。此時,就可以用@ExceptionHandler
來捕獲對應的異常,並且統一返回。
首先,我們自定義一個異常:
public class NoSuchStudentException extends RuntimeException {
public NoSuchStudentException(String message) {
super(message);
}
}
然后我們編寫相關Controller方法:
@RestController
public class StudentController {
@GetMapping("/student")
public ResponseEntity<Student> handle() {
throw new NoSuchStudentException("沒有找到該student");
}
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler
public String exception(NoSuchStudentException exception) {
return exception.getMessage();
}
}
此時發送GET http://localhost:8080/student
請求,會返回如下響應:
HTTP/1.1 404
Content-Type: text/plain;charset=UTF-8
Content-Length: 22
Date: Tue, 04 May 2021 14:09:51 GMT
Keep-Alive: timeout=60
Connection: keep-alive
沒有找到該student
上述執行流程如下:
- 接收
GET http://localhost:8080/student
請求,分發到handle()
方法。 handle()
方法執行過程中拋出NoSuchStudentException
異常。NoSuchStudentException
被相應的exception()
方法捕獲,然后根據@ResponseStatus
和錯誤消息返回給客戶端。
其實@ExceptionHandler
所標注的方法十分靈活,比如:
- 它的形參代表該方法所能捕獲的異常,作用與
@ExceptionHandler
的value
屬性相同。 - 它的返回值也十分靈活,既可以指定為上述的
@ResponseBody
或ResponseEntity<T>
等綁定到響應體中的值,也可以指定為Model
等視圖相關值。 - 由於當前考慮的是前后端分離場景,因此我們需要指定
@ResponseBody
,上面代碼已經聲明了@RestController
。 @ResponseStatus
不是必須的,我們可以自己構造出合適的響應對象。@ExceptionHandler
只能處理本類中的異常。
上面代碼中我們只針對NoSuchStudentException
進行處理,如果此類中還有其他異常,則需要另外編寫對應的異常處理方法。我們還有一種最佳實踐方式,即定義一個統一處理異常,然后在方法中進行細化處理:
@RestController
public class StudentController {
@GetMapping("/student")
public ResponseEntity<Student> handle() {
throw new NoSuchStudentException();
}
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler
public String exception(Exception exception) {
String message = "";
if (exception instanceof NoSuchStudentException) {
message = "沒有找到該student";
} else {
}
return message;
}
}
我們來看一下@ExceptionHandler
的源碼:
package org.springframework.web.bind.annotation;
@Target(ElementType.METHOD) // 只能標注在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
/**
* Exceptions handled by the annotated method. If empty, will default to any
* exceptions listed in the method argument list.
*/
Class<? extends Throwable>[] value() default {};
}
我們來看一下@ResponseStatus
的源碼:
package org.springframework.web.bind.annotation;
@Target({ElementType.TYPE, ElementType.METHOD}) // 可以標記在類(會被繼承)或方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseStatus {
/**
* Alias for {@link #code}. 狀態碼
*/
@AliasFor("code")
HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
/**
* The status <em>code</em> to use for the response. 狀態碼
*/
@AliasFor("value")
HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
/**
* The <em>reason</em> to be used for the response. 原因短語
*/
String reason() default "";
}
最后,總結一下@ExceptionHandler
的用法:
@ExceptionHandler
標記某方法為本Controller中對某些異常的處理方法。- 該方法的形參表示捕獲的異常,與
@ExceptionHandler
的value
屬性功能一致。 - 該方法的返回值多種多樣,在前后端分離情況下,需要與
@ResponseBody
結合使用。 - 結合
@ResponseStatus
方便地返回狀態碼和對應的原因短語。
4.4 @ControllerAdvice
上面介紹的@ExceptionHandler
有一個很明顯的局限性:它只能處理本類中的異常。
接下來我們來介紹一個十分強大的@ControllerAdvice
注解,使用它與@ExceptionHandler
相結合,能夠管理整個應用中的所有異常。
我們定義一個統一處理全局異常的類,使用@ControllerAdvice
標注。並將之前的異常處理方法移到此處(注意此時需要添加@ResponseBody
):
@ControllerAdvice
@ResponseBody
public class AppExceptionHandler {
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler
public String exception(Exception exception) {
return exception.getMessage();
}
}
將之前的Controller修改成如下:
@RestController
public class StudentController {
@GetMapping("/student")
public ResponseEntity<Student> handle() {
throw new NoSuchStudentException("沒有找到該student");
}
}
發送GET http://localhost:8080/student
請求,此時會由AppExceptionHanler
類中的exception()
方法進行捕獲:
HTTP/1.1 404
Content-Type: text/plain;charset=UTF-8
Content-Length: 22
Date: Tue, 04 May 2021 14:39:26 GMT
Keep-Alive: timeout=60
Connection: keep-alive
沒有找到該student
我們來看看@ControllerAdvice
的源碼:
package org.springframework.web.bind.annotation;
/**
* Specialization of {@link Component @Component} for classes that declare
* {@link ExceptionHandler @ExceptionHandler}, {@link InitBinder @InitBinder}, or
* {@link ModelAttribute @ModelAttribute} methods to be shared across
* multiple {@code @Controller} classes.
* 可以統一管理全局Controller類中的@ExceptionHandler、@InitBinder和@ModelAttribute方法。
*
* <p>By default, the methods in an {@code @ControllerAdvice} apply globally to
* all controllers. 默認情況下會管理應用中所有的controllers。
*
* Use selectors such as {@link #annotations},
* {@link #basePackageClasses}, and {@link #basePackages} (or its alias
* {@link #value}) to define a more narrow subset of targeted controllers.
* 使用annotations、basePackageClasses、basePackages和value屬性可以縮小管理范圍。
*
* If multiple selectors are declared, boolean {@code OR} logic is applied, meaning
* selected controllers should match at least one selector. Note that selector checks
* are performed at runtime, so adding many selectors may negatively impact
* performance and add complexity.
* 如果同時聲明上述多個屬性,那么會使用它們的並集。由於在運行期間檢查,所有聲明多個屬性可能會影響性能。
*/
@Target(ElementType.TYPE) // 只能標記到類上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 含有@Component元注解,因此可以被Spring掃描並管理
public @interface ControllerAdvice {
/**
* Alias for the {@link #basePackages} attribute. 同basePackages,管理controllers的掃描基礎包數組。
*/
@AliasFor("basePackages")
String[] value() default {};
/**
* Array of base packages. 管理controllers的掃描基礎包數組。
*/
@AliasFor("value")
String[] basePackages() default {};
/**
* 管理的Controllers所在的基礎包中必須包含其中一個類。
*/
Class<?>[] basePackageClasses() default {};
/**
* Array of classes. 管理的Controllers必須至少繼承其中一個類。
*/
Class<?>[] assignableTypes() default {};
/**
* Array of annotation types. 管理的Controllers必須至少標注有其中一個注解(如@RestController)
*/
Class<? extends Annotation>[] annotations() default {};
}
最后,我們總結@ControllerAdvice
的用法:
@ControllerAdvice
用來標注在類上,表示其中的@ExceptionHandler
等方法能進行全局管理。@ControllerAdvice
包含@Component
元注解,因此可以被Spring掃描並管理。- 可以使用
basePackages
、annotations
等屬性來縮小管理的Controller的范圍。
5 總結
在前后端分離項目中,Spring MVC管理着后端的Controller層,是前后端交互的接口。本文對Spring MVC中最常用、最基礎的注解的使用方法進行了系統介紹,使用這些常用注解,足以完成絕大部分的日常工作。
最后,我們對Spring MVC的使用流程做一個總結:
- 引入依賴:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 創建Controller類:
@Controller
或@RestController
注解。 - 指定分發地址:
@RequestMapping
以及各種@XxxMapping
注解。 - 接收請求參數:
@PathVariable
、@RequestParam
、@RequestHeader
、@CookieValue
、@RequestBody
、HttpEntity<T>
以及@RequestPart
和MultipartFile
。 - 發送響應數據:
@ResponseBody
、ResponseEntity<T>
以及@ExceptionHandler
和@ControllerAdvice
。