SpringMVC常用注解(前后端分離)


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類的beanNamestudentController

  • @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類的beanNamestudentController

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的屬性有很多,但是常用的只有valuepathmethod。其中valuepath等價,用來指定分發地址。method則用來指定對應的HTTP請求方式。

1、valuepath

對於valuepath屬性,它們的功能其實我們之前就見到過了:指定相對於服務器套接字的分發地址。要小心的是在類上是否標注了@RequestMapping

如果@RequestMapping不顯式指定屬性名,那么默認是value屬性:

@RequestMapping("student")

當然我們也可以顯式指定屬性名:

@RequestMapping(value = "student")
@RequestMapping(path = "student")

需要注意的是valuepath屬性的類型是String[],這表示它們可以同時指定多個分發地址,即一個方法可以同時處理多個請求。如果我們指定了兩個分發地址:

@RestController
public class StudentController {
    @RequestMapping(path = {"student", "/getStudent"})
    public Student getStudent() {
        // 簡單模擬獲取student流程
        return new Student("Xianhuii", 18);
    }
}

此時,無論瀏覽器發送GET http://localhost:8080/studentGET 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
}

我們對valuepath屬性做一個小結:

  • 在不顯式聲明屬性名的時候,默認為value屬性,如@RequestMapping("/student")等價於@RequestMapping(value = "/student")
  • 在聲明多個@RequestMapping的屬性時,必須顯式指出value屬性名,如@RequestMapping(value = "student", method = RequestMethod.GET)
  • valuepath等價,如@RequestMapping(value = "/student")等價於@RequestMapping(path = "/student")
  • valuepath屬性的類型是String[],一般至少為其指定一個值。在指定多個值的情況下,需要用{}將值包裹,如@RequestMapping({"/student", "/getStudent"}),此時表示該方法可以處理的所有分發地址。
  • 需要注意類上是否標注了@RequestMapping,如果標注則為分發地址的基礎,具體方法的實際分發地址需要與之進行拼接。
  • 此外,在某些情況下,@RequestMapping的作用不是指定分發地址,可以不指定該屬性值。

2、method

method屬性用來指定映射的HTTP請求方法,包括GETPOSTHEADOPTIONSPUTPATCHDELETETRACE,分別對應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()方法可以支持GETPOST兩種HTTP請求方法。因此,發送GET http://localhost:8080/studentPOST http://localhost:8080/student都能得到正確的響應。但是若發送其他HTTP請求方法,如PUT http://localhost:8080/student,則同樣會返回上述405響應。

除了指定method屬性值的個數,其標注位置也十分重要。如果在類上@RequestMappingmethod屬性中指定了某些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/studentPOST 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請求方法。
  • 類上@RequestMappingmethod屬性所指定的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}的路徑變量。
  • 如果沒有顯式指定valuename屬性,則形參名必須與對應的路徑變量名一致。
  • 路徑變量中可以使用{pathVariableName:regex}方式指明匹配規則。

3 接收請求數據

我們可以直接在Controller的方法的形參中使用特定的注解,來接收HTTP請求中特定的數據,包括請求參數、請求頭、請求體和cookie等。

也可以直接聲明特定的形參,從而可以獲取框架中用於與客戶端交互的特殊對象,包括HttpServletRequestHttpServletResponse等。

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;
    }
}

上述過程沒有顯式指定@RequestParamvaluename屬性,因此形參名必須與請求參數名一一對應。如果我們顯式指定了valuename屬性,那么形參名就可以任意了:

@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>作為形參,那么會將所有請求參數納入該集合中,並且此時對valuename屬性沒有要求:

@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請求參數值。
  • 如果形參為基本類型,可以獲取對應的請求參數值。此時需要注意請求參數名是否需要與形參名一致(是否指定valuename屬性)。
  • 如果形參為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-EncodingKeep-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
    }
}

