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
Spring之旅 文章被收录于专栏

1、Spring Framework 2、Spring Boot 3、Spring Cloud 4、穿插Spring其他有用的组件 学习很难,更新很慢,但是^-^ JUST DO IT!!!

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务