[SpringBoot][11][构建REST风格的网站]

第 11 章 构建REST风格网站

HTTP协议发展的过程中,提出了很多的规则,但是这些规则有些烦琐,于是又提出了一种风格约定,它便是REST风格。实际上严格地说它不是一种标准,而是一种风格。在现今互联网的世界中这种风格己经被广泛使用起来了。尤其是现今流行的微服务中,这样的风格甚至被推荐为各个微服务系统之间用于交互的方式。

首先在REST风格中,每一个资源都只是对应着一个网址,而一个代表资源的网址应该是一个名词,且不存在动词,这代表对一个资源的操作。在这样的风格下对于简易参数则尽量通过网址进行传递。例如,要获取idl的用户的URL可能就设计http//localhost:8080/user/1

11.1 REST简述

11.1.1 REST名词解释

REST按其英文名称(Representational State Transfer)可翻译为表现层状态转换。首先需要有资源才能表现,所以第一个名词是"资源"。有了资源也要根据需要以合适的形式表现资源,这就是第二个名词是"表现层"。最后是资源可以被新增、修改、删除等,也就是第三个名词“状态转换”

  • 资源: 它可以是系统权限用户、角色和菜单等,也可以是一些媒体类型,如文本、图片、歌曲,总之它就是一个具体存在的对象。每个资源对应一个独一无二的URI。在REST中,URI也可以称为端点(EndPoint)。
  • 表现层: 有了资源还需要确定如何表现这个资源。例如,一个用户可以使用JSONXML或者其他的形式表现出来
  • 状态转换: 现实中资源并不是一成不变的, 它是一个变化的过程,一个资源可以经历创建(create)、访问)(visit) 、修改(update)和删除(delete)的过程

综上我们可以总结REST风格架构的特点

  • 服务器存在一系列的资源,每一个资源通过单独唯一的URI进行标识
  • 客户端和服务器之间可以相互传递资源,而资源会以某种表现层得以展示
  • 客户端通过HTTP协议所定义的动作对资源进行操作,以实现资源的状态转换

11.1.2 HTTP的动作

可以用HTTP请求的类型,来代表对资源的CRUD的行为

  • GET: READ,访问服务器资源
  • POST: CREATE,在服务器创建新的资源
  • PUT: UPDATE,修改服务器已经存在的资源,使用PUT时需要把资源的全部属性一并提交
  • PATCH: UPDATE,修改服务器已经存在的资源,使用PATCH时只需要将部分资源属性提交
  • DELETE: DELETE,从服务器将资源删除

下面举几个REST风格的例子

1
2
3
4
5
6
7
8
9
10
# 获取用户信息,1代表用户编号
GET /user/1
# 查询多个用户信息
GET /users/{userName}/{note}
# POST 创建用户
POST /user/{userName}/{sex}/{note}
# 修改用户全部属性
PUT /user/{id}/{userName}/{sex}/{note}
# 修改用户名称
PATCH /user/{id}/{userName}

URI中并没有出现动词,而对于参数主要通过URI设计去获取。对于参数数量超过5个的可以考虑使用传递JSON的方式来传递参数

11.1.3 REST风格的一些误区

  1. REST风格的URI中不存在动词,例如GET /user/get/1,应改为GET /user/1

  2. URI中不应该加入版本号,例如下面GET /v1/user/1,如果存在版本号应设置在请求头中

  3. 类似这种PUT users?userName=user_name&note=note是不推荐使用的,应改为PUT users/{userName}/{note}

11.2 使用SpringMVC开发REST风格端点

SpringREST风格的支持是基于SpringMVC设计基础上的,在Spring 4.3之后则有更多的注解引入使得REST风格的开发更为便捷。

11.2.1 SpringMVC整合REST

