Spring中HTTP相关注解介绍

一段典型的后端 Controller 层的代码

@RestController
public class HTTPController {
    @GetMapping("get/params")
    public HashMap<String, Integer> queryData(@RequestParam Integer pageNo, @RequestParam Integer pageSize) {
        log.info("pageNo {}, pageSize {}", pageNo, pageSize);
        HashMap<String,Integer> info = new HashMap<>();
        info.put("pageNo",pageNo);
        info.put("pageSize", pageSize);
        return info;
    }
}

这段代码中涉及到了三个注解的使用 @RestController@GetMapping, @RequestParam,也分别对应了注解的三个使用层面,类相关注解,方法注解,以及方法参数注解。

这篇文章目的是希望能详细了解三种使用层面上的不同注解,搞清楚不同注解的适用场景。

# 类相关的注解

# @Controller

@Controller 注解官方的解释 (opens new window)是,它表示被注解的这个类是一个控制器类(比如声明可以返回 HTTP 响应的 Web 控制器)。

但是如果将类只用 @Controller 注释的话,访问方法对应的路径还是会报 404 ,这是为什么?因为只使用 @Conroller ,返回值默认是一个视图名称,Spring 会通过这个视图名称查找相关连的视图来呈现,这个视图配置过程需要单独进行,如果找不到对应视图的话,则会导致 404。

视图配置流程可以参考这里 (opens new window),这里不再赘述了。

现代的业务开发里前后端都是分离的,一般在和前端对接的时候,后端只需要返回前端需要的数据即可,具体的页面逻辑是前端自己完成的,所以我们想要返回数据应该怎么办呢?

答案是可以增加一个 @ResponseBody ,这个注解表示方法返回值应绑定到 Web 响应正文。比如下面这个方法里,最终只返回一个字符串:

// 控制器部分
@Controller
@ResponseBody
public class HTTPController {
    @GetMapping("get/pure")
    public String queryData() {
        return "query";
    }
}
// 网络请求
curl "http://localhost:8083/get/pure"
// 响应结果
query

# @RestController

@RestController 注解官方的解释 (opens new window)是,它是 @Controller@ResponseBody 的组合的一种简便写法。

像上面的例子中 将 @Contoller@ResponseBody 替换为 @RestController 之后,返回的结果并不会变。

@RestController 还有一个额外的好处是,如果你返回的是对象、字典或者数组,则返回值会自动变为 JSON 的格式,比如如下方法

@GetMapping("get/person")
public PersonResponse queryPerson() {
    PersonResponse personResponse = new PersonResponse();
    personResponse.setName("fanthus");
    personResponse.setId(1);
    return personResponse;
}

public class PersonResponse {
    private Integer id;
    private String name;
}

请求后得到的响应为👇
{"id":1,"name":"fanthus"}

# @Controller VS @RestController

@Controller 返回一个视图页面,在前后端不分离的年代,经常用到。

@RestController@Contoller@ResponseBody 的组合,它返回普通数据,并将数据写入到 HTTP 响应的 body 中,同时它对于复杂数据会进行序列化。

现在开发里,基本上所有正常返回数据的控制器,都需要增加 @RestController 这个注解。

# 方法相关注解

# @RequestMapping

@RequestMapping (opens new window) 表示将 Web 请求映射到请求处理类中对应方法。

@RequestMapping 也可以用在类级别上(类似 @Contorlller),不过大部分时候还是用在方法层级上。

那如何让一个方法表示成是 GET 方法以及指定请求对应路径呢?@RequestMapping 提供了一系列参数来指定这些 HTTP 请求参数。

参数列表有 method, name, value, path, headers, params, produces, consumes.

  1. method 标识对应的请求方法。最常见的请求方法就是 RequestMethod.GETRequestMethod.POST

  2. valuepath 都表示 URL 请求路径,这俩可以互相替代。value 是默认属性,可以省略不写。所以我们常见的这种写法 @RequestMapping("/pets") 其实是省略了参数名的。

    而且根据 @RequestMapping (opens new window) 的官方文档,可以给这两个参数提供数组,即同一个方法可以匹配多个路径。

  3. params 限制请求参数,比如这种写法 @RequestMapping(method = RequestMethod.GET, path = {"/"} , params = {"id=1"}) 就会限制请求带的参数 id 只能为 1,如果带别的参数,或者不带参数都会报 HTTP 400 请求错误。

  4. headers 限制请求头。

  5. consumes 显示请求内容类型,即请求头中的 Content-Type 值。

  6. produces 限制响应内容类型。

上面这些参数组合起来能控制请求的处理,举个例子

@RequestMapping(method = RequestMethod.GET, path = {"requestMapping/get", "requestMapping/get2"} , params = {"id=1"}, headers = "Content-Type=application/json")
public String reqestMapping() {
	return "request mapping";
}
// 上面这个方法只能通过 curl 命令来请求
- curl "http://localhost:8083/requestMapping/get?id=1" \
     -H 'Content-Type: application/json'
- curl "http://localhost:8083/requestMapping/get2?id=1" \
     -H 'Content-Type: application/json'

有一个name属性没有介绍,不过这个用的不多,经常用到的场景是用于文档生成。

# @GetMapping

@GetMapping (opens new window) 表示注释的方法对应的 HTTP 请求为 GET 请求。

本质上 @GetMapping@RequestMapping(method = RequestMethod.GET) 的缩写形式。所以 @GetMapping 也能复用 @RequestMapping 的参数列表。

# @PostMapping

@PostMapping (opens new window) 表示注释的方法对应的 HTTP 请求为 POST 请求。

本质上 @PostMapping@RequestMapping(method = RequestMethod.POST) 的缩写形式。具体使用方式可以参考 @GetMapping

# 参数相关注解

说完了方法上的注解,接下来聊聊方法参数内的注解,比较常用就是

# @RequestParam

@RequestParams (opens new window) 这个注解表示Web 请求参数绑定到方法参数。

在 Spring MVC 中,请求参数可能是 query parameters(查询参数)、form data(表单数据)和 multipart requests 中的一个,所以这个参数不光能用在 GET 请求,也能用在 POST 请求中。

这个注解也有自己的参数设置,如下

  • name。要绑定到的请求参数的名称,即如果真实方法参数名是 pageNo,你想要映射到的 HTTP 请求参数里的参数名是 pageStart,则可以用 name 属性。例子如下

    public String queryData(@RequestParam(name = "pageStart") Integer pageNo, @RequestParam Integer pageSize)
    上面这个方法对应的请求命令如下
    curl "http://localhost:8083/get/params?pageSize=200&pageStart=1"
    
  • required。表示这个参数是否是必传的,这个注解参数经常会用到,比如用户登录的时候,用户名和密码这些都是必填的,我们可以通过这个参数来对 HTTP 请求参数进行验证。

  • defaultValue。当未提供请求参数或请求参数为空值时用作后备的默认值,如果提供了这个值,则会将 required 参数设置为 false。

有的时候我们进行一些数据批量查询的时候,可能会传给服务器一个数组列表,比如 curl "http://localhost:8083/get/paramArray?params=200,300,number,400 (opens new window)" 这种命令,那应该怎么设计参数列表,才能接收这样的参数值呢?可以将参数设置为字符数组类型,如下

public String queryParamArray(@RequestParam List<String > params)

我觉得最好是设置为字符串数组,因为足够通用,如果确保传过来的就是数字,则 List<Integer> 类型也行,但如果不是的话,就可能报异常了。

有的时候请求参数非常多,我们希望能将这些请求参数封装成一个类,怎么做?直接封装即可。例子如下

// 参数封装成类
public class PageGetRequest {
    private Integer pageNo;
    private Integer pageSize;
}
// 直接将参数类型声明为对应类就 OK
@GetMapping("get/paramModel")
public String queryData(PageGetRequest requestGetModel) {
    log.info("pageNo {}, pageSize {}", requestGetModel.getPageNo(), requestGetModel.getPageSize());
    return "hello";
}

注意上面的方法参数前面并没有加 @RequestParam 注解,如果加上注解则会报错如下,目测是不支持的参数类型。

Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'requestGetModel' for method parameter type PageGetRequest is not present]

@RequestParam 有的时候并不是必须的,因为 Spring Boot 提供了自动参数解析的功能。Spring Boot 可以自动解析请求中的查询参数并将它们绑定到方法参数,如果HTTP 请求参数名和方法参数名保持一致的话,则无需显式使用 @RequestParam 注解。这个规则不仅适用于 GET 请求,也适用于 POST 的表单请求。不过如果你想要 @RequestParam 的别的参数,那还是要显式的去写注解。

# RequestBody

@RequestBody (opens new window) 表示方法参数应绑定到 HTTP 请求体 body。所以 @RequestBody 基本上不会用 GET 请求中。

@RequestBody (opens new window) 注解也有参数,不过比 @RequestParam 参数少,只有一个 required 参数,含义和 @RequestParam 中的 required 一样。

下面这个例子展示了使用 application/json 做请求类型的 POST 请求的执行情况