理論上,我們也可以不顯式指定@RequestHeadervaluename屬性值,而使用對應的形參名。但是由於HTTP請求頭中一般含有-,而Java不支持此種命名方式,因此推薦還是顯式指定valuename屬性值。

另外,我們也可以使用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請求頭,一般推薦使用valuename顯式指定請求頭名。
  • 也可以使用Map<String, String>MultiValueMap<String, String>一次性獲取所有請求頭,但是從該集合中獲取對應值時要注意其key值的大小寫形式,如accept-encoding
  • 我們也可以使用requireddefaultValue對是否必須具備該請求頭進行特殊處理。

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
    }
}

同樣,我們也可以不顯式指定valuename屬性值,此時形參名應與需要獲取的cookie鍵值對的key一致:

@RestController
public class StudentController {

    @GetMapping("/cookie")
    public void handle(@CookieValue String cookie1) {
        System.out.println(cookie1);	// value1
    }
}

需要注意的是,默認情況下不能同之前的@RequestParam@RequestHeader那樣使用MapMultiValueMap來一次性獲取所有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鍵相對應(是否指定了requireddefaultValue屬性)。
  • 注意:不能使用MapMultiValueMap一次性獲取所有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屬性。如果requiredtrue,表示請求體中必須包含對應數據,否則會拋異常。如果requiredfalse,表示請求體中可以沒有對應數據,此時形參值為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對象中值包含headersbody兩個成員變量,分別代表請求頭和請求體,對應為HttpHeaders和泛型T類型。我們可以調用HttpEntitygetHeaders()getBody()方法分別獲取到它們的數據。

另外,HttpHeaders類中只有一個Map屬性:final MultiValueMap<String, String> headers,為各種請求頭的集合。我們可以對其進行集合相關操作,獲取到需要的請求頭。

3.6 @RequestPartMultipartFile

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形參,分別用來接收表單中定義的兩個文件。

注意到此時形參名與表單中標簽名一致,所以其實這里也可以不顯式指出@RequestPartvaluename屬性(但是不一致時必須顯式指出):

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);
    }
}

最后,我們@RequestPartMultipartFile接口做一個總結:

  • @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響應,表示接收到studentjson數據:

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>的子類,它除了擁有父類中的headersbody成員變量,自己還新增了一個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>中包含statusheadersbody三個成員變量,共同組成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

上述執行流程如下:

  1. 接收GET http://localhost:8080/student請求,分發到handle()方法。
  2. handle()方法執行過程中拋出NoSuchStudentException異常。
  3. NoSuchStudentException被相應的exception()方法捕獲,然后根據@ResponseStatus和錯誤消息返回給客戶端。

其實@ExceptionHandler所標注的方法十分靈活,比如:

  • 它的形參代表該方法所能捕獲的異常,作用與@ExceptionHandlervalue屬性相同。
  • 它的返回值也十分靈活,既可以指定為上述的@ResponseBodyResponseEntity<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中對某些異常的處理方法。
  • 該方法的形參表示捕獲的異常,與@ExceptionHandlervalue屬性功能一致。
  • 該方法的返回值多種多樣,在前后端分離情況下,需要與@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掃描並管理。
  • 可以使用basePackagesannotations等屬性來縮小管理的Controller的范圍。

5 總結

在前后端分離項目中,Spring MVC管理着后端的Controller層,是前后端交互的接口。本文對Spring MVC中最常用、最基礎的注解的使用方法進行了系統介紹,使用這些常用注解,足以完成絕大部分的日常工作。

最后,我們對Spring MVC的使用流程做一個總結:

  1. 引入依賴:
<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>
  1. 創建Controller類:@Controller@RestController注解。
  2. 指定分發地址:@RequestMapping以及各種@XxxMapping注解。
  3. 接收請求參數:@PathVariable@RequestParam@RequestHeader@CookieValue@RequestBodyHttpEntity<T>以及@RequestPartMultipartFile
  4. 發送響應數據:@ResponseBodyResponseEntity<T>以及@ExceptionHandler@ControllerAdvice


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM