图源:简书 (jianshu.com)
虽然在之前的系列文章中已经在示例中演示了怎么接收请求,但那些示例都过于简单,在实际开发中往往会遇到各种各样接收请求和处理参数的问题,所以有必要这里专门讨论一下。
同样的,这里将使用从零开始 Spring Boot 11:返回数据 - 魔芋红茶’s blog (icexmoon.cn)中最终代码作为基础代码,在这之上演示如何接收各种类型的请求和参数。
在开始之前需要说明的是,实际上浏览器并不支持完整的HTTP1.1协议,HTTP1.1协议中定义了GET/POST/DELETE/PUT四种常见HTTP Method以及其它几种不常见HTTP Method,但主流的浏览器都没有对其进行完整支持,要么是不支持DELETE和PUT方法,要么是某些方法不支持使用报文体传递参数,但这并非是HTTP协议本身的问题,只是浏览器作为一种最常见的HTTP客户端其本身的局限性。
所以,如果是基于浏览器的Web开发,那最好仅使用GET和POST两种请求,如果是基于完整实现HTTP1.1的HTTP客户端,比如移动应用或者服务器,可以使用全部的HTTP 1.1定义的HTTP Method并使用Restful风格定义接口,这点在之后会说明。
首先,我们来看如何接收和处理GET和POST的浏览器请求,这是工作中绝大多数情况下要处理的问题。
在使用GET请求的时候我们可以通过几种不同的方式来传递参数,先看一种最简单的方式:
在BookController
中添加以下代码:
@Data
private static class GetBookInfoVO implements IResult {
@ApiModelProperty("图书id")
private Integer id;
@ApiModelProperty("图书名称")
private String name;
@ApiModelProperty("图书介绍")
private String desc;
@ApiModelProperty("添加人id")
private Integer uid;
public static GetBookInfoVO newInstance(Book book){
GetBookInfoVO vo = new GetBookInfoVO();
vo.setId(book.getId());
vo.setName(book.getName());
vo.setDesc(book.getDescription());
vo.setUid(book.getUserId());
return vo;
}
}
@ApiOperation("获取书籍详情")
@GetMapping("/book/detail/{id}")
public GetBookInfoVO getBookInfo(@PathVariable Integer id) {
Book book = bookService.getById(id);
if (book == null){
throw new ResultException(Result.ErrorCode.PARAM_CHECK,String.format("%d不是一个有效的书籍id",id));
}
return GetBookInfoVO.newInstance(book);
}
这里getBookInfo
方法负责接收GET请求/book/detail/{id}
并返回数据,这里{id}
是一个路径参数,所谓的路径参数就是直接拼接在URL路径中的参数。相应的,使用@PathVariable
注解就可以将相应的路径参数的值绑定到方法形参中。
GetBookInfoVO
是一个方法对应的VO(view object,视图对象),对于简单应用我们并不需要定义VO,可以直接将Entity中定义的类型返回,但对于复杂一些的应用,最好定义VO以降低Controller层和DAO的耦合。
关于MVC中常用的几种对象VO\BO\DTO等,有机会会专门进行说明。
现在如果使用HTTP调试工具请求http://localhost:8080/book/detail/1
,就可以看到下面这样的返回信息:
{
"success": true,
"msg": "",
"data": {
"id": 1,
"name": "test",
"desc": "sdfdsfdsfsdf",
"uid": 1
},
"code": "SUCCESS"
}
books应用是要求登录的,所以不要忘记登录并使用
token
作为报文头以通过登录验证。
实际上路径参数是Restful风格API所提倡使用的一种传参形式,可以用它很明确地定义资源访问,比如:
但就像前文说的,因为浏览器支持的缘故,在一般的基于浏览器的Web开发中,并不好使用标准的Restful风格API,所以可能更多的是仅使用GET和POST构建的一般形式的API接口。
下面看GET请求更一般的传参形式:
在同一个Controller下添加以下代码:
@ApiOperation("查询书籍列表")
@GetMapping("/book/list/query")
public List<Book> getQueryBooks(@RequestParam String name, @RequestParam String desc) {
List<Book> books = new IResultArrayList<>();
QueryWrapper<Book> qw = new QueryWrapper<>();
if (!ObjectUtils.isEmpty(name)) {
qw.like("name", name);
}
if (!ObjectUtils.isEmpty(desc)) {
qw.like("description", desc);
}
List<Book> findBooks;
if (!qw.isEmptyOfWhere()) {
findBooks = bookService.list(qw);
} else {
findBooks = bookService.list();
}
books.addAll(findBooks);
return books;
}
这里使用@RequestParam
注解来将查询字符串中的参数值绑定到形参,然后就可以用下面的方式来请求:
http://localhost:8080/book/list/query?name=test&desc=df
这里URL中?=
后的部分就是查询字符串,以&
分隔的键值对形式来组织需要传递的参数。
一切顺利的话就能看到类似于下面的请求结果:
{
"success": true,
"msg": "",
"data": [
{
"id": 1,
"name": "test",
"description": "sdfdsfdsfsdf",
"userId": 1
}
],
"code": "SUCCESS"
}
需要注意的是,默认情况下@RequestParam
注解绑定的查询参数是”必传参数“,也就是说如果请求URL中缺少相应的参数就会报错,比如请求:
http://localhost:8080/book/list/query
会看到类似这样的报错信息:
{
"timestamp": "2022-07-24T04:40:35.048+00:00",
"status": 400,
"error": "Bad Request",
"trace": "org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'name' ..."
...
}
虽然可以通过传递空字符串的方式来解决这个问题:
http://localhost:8080/book/list/query?name=&desc=
但是这样还是对前端开发不太友好,更好的做法是通过@RequestParam
注解的属性将其设置为”非必传参数“:
@ApiOperation("查询书籍列表")
@GetMapping("/book/list/query")
public List<Book> getQueryBooks(@RequestParam(required = false) String name, @RequestParam(required = false) String desc) {
...
}
现在再请求缺少相应参数的URL就不会报错了。
还值得一提的是,如果请求参数中包含ASC字符集以外的字符,会被HTTP client转义后传递。比如
http://localhost:8080/book/list/query?name=海底
会被转义为:
http://localhost:8080/book/list/query?name=%E6%B5%B7%E5%BA%95
实际上这种转义是HTTP协议为了不让URL中出现的特殊字符影响到URL解析所规定的。这种方式也不会影响到正常接收中文等特殊字符的参数。
POST请求最常见的是以表单方式提交信息。
添加以下代码:
@PostMapping("/book/form/add")
@ApiOperation("添加书籍(使用表单)")
public Result addBookByForm(@RequestParam String name, @RequestParam String desc){
//添加图书
Subject subject = SecurityUtils.getSubject();
String uname = (String) subject.getPrincipal();
User user = userService.getUserByName(uname);
Book book = new Book();
book.setUserId(user.getId());
book.setName(name);
book.setDescription(desc);
bookService.save(book);
return Result.success();
}
然后就可以使用HTTP client以表单的方式提交信息:
需要说明的是表单提交具体有两种方式:
这两种方式有一些细微差别,但都可以通过@RequestParam
注解获取值,这里不展开说明。
除了表单提交,直接将JSON格式的字符串作为请求报文体传输也是很常见的方式,这种方式被广泛用于前后端分离的Web应用开发中。
事实上books
中已经有这样的请求:
@RequiresRoles("manager")
@PostMapping("/book/add")
@ApiOperation("添加书籍")
public Result addBook(@RequestBody Book book) {
//添加图书
Subject subject = SecurityUtils.getSubject();
String name = (String) subject.getPrincipal();
User user = userService.getUserByName(name);
book.setUserId(user.getId());
bookService.save(book);
return Result.success();
}
不过这个请求直接使用DAO
类型book
来接受参数,更具解耦的做法是使用一个DTO
(Data Traslate Object,数据传输对象)来接收参数:
@Data
private static class AddBookDTO {
@ApiModelProperty("书籍名称")
private String name;
@ApiModelProperty("书籍说明")
private String desc;
}
@RequiresRoles("manager")
@PostMapping("/book/add")
@ApiOperation("添加书籍")
public Result addBook(@RequestBody AddBookDTO dto) {
//添加图书
Subject subject = SecurityUtils.getSubject();
String name = (String) subject.getPrincipal();
User user = userService.getUserByName(name);
Book book = new Book();
book.setUserId(user.getId());
book.setName(dto.getName());
book.setDescription(dto.getDesc());
bookService.save(book);
return Result.success();
}
同样的,可以用HTTP Client来模拟请求进行测试:
需要注意的是,此时请求报文头
content-type
是application/json
。
最后要说明的是,除了通过以上的方式传递参数,POST可以同时使用路径参数和查询字符串传递参数,甚至是同时使用这些方式,但不推荐这样做,因为会让使用接口的前端人员迷惑。
在接收和处理参数的时候,需要特别注意包装类和基本类型的差别。
假设在查询书籍列表的时候我们需要利用添加书籍的用户进行筛选,为其添加一个查询参数:
@ApiOperation("查询书籍列表")
@GetMapping("/book/list/query")
public List<Book> getQueryBooks(@RequestParam(required = false) String name, @RequestParam(required = false) String desc, @RequestParam(required = false) int userId) {
List<Book> books = new IResultArrayList<>();
QueryWrapper<Book> qw = new QueryWrapper<>();
if (!ObjectUtils.isEmpty(name)) {
qw.like("name", name);
}
if (!ObjectUtils.isEmpty(desc)) {
qw.like("description", desc);
}
if (userId>0){
qw.eq("user_id", userId);
}
List<Book> findBooks;
if (!qw.isEmptyOfWhere()) {
findBooks = bookService.list(qw);
} else {
findBooks = bookService.list();
}
books.addAll(findBooks);
return books;
}
看着似乎没有什么问题,请求http://localhost:8080/book/list/query?userId=1
也可以看到返回结果。
但如果不传递userId
:http://localhost:8080/book/list/query
就会看到下面的报错信息:
{
"timestamp": "2022-07-24T04:54:44.487+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "java.lang.IllegalStateException: Optional int parameter 'userId' is present but cannot be translated into a null value due to being declared as a primitive type..."
}
这是因为当Spring Boot检查到请求不带userId
参数时,就会试图使用null
来给形参userId
赋值,但显然试图将int
类型的变量赋值成null
会产生异常。
所以更合适的做法是使用包装类而非基础类型来接受参数:
@ApiOperation("查询书籍列表")
@GetMapping("/book/list/query")
public List<Book> getQueryBooks(@RequestParam(required = false) String name, @RequestParam(required = false) String desc, @RequestParam(required = false) Integer userId) {
...
if (userId!=null && userId>0){
qw.eq("user_id", userId);
}
...
}
需要注意的是,使用包装类后就需要在必要的时候检查是否为null
。
比较不同的是,如果是接收POST方式传递的JSON格式的信息,就不会有类似的问题:
@Data
private static class GetQueryBooksByPostJSONDTO {
@ApiModelProperty("书籍名称")
private String name;
@ApiModelProperty("书籍介绍")
private String desc;
@ApiModelProperty("添加书籍人id")
private int userId;
}
@ApiOperation("查询书籍列表(POST+JSON方式)")
@PostMapping("/book/list/query")
public List<Book> getQueryBooksByPostJSON(@RequestBody GetQueryBooksByPostJSONDTO dto) {
List<Book> books = new IResultArrayList<>();
QueryWrapper<Book> qw = new QueryWrapper<>();
String name = dto.getName();
if (!ObjectUtils.isEmpty(name)) {
qw.like("name", name);
}
String desc = dto.getDesc();
if (!ObjectUtils.isEmpty(desc)) {
qw.like("description", desc);
}
int userId = dto.getUserId();
if (userId > 0) {
qw.eq("user_id", userId);
}
List<Book> findBooks;
if (!qw.isEmptyOfWhere()) {
findBooks = bookService.list(qw);
} else {
findBooks = bookService.list();
}
books.addAll(findBooks);
return books;
}
如果不传递任何参数进行请求:
此时int
类型的dto.userId
就会被初始化为0
,而不是像使用@RequestParam
时候那样报错。
需要注意的是,即使什么参数都不传,也要传递一个
{}
,表明这是一个JSON化的空对象,否则服务端会产生一个解析错误。
虽然上面的方式不会产生错误,但是依然不推荐在接收参数时候使用基础类型,因为像上面这样,当你得到一个值为0
的dto.userId
的时候,你无法判断是客户端传递了一个{userId:0}
这样的请求还是{}
。
所以更为推荐的做法依然是使用包装类而非基础类型:
@Data
private static class GetQueryBooksByPostJSONDTO {
...
@ApiModelProperty("添加书籍人id")
private Integer userId;
}
@ApiOperation("查询书籍列表(POST+JSON方式)")
@PostMapping("/book/list/query")
public List<Book> getQueryBooksByPostJSON(@RequestBody GetQueryBooksByPostJSONDTO dto) {
...
Integer userId = dto.getUserId();
if (userId != null && userId > 0) {
qw.eq("user_id", userId);
}
...
}
Restful是一种接口设计风格,它的关键在于通过用URL描述资源、用HTTP Method描述动作来让接口用途变得一目了然。
可以通过RESTful API 一种流行的 API 设计风格进一步了解Restful接口。
上边已经列举过一个Restful风格接口的示例:
可以看到这样定义的接口相当简洁。
但Restful接口在使用中会遇到之前说过的问题,即因为浏览器对HTTP协议支持的不完整,导致在某些HTTP Method下无法通过请求体传输参数。
如果的确是基于浏览器而非完整功能的HTTP Client进行开发,可以使用一种变通方式实现Restful接口,比如:
即使用POST来代替所有的HTTP Method,并且在URL中添加描述来实现Restful的语义。
需要牢记,Restful并非具体的技术,所以不需要教条地严格实现。
最近工作比较忙,没太多时间水博客,所以就到这里了。
本篇文章的所有代码可以从learn_spring_boot (github.com)获取。
谢谢阅读。