// curl 命令
curl -X "POST" "http://localhost:8083/post/json" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "name": "fanthus"
}'
// POST 请求
@PostMapping("post/json")
public String postJSON(@RequestBody String jsonStr) {
    log.info("content {}", jsonStr);
    return "hello";
}
// 请求后方法打印内容
content {"name":"fanthus"}

表单请求也类似,POST 请求方法不需要改变,只需要将请求命令替换为 POST 表单请求即可,但是如果参数类型不是字符串,而是对象的话,表单请求的参数不能使用 @RequestBody 去接。 否则报错 Unsupported Media Type..

POST 请求体通常都会映射到一个对象,如果我们按照上面的写法,我们需要自己去进行字符串转对象的操作,但我们通常不会这么做,我们希望在方法里直接拿到 POST 请求后,直接拿到对象实例,这时候只需要将 POST 请求体封装成对象形式,然后直接接收就行,例子如下

// 将请求体映射为对象
public class PagePostRequest {
    private String name;
}
// 参数类型替换
@PostMapping("post/jsonModel")
public String postJSONModel(@RequestBody **PagePostRequest** jsonRequest) {
    log.info("request {}, name {}", jsonRequest, jsonRequest.getName());
    return "hello";
}
// 请求内容不变,对应执行结果如下
request PagePostRequest(name=fanthus), name fanthus

# @RequestParam VS @RequestBody

这两个注解比较容易混淆,我刚接触的时候就有点晕,不过想要区分也很简单。

  • @RequestBody 基本上是用于 application/json 这种类型的 POST 请求的,因为它是将请求体绑定了方法参数。而且通常 @RequestBody 附带的参数是个对象,因为它类型直接映射请求体会比较复杂。
  • @RequestParam 是用户 GET 请求,附加表单和 multipart 的 POST 请求,不支持 application/json 类型的 POST 请求。

我们实现的时候如果按照 application/json 类型POST 请求用 @RequestBody ,其余请求用 @RequestParam 则一般就不会出错。

大家如果觉得这部分我说的有问题,可以在评论里指出来。

# @RequestHeader

@RequestHeader (opens new window) 是Spring MVC中的注解之一,用于从HTTP请求头中提取信息并将其绑定到方法参数。 通常用法如下

@GetMapping("get/requestHeader")
public String getRequestHeader(@RequestHeader HashMap<String, Object> header) {
    log.info("getRequestHeader {}",header);
    return "hello";
}
// 执行请求命令
curl "http://localhost:8083/get/requestHeader?pageSize=200&pageNo=1"
// 打印结果如下
getRequestHeader {host=localhost:8083, connection=close, user-agent=RapidAPI/4.2.0 (Macintosh; OS X/13.6.0) GCDHTTPRequest}

# @Valid

这个注解也经常用到,主要是校验对象参数,它需要单独引入一个库,如下

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

@Valid 主要是配合一些对象内部的属性修饰符使用,比如下面这个例子

// 请求内容
public class ValidRequest {
    @NotNull
    private String name;
    @AssertFalse
    private  boolean hasVote;
    @Min(1)
    private  Integer voteNumber;
    @NotBlank(message="原因不能为空")
    private  String reason;
}
// 获取数据
@GetMapping("get/valid")
public String getValid(@Valid ValidRequest requst) {
    log.info("request {}", requst);
    return "hello";
}
// 符合条件的请求
curl "http://localhost:8083/get/valid?name=fanthus&hasVote=false&voteNumber=2&reason=hello"
// 执行结果
request ValidRequest(name=fanthus, hasVote=false, voteNumber=2, reason=hello)

注意的是 @Valid 并不限制 GET 或者 POST 请求。上面的例子是 GET 请求,如果是 POST 请求的话,则方法如下

@PostMapping("post/valid")
public String postValid(@RequestBody @Valid ValidRequest requst)

注意 @Valid 不要和 @Validated 搞混。

如果因为一些原因不能使用 @RequestParam,则使用 @Valid+@NotNull 组合可以替代 @RequestParam 中的 required = true 的设置。

# End

以上就是我总结的一些 Spring 中 HTTP 相关注解的使用方法,只是从使用层面梳理了下,并没有深入到原理层,不过对于初学者应该够用了。

参考地址:

  1. @RestController vs @Controller (opens new window)
  2. What is difference between @RequestBody and @RequestParam? (opens new window)
  3. What does the @Valid annotation indicate in Spring? (opens new window)
  4. SpringBoot 中使用 @Valid 注解 + Exception 全局处理器优雅处理参数验证 (opens new window)

关注我的微信公众号,我在上面会分享我的日常所思所想。