spring全集 - - - 第五章RESTful开发

2.1 RESTful 风格

RESTful 是一种软件架构风格,它采用了以上提到的要素来构建网络应用程序

  • 设计要访问的资源【名词】,用统一的 URI 表示

  • 选择资源的展现的方式:一般是 json 格式,也可以是其它格式

  • 用 HTTP Method 【动词】来转换资源状态

例如有一本《神雕侠侣》这个资源,它有唯一 id=10 ,这样设计

获取资源,Accept 一般用来指示我想要的格式

GET /api/books/10 HTTP/1.1
Accept: application/json;charset=utf-8

服务器返回,Content-Type 一般用来指示我提供的格式

HTTP/1.1 200 OK 

Content-Type: application/json;charset=utf-8
{
    "id":10,
    "title":"神雕侠侣",
    "author":"金庸"
}

 

如果想换一种格式,注意资源 URI 不用变!

GET /api/books/10 HTTP/1.1
Accept: application/xml;charset=utf-8 

 服务器返回

HTTP/1.1 200 OK 
Content-Type: application/xml;charset=utf-8

<book>
    <id>10</id>
    <title>10</title>
    <author>金庸</author>
</book>

 更新资源,注意资源 URI 不用变!

PUT /api/books/10 HTTP/1.1
Content-Type: application/json;charset=utf-8

{
    "title":"神雕侠侣",
    "author":"查良镛"
}

 

删除资源,注意资源 URI 不用变!  

DELETE /api/books/10 HTTP/1.1 

新增资源,可以这么干:如果希望服务器生成 id,则用 post,否则用 put  

POST /api/books HTTP/1.1
Content-Type: application/json;charset=utf-8

{
	"title":"侠客行",
	"author":"金庸"
}

 

注意

  • RESTful 是一种风格,并没有什么规范强制你【必须】怎么做,所以常常可以看到一些与 RESTful 理念不符的实例,因开发者而不同,此为正常现象

2.2 开发 RESTful 应用

⭐️1) 设计统一 URI

要实现【设计要访问的资源【名词】,用统一的 URI 表示】这一特性,我们发现 RESTful 风格中,唯一标识 id,并不是像之前一样从请求参数(即 ? 后)传递过来,而是此 id 就是路径的组成部分,因此需要方便的办法获取路径中的参数。

Spring 提供了 @PathVariable 来解析资源路径中的参数信息

例1:当路径参数名与请求参数名一致时

@RestController
public class Controller01 {

    private static final Logger log = LoggerFactory.getLogger(Controller01.class);

    @RequestMapping("/test1/{id}")
    public String test1(@PathVariable int id) {
        log.debug("编号:{}", id);
        return "test";
    }
}

 其中 {参数名} 代表路径中变化的部分,比如这里的 {id} 就代表设置了一个 id 参数,可以用来接收

  • /test1/100

  • /test1/101 等路径中的 100、101 这些编号值,SpringMVC 会检查标注了 @PathVariable 的方法参数,将这些值传递给同名参数

测试

http://localhost:8080/test1/100

服务器控制台输出  

[DEBUG] 08:36:43.731 [http-bio-8080-exec-8] c.i.c.c.Controller01 - 编号:100 

 注意 RESTful 的路径参数作用与普通 ?参数名=参数值 没有两样,都是用来获取请求中的信息。区别仅在于风格格式不同,一种是将信息包含在路径里,一种是将信息跟在 ? 号后

例2:当路径参数名与请求参数名不一致时  

@RequestMapping("/test2/{sid}")
public String test2(@PathVariable("sid") int id) {
	log.debug("编号:{}", id);
	return "test";
}

测试

http://localhost:8080/test2/200

服务器输出

 [DEBUG] 08:37:47.302 [http-bio-8080-exec-1] c.i.c.c.Controller01 - 编号:200 

例3:路径参数可以有多个

@RequestMapping("/test3/{id}/{name}")
public String test3(@PathVariable int id, @PathVariable String name) {
	log.debug("编号:{} 用户名:{}", id, name);
	return "test";
}

 测试

http://localhost:8080/test3/300/zhangsan

服务器输出

 [DEBUG] 08:38:33.937 [http-bio-8080-exec-1] c.i.c.c.Controller01 - 编号:300 用户名:zhangsan 

注意 当参数比较多时,仍然建议把信息放在请求体中而不是路径里,路径里一般只放关键信息  

2) 区分请求方法

要实现【用 HTTP Method 【动词】来转换资源状态】就需要更方便的办法按 请求方法(GET POST 等)来区分请求,访问资源时,增删改查,路径都是同一个(增可能会有不同),这时就需要对路径加以区分,否则就会有歧义:

@RestController
public class Controller02 {
    private static final Logger log = LoggerFactory.getLogger(Controller02.class);

    @RequestMapping("/users/{id}")
    public void get(@PathVariable Integer id) {
        log.debug("get...");
    }

    @RequestMapping("/users/{id}") // 当然新增一般不需指定 id
    public void post(@PathVariable Integer id) {
        log.debug("post...");
    }
}

 启动时会报错

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'controller02' method 
com.itheima.controller.case05.Controller02#post(Integer)
to { /user/{id}}: There is already 'controller02' bean method com.itheima.controller.case05.Controller02#get(Integer) mapped.

显然必须加以区分,怎么区分呢?可以通过 Request 的不同 Method 来区分。其实想想也是正常,RESTful 不就是建议用 Request Method 来区分对资源的增删改查嘛

代码改成下面这样就可以了

@RestController
public class Controller02 {
    private static final Logger log = LoggerFactory.getLogger(Controller02.class);

    @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
    public void get(@PathVariable Integer id) {
        log.debug("get...");
    }

    @RequestMapping(value = "/users/{id}", method = RequestMethod.POST)
    public void post(@PathVariable Integer id) {
        log.debug("post...");
    }
}

但显然有些繁琐,这里 SpringMVC 为我们提供了几个简化(衍生)注解,相信一看就明白

@RestController
public class Controller02 {
    private static final Logger log = LoggerFactory.getLogger(Controller02.class);

    @GetMapping("/users/{id}")
    public void get(@PathVariable Integer id) {
        log.debug("get...");
    }

    @PostMapping("/users/{id}") 
    public void post(@PathVariable Integer id) {
        log.debug("post...");
    }

    @DeleteMapping("/users/{id}")
    public void delete(@PathVariable Integer id) {
        log.debug("delete...");
    }

    @PutMapping("/users/{id}")
    public void put(@PathVariable Integer id) {
        log.debug("put...");
    }
}

顺便把 @DeleteMapping 和 @PutMapping 也加上了

不过又有了新的问题:浏览器表单可以发送 POST、GET 请求,但它不支持 PUT、DELETE 请求,虽然有方法可以让表单通过 POST 模拟后两种,但属于偏门知识,这里就不讲了,正规的、常见方法有三种:

  • ajax 可以支持各种请求的发送,但属于前台知识,不是重点

  • Spring 提供了 RestTemplate 发送各种请求,底层就是用 java 代码发送 http 请求

  • 第三方工具发送请求,底层也都是 http

    • 图形界面的有 postman

    • 命令行界面的有 curl

​3) @RequestBody

客户端的参数比较多,比较复杂时,常会在请求体中传递过来 json 字符串,而 controller 这边可以用 @RequestBody 将此 json 字符串转换为 java 对象

客户端数据

 {"name":"张三", "age":18}

根据参数设计一个用来接收的 java 类  

static class Student {
	String name;
	Integer age;

	// 省略 get set 方法
}

方法参数用 @RequestBody 标注

@RestController
public class Controller03 {

    @PostMapping("/students")
    public void post(@RequestBody Student student) {
        log.debug("{}", student);
    }
	
	// ...
}

如果客户端数据更复杂一些,一样没问题

[
	{"name":"张三", "age":18},
	{"name":"李四", "age":20}
]

控制器方法

@RestController
public class Controller03 {

    @PutMapping("/students")
    public void put(@RequestBody List<Student> students) {
        for (Student student : students) {
            log.debug("{}", student);
        }
    }
	
	// ...
}

4) 请求参数校验

请求参数接收完,还有一步重要操作,那就是校验。数据不校验,就可能导致非法、残缺数据入库,严重还可能引起系统漏洞

步骤

  1. 添加依赖 - 代码片段1

  2. 对象属性上添加校验规则 - 代码片段2

  3. 控制器方法改动 - 代码片段3

代码片段1(pom.xml)

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

代码片段2

static class Student {
    @NotEmpty
    String name;
    @Min(18)
    Integer age;

    // ...
}

校验规则有不同注解进行控制,代码中的

  • @NotEmpty 表示该参数不能为 null,也不能为 ''

  • @Min 用来验证参数是整数,并且最小值不能小于 18

代码片段3

@RestController
public class ValidateController {

    private static final Logger log = LoggerFactory.getLogger(ValidateController.class);

    @PostMapping("/validate")
    public void validate(@Valid @RequestBody Student student, BindingResult result) {
        log.debug(student.toString());
        if (result.hasErrors()) {
            for (FieldError error : result.getFieldErrors()) {
                log.debug("{} 验证错误: {}", error.getField(), error.getDefaultMessage());
            }
        }
    }
}

说明

  • @Valid 用来标注是哪个对象需要验证

  • BindingResult result 用来保存验证结果,获取结果方法见以上代码

  • BindingResult result 与被 @Valid 对象必须紧邻

结果1 - 当姓名与年龄都不正确时

2021-11-02 09:03:12.147 DEBUG 30652 --- [nio-8080-exec-2] com.itheima.demo3.Controller06           : Student{name='', age=11}
2021-11-02 09:03:12.147 DEBUG 30652 --- [nio-8080-exec-2] com.itheima.demo3.Controller06           : age 验证错误: 最小不能小于18
2021-11-02 09:03:12.147 DEBUG 30652 --- [nio-8080-exec-2] com.itheima.demo3.Controller06           : name 验证错误: 不能为空

结果2 - 当年龄正确,姓名为空时

2021-11-02 09:03:43.525 DEBUG 30652 --- [nio-8080-exec-3] com.itheima.demo3.Controller06           : Student{name='', age=18}
2021-11-02 09:03:43.525 DEBUG 30652 --- [nio-8080-exec-3] com.itheima.demo3.Controller06           : name 验证错误: 不能为空

​5) @ResponseBody

虽然前面讲到了 RESTful 软件架构风格中,资源可以有多种表现形式,但实际用的最多的还是 json,之前见过的 @ResponseBody 的作用将控制器方法的返回值做相应的转换,再写入响应:

  • 如果方法返回的是字符串,这时结果仍被视为普通字符串,即 text/plain

  • 如果方法返回的是 Java Bean、List、Map 等,这时结果会转换为 json 字符串,即 application/json

  • 方法可以声明为 void,表示响应体没有内容

下面的例子都用到同一个 User 类

public class User {
    private Integer id;
    private String username;
    private String password;
    private Date birthday;
	
	// ...
}

例1 - 转换 java bean

@RequestMapping("/json/c1")
@ResponseBody
public User c1() {
	return new User(1001, "张三", "123", new Date());
}

访问后返回响应

{
  "id": 1001,
  "username": "张三",
  "password": "123",
  "birthday": 1611019823007
}

例2 - Jackson 注解

注意到

  1. 日期被转换为毫秒值

    • 当然这个值用起来也挺方便的,只是可读性差,可以用 @JsonFormat 注解来格式化

  2. 密码属性被转换成了 json

    • 如果转换时,希望某一属性被忽略,可以使用 @JsonIgnore 注解

public class User {
    private Integer id;
    private String username;
	
    @JsonIgnore
    private String password;
	
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date birthday;
	
	// ...
}

访问后响应

HTTP/1.1 200 OK