只要把URI设计为符合REST风格规范,那么显然就己经满足REST风格了。不过为了更为便捷地支持REST风格的开发,Spring 4.3之后除了@RequestMapping外,还可以使用以下5个注解,这5个注解主要是针对HTTP的动作而言的,通过它们就能够有效地支持REST风格的规范

  • @GetMapping: 对应HTTPGET请求,获取资源
  • @PostMapping: 对应HTTPPOST请求,创建资源
  • @PutMapping: 对应HTTPPUT请求,提交所有资源属性以修改资源
  • @PatchMapping: 对应HTTPPATCH请求,提交资源部分修改的属性
  • @DeleteMapping: 对应HTTPDELETE请求,删除服务器端的资源

REST风格的设计中,如果是简单的参数,往往会通过URL直接传递,在SpringMVC可以使用注解@PathVariable进行获取,对于那些复杂的参数,可以考虑使用请求体JSON的方式提交给服务器,这样就可以使用注解@RequestBodyJSON数据集转换为Java对象。

在现今的开发中,数据转化为JSON是最常见的方式,这个时候可以考虑使用注解@ResponseBody,这样SpringMVC就会通过MappingJackson2HttpMessageConverter最终将数据转换为JSON数据集,而在SpringMVCREST风格的设计中,甚至可以使用注解@RestController让整个控制器都默认转换为JSON数据集。

11.2.2 使用Spring开发REST风格端点

我们可以使用POST动作来创建资源

1
2
3
4
5
@PostMapping("/user")
@ResponseBody
public User insertUser(@RequestBody User user){
return userService.insertUser(user);
}
  • @PostMapping表示采用POST动作提交用户信息
  • @ReguestBody代表接收的是一个JSON数据集参数
  • @ResponseBody代表会将函数的返回值转化为JSON格式传递给前端

接下来就是使用GET动作来获取对象了

1
2
3
4
5
@GetMapping(value="/user/{id}")
@ResponseBody
public User getUser(@PathVariable("id")Long id){
return userService.getUser(id);
}
  • 采用注解@GetMapping声明HTTPGET请求,并且把参数编号(id)以URI的形式传递,这符合了REST风格的要求
  • getUser方法中使用了注解@PathVariableURI中获取参数
  • @ResponseBody代表会将函数的返回值转化为JSON格式传递给前端

更新和删除操作也与上面的例子类似,这里略

11.2.3 使用@RestController

因为现在前后端分离,所以使用JSON作为前后端交互已经十分普遍。如果每一个方法都加入@ResponseBody才能将数据模型转换为JSON,显然有些冗余。SpringMVC还存在一个注解@RestController,它可以修饰控制器类,将类中的方法返回值转化为JSON数据

1
2
3
4
5
6
7
@RestController
public class UserController{
@GetMapping(value="/user/{id}")
public User getUser(@PathVariable("id")Long id){
return userService.getUser(id);
}
}

11.2.4 渲染结果

@RequestMappingGetMapping等注解中还存在consumesproduces两个属性。其中consumes
代表的是限制该方法接收什么类型的请求体(body), produces代表的是限定返回的媒体类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回。

假如我们想要某个方法返回一个字符串,而不是JSON的话,可以采用如下方法

1
2
3
4
5
@GetMapping(value="/user/name/{id}", produces=MediaType.TEXT_PLAIN_VALUE)
public String getUserName(@PathVariable("id") Long id){
User user = userService.getUser(id);
return user.getUserName();
}
  • 对于getUserName方法,因为@GetMapping的属性produces声明为普通文本类型,也就是修改了原有@RestController默认的JSON类型,同样结果也会被SpringMVC自身注册好的StringHttpMessageConverter拦截, 这样就可以转变为一个简单的字符串。

11.2.5 处理HTTP状态码、异常和响应头

当发生资源找不到或者处理逻辑发生异常时, 需要考虑的是返回给客户端的HTTP状态码和错误消息的问题。为了简化这些开发,Spring提供了实体封装类ResponseEntity和注解@ResponseStatusResponseEnti ty可以有效封装错误消息和状态码,通过@ResponseStatus可以配置指定的响应码给客户端。

