从零开始 Spring Boot 12:接收请求

从零开始 Spring Boot 12:接收请求

spring boot

图源:简书 (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

在使用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所提倡使用的一种传参形式,可以用它很明确地定义资源访问,比如:

  • DELETE /book/{id} 表示删除书籍
  • GET /book/{id} 表示获取书籍详情
  • POST /book 表示添加书籍
  • PUT /book/{id} 表示修改书籍信息

但就像前文说的,因为浏览器支持的缘故,在一般的基于浏览器的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

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以表单的方式提交信息:

从零开始 Spring Boot 12:接收请求_第1张图片

需要说明的是表单提交具体有两种方式:

  • multipart/form-data
  • application/x-www-form-urlencoded

这两种方式有一些细微差别,但都可以通过@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来模拟请求进行测试:

从零开始 Spring Boot 12:接收请求_第2张图片

需要注意的是,此时请求报文头content-typeapplication/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也可以看到返回结果。

但如果不传递userIdhttp://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;
    }

如果不传递任何参数进行请求:

从零开始 Spring Boot 12:接收请求_第3张图片

此时int类型的dto.userId就会被初始化为0,而不是像使用@RequestParam时候那样报错。

需要注意的是,即使什么参数都不传,也要传递一个{},表明这是一个JSON化的空对象,否则服务端会产生一个解析错误。

虽然上面的方式不会产生错误,但是依然不推荐在接收参数时候使用基础类型,因为像上面这样,当你得到一个值为0dto.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 接口

Restful是一种接口设计风格,它的关键在于通过用URL描述资源、用HTTP Method描述动作来让接口用途变得一目了然。

可以通过RESTful API 一种流行的 API 设计风格进一步了解Restful接口。

上边已经列举过一个Restful风格接口的示例:

  • DELETE /book/{id} 表示删除书籍
  • GET /book/{id} 表示获取书籍详情
  • POST /book 表示添加书籍
  • PUT /book/{id} 表示修改书籍信息

可以看到这样定义的接口相当简洁。

但Restful接口在使用中会遇到之前说过的问题,即因为浏览器对HTTP协议支持的不完整,导致在某些HTTP Method下无法通过请求体传输参数。

如果的确是基于浏览器而非完整功能的HTTP Client进行开发,可以使用一种变通方式实现Restful接口,比如:

  • POST /delete/book/{id} 表示删除书籍
  • POST /get/book/{id} 表示获取书籍详情
  • POST /post/book 表示添加书籍
  • POST /put/book/{id} 表示修改书籍信息

即使用POST来代替所有的HTTP Method,并且在URL中添加描述来实现Restful的语义。

需要牢记,Restful并非具体的技术,所以不需要教条地严格实现。

最近工作比较忙,没太多时间水博客,所以就到这里了。

本篇文章的所有代码可以从learn_spring_boot (github.com)获取。

谢谢阅读。

你可能感兴趣的:(JAVA,spring,boot,java,restful)