{
  "id": 1001,
  "username": "张三",
  "birthday": "2021-01-19 09:41:33"
}

注意

  • 这几个注解 jackson 类库提供的,如果你将来用其他 json 实现,需要根据实际情况修改

  • @JsonFormat 注解也可以用以下全局配置,更为方便

    • spring.jackson.date-format=yyyy-MM-dd HH:mm:ss

    • spring.jackson.time-zone=GMT+8:00

例3 - 忽略 null 值

@RequestMapping("/json/c2")
@ResponseBody
public User c2() {
    return new User(null, "张三", null, null);
}

 访问后响应

HTTP/1.1 200 OK

{
  "id":null,
  "username":"张三",
  "birthday":null
}

有时候希望 null 值的属性不要参与转换 json,可以这么做

@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
    
	// ...
}

或者通过配置

spring.jackson.default-property-inclusion=non_null

结果变成

HTTP/1.1 200 OK

{
  "username":"张三"
}

例4 - 转换 List

@RequestMapping("/json/c4")
@ResponseBody
public List<String> c4() {
	return Arrays.asList("张三", "李四");
}

访问后响应

HTTP/1.1 200 OK

[
  "张三",
  "李四"
]

例5 - 转换 Map

@RequestMapping("/json/c5")
@ResponseBody
public Map<String, String> c5() {
	Map<String, String> map = new HashMap<>();
	map.put("beijing", "北京");
	map.put("shanghai", "上海");
	map.put("shenzhen", "深圳");
	return map;
}

访问后响应

HTTP/1.1 200 OK

{
  "shanghai": "上海",
  "shenzhen": "深圳",
  "beijing": "北京"
}

可以看到它的转换结果与 java bean 是一样的,但 Map 有缺点:

  • Map 类型是弱类型,将来接参数时,key还好说都是 String,但 value 转换时不能控制该转换为何种类型

  • Map 上无法方便配合上述注解,对转换 json 时进行控制

6) 响应码与响应头

问题

@RestController
public class Controller06 {
    private static final Logger log = LoggerFactory.getLogger(Controller06.class);

    @RequestMapping("/rs/c1")
    public User c1() {
        int i = 1 / 0;
        return new User(1001, "张三", "123", new Date());
    }

    @ExceptionHandler
    public Map<String, String> ex(ArithmeticException e) {
        Map<String, String> map = new HashMap<>();
        map.put("error", e.getMessage());
        return map;
    }
}

 访问后响应

HTTP/1.1 200 OK

{
  "error": "/ by zero"
}

虽然也可以,但缺点是,必须解析响应体后才知道出现了错误

@ResponseStatus

可以用 @ResponseStatus 来指定控制器方法或异常处理方法的响应头

修改代码为

@RestController
public class Controller06 {

    // ...

    @ExceptionHandler
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, String> ex(ArithmeticException e) {
        Map<String, String> map = new HashMap<>();
        map.put("error", e.getMessage());
        return map;
    }
}

访问后响应

HTTP/1.1 500 Internal Server Error

{
  "error": "/ by zero"
}

其实在 RESTful 架构风格中,建议响应状态码应当更精确,例如

响应状态码用法
200 OK查询、更新、删除成功时
201 Created新增成功时,一般响应中应含有新资源的 URI
202 Accepted请求成功接收,会在未来处理(异步处理)
204 No Content成功但没有响应体时
400 Bad Request请求有误(例如参数不正确)
401 Unauthorized请求身份验证失败
403 Forbidden请求身份验证成功,但没有权限
404 Not Found请求资源不存在
405 Method Not Allowed请求 Method 不被支持
415 Unsupported Media Type请求资源的表现形式不支持
429 Too Many Requests请求次数超过限额
500 Internal Server Error服务器内部错误,服务仍可用
503 Service Unavailable服务器已不可用

这时候 @ResponseStatus 就可以派上用场了

响应码中还有一个比较重要的 304 这个讲到缓存时再说

ResponseEntity