下面我们修改插入用户的方法,将状态码修改为201,并且插入响应头的属性来标识这次请求的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@PostMapping(value = "/user2/entity")
public ResponseEntity<UserVo> insertUserEntity(
@RequestBody UserVo userVo) {
User user = this.changeToPo(userVo);
userService.insertUser(user);
UserVo result = this.changeToVo(user);
HttpHeaders headers = new HttpHeaders();
String success =
(result == null || result.getId() == null) ? "false" : "true";
// 设置响应头,比较常用的方式
headers.add("success", success);
// 返回创建成功的状态码
return new ResponseEntity<UserVo>(result, headers, HttpStatus.CREATED);
}
@PostMapping(value = "/user2/annotation")
// 指定状态码为201(资源已经创建)
@ResponseStatus(HttpStatus.CREATED)
public UserVo insertUserAnnotation(@RequestBody UserVo userVo) {
User user = this.changeToPo(userVo);
userService.insertUser(user);
UserVo result = this.changeToVo(user);
return result;
}
  • insertUserEntity方法中定义返回为一个ResponseEntity<User>的对象,这里还生成了响应头( HttpHeaders对象),并且添加了属性success来表示请求是否成功,在最后返回的时刻生成了一个ResponseEntity<User>对象,然后将查询到的用户对象和响应头捆绑上,并且指定状态码为201(创建资源成功)
  • insertUserAnnotation方法上则使用了@ResponseStatus注解将HTTP的响应码标注为201(创建资源成功),所以在方法正常返回时Spring就会将响应码设置为20l

但是有时候会出现一些异常,例如,按照id查找用户,可能查找不到数据,这个时候就不能以正常返回去处理了,又或者在执行的过程中产生了异常,这也是需要我们进行处理的。通过@ControllerAdvice@ExceptionHandler注解可以通过控制器通知的方式对异常情况进行处理

首先我们自定义一个查找失败异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class NotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
// 异常编码
private Long code;
// 异常自定义信息
private String customMsg;

public NotFoundException() {
}

public NotFoundException(Long code, String customMsg) {
super();
this.code = code;
this.customMsg = customMsg;
}
//getter and setter
}

然后自定义一个控制器通知,来自定义异常的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//控制器通知
@ControllerAdvice(
// 指定拦截包的控制器
basePackages = { "com.springboot.chapter11.controller.*" },
// 限定被标注为@Controller或者@RestController的类才被拦截
annotations = { Controller.class, RestController.class })
public class VoControllerAdvice {
// 异常处理,可以定义异常类型进行拦截处理
@ExceptionHandler(value=NotFoundException.class)
// 以JSON表达方式响应
@ResponseBody
// 定义为服务器错误状态码
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> exception(HttpServletRequest request, NotFoundException ex) {
Map<String, Object> msgMap = new HashMap<>();
// 获取异常信息
msgMap.put("code", ex.getCode());
msgMap.put("message", ex.getCustomMsg());
return msgMap;
}
}
  • 这里使用了@ControllerAdvice来标注类,说明在定义一个控制器通知。
  • basePackages配置了它所拦截的包,annotations限定了拦截的那些被标注为注解@Controller@RestController的控制器,
  • 这里的@ExceptionHandler定义了拦截NotFoundException的异常
  • @ResponseBody定义了响应的信息以JSON格式表达
  • @ResponseStatus定义了状态码为500(服务器内部错误),这样就会把这个状态码传达给请求者

最后我们让前文定义的getUser方法抛出异常,这样完成了当发生资源找不到或者处理逻辑发生异常这种情况下的处理逻辑

1
2
3
4
5
6
7
8
9
@GetMapping(value="/user/{id}")
@ResponseBody
public User getUser(@PathVariable("id")Long id){
User user = userService.getUser(id);
if(user == null){
throw new NotFoundException(1L, "找不到用户["+id+"]信息")
}
return userService.getUser(id);
}

这样当发生异常时,就可以返回500状态码和异常信息字符串了