如果希望控制响应的各个部分:响应码、响应头、响应体,还可以使用 ResponseEntity 类型作为控制器方法的返回值

例如:

@RequestMapping("/rs/c2")
public ResponseEntity<User> c2() {
	return ResponseEntity.ok()
			.header("My-Header", "yeah")
			.body(new User(1001, "张三", "123", new Date()));
}

访问后响应

HTTP/1.1 200 OK
My-Header: yeah

{
  "id": 1001,
  "username": "张三",
  "birthday": "2021-01-19 11:05:38"
}

7) 统一响应格式

  • 响应状态码的不足:上一节说到,可以用状态码较为精确地描述此操作是成功失败、是客户端的问题、还是服务端的问题,但状态码毕竟有限,是最为通用的描述,如果牵扯业务,就有些不够用了

  • 返回的响应体中,可能会表示正常的数据,也有可能包含错误提示,若不统一,就增加了前端解析成本

因此一般应用开发时,会定义一个 AppResult 统一响应格式

AppResult

@JsonInclude(JsonInclude.Include.NON_NULL)
public class AppResult {
	// 应用状态码,对响应状态码算是补充和扩展
    private Integer code;	
	// 应用出错描述
    private String msg;	
	// 应用响应数据
    private Object data;
    public AppResult(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }	
	// ...
}

对应的 json 数据如下

HTTP/1.1 响应状态码

{
  "code": "应用状态码",
  "msg": "应用出错描述",
  "data": "应用响应数据"
}

应用状态码都是应用内自定义的,以豆瓣 api 的为例,根据豆瓣 api 中通用错误码来看,它的这些错误码有两层分类:大类关联响应状态码

400 - 客户端校验错误
401 - 客户端认证失败
403 - 客户端权限错误
404 - 资源不存在

而每大类下又分了多个小类,小类关联应用状态码,例如  

400
    999 unknown_v2_error
    1002 missing_args     缺失参数
    1003 image_too_large  图片太大
    1004 has_ban_word     有违禁词
    1005 input_too_short
    1006 target_not_fount
    1008 image_unknow
    1009 image_wrong_format
    1012 title_missing
    1013 desc_missing
403
    1001 need_permission   需要权限
    1007 need_captcha      需要验证码
    1010 image_wrong_ck
    1011 image_ck_expired   图片过期

本章注解

Web 注解

注解名称位置注解作用备注
@RequestMapping方法映射路径
@RequestMapping为映射路径加统一前缀
@ResponseBody方法该控制器方法的返回值即为响应体内容
@ResponseBody影响该类的所有控制器方法
@RestController组合注解 @Controller + @ResponseBody
@RequestParam参数主要设置请求参数默认值
@RequestHeader参数获取请求头,头名称不区分大小写
@CookieValue参数获取 cookie 值
@ExceptionHandler方法处理控制器异常
@RestControllerAdvice该类用来全局处理控制器异常,@ControllerAdvice + @ResponseBody
@ResponseStatus方法控制返回响应的状态码

JSON 注解

注解名称位置注解作用备注
@JsonIgnore成员变量转 json 时忽略此成员变量
@JsonFormat成员变量转 json 时控制日期格式和时区有等价配置
@JsonInclude控制该类取值 null 的成员变量不参与转换有等价配置

校验注解

注解名称位置注解作用备注
@Valid方法参数该参数需要校验
@NotEmpty成员变量字符串不能为 null、空、集合不能为空
@Min成员变量数字不能小于某个值
@Max成员变量数字不能大于某个值
@NotNull成员变量不能为 null
@NotBlank成员变量字符串不能为 null、空、不能全为空白字符
@DecimalMin成员变量与 @Min 类似,但可以校验字符串
@DecimalMax成员变量与 @Max 类似,但可以校验字符串
@Digits成员变量校验数字的整数、小数位数
@Size成员变量字符串长度、集合大小
@Past成员变量必须是过去的时间
@Future成员变量必须是未来的时间
@Pattern成员变量必须符合正则表达式
@Email成员变量必须符合 Email 格式


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值