Restful架构

REST 简介


Roy Thomas Fielding 在 2000 年的博士论文中提出 Rest 架构。它是思考如何开发在互联网环境使用的软件的结果。Fielding 将他对互联网软件的架构原则,定名为 REST,即 Representational State Transfer。如果一个架构符合 REST 原则,那么就称为 RESTful 架构。

RESTful 架构是目前最流行的一种互联网软件架构。REST 的名称,表现层状态转化,表现层指资源(Resources) 的表现层。

资源

资源其实就是网络上的一个实体,你可以用一个 URI 指向它,每种资源对应一个特定的 URI。要获取这个资源,访问它的 URI 就可以,URI 称为每一个资源的地址或者独一无二的识别符。而上网也就是与互联网上一系列的资源互动。

资源是一种信息实体,可以有多种表现形式,资源的具体呈现出来的形式,叫做它的表现层(Representation)

例如:文本可以用 txt 格式表现,也可以用 HTML 格式、XML 格式、JSON 格式表现,甚至二进制格式。图片可以用 JPG 格式表现,也可以用 PNG 格式表现。

表现层

URI 只代表资源的实体,不代表它的形式,严格说,一些网址最后的 html 后缀名不是必要的,因为这个后缀名属于表现层范涛,而 URI 应该只代表资源的位置。它的具体表现形式,应该在 HTTP 请求的头信息中用 Accept 和 Content-Type 字段指定的。这两个字段才是对表现层的描述。

状态转化(State Transfer)

浏览网站时,代表客户端和服务器互动,这个过程势必涉及数据和状态变化。HTTP 是无状态协议,这意味着,所有状态都保存在服务器端。如果客户端想要操作服务器,必须通过某种手段,让服务器端发生 状态转化 (State Transfer)。这种转化是建立在表现层上的。所以是表现层状态转化。

客户端只能通过 HTTP 协议的 GET、POST、PUT、DELETE 来和服务器交互。GET 获取资源,POST 新建资源(更新资源),PUT 更新资源,DELETE 删除资源。

RESTful 架构:

  • 每一个 URI 代表一种资源
  • 客户端和服务器之间,传递这种资源的某种表现层
  • 客户端通过 HTTP 对服务器端资源进行操作,实现 表现层状态转化。

RESTful 设计


以前我们在设计接口时:

  • 列表
    • /bbs_list.do?keyword=xx
  • 查看一篇帖子
    • /bbs_view.do?id=xx
  • 查看一篇帖子的所有回帖
    • /bbs_replay_list.do?id=xx
  • 回帖
    • /bbs_replay.do?id=xx
  • 删帖
    • /bbs_delete.do?id=xx

在加上很多人的习惯不同,命名会出现 delete、remove 等等近义词。代码就非常混乱。大量的接口方法,URL 地址设计复杂,需要在 URL 里面表示出资源及其操作。

在 RESTful 架构中,每一个网址代表一种资源,网址中不能有动词,只能有名词。所用的名词往往与数据库的表名对应。一般的,数据库中的表都是一种记录的集合,所以 URI 中的名词应该也是复数。比如:

  • https://api.example.com/v1/zoos 动物园资源
  • https://api.example.com/v1/animals 动物资源
  • https://api.example.com/v1/employees 饲养员资源

参考例子:https://api.github.comRESTful API 指南

HTTP 动作设计

动作 意义
GET (SELECT) 从服务器取出资源(一项或多项)
POST (CREATE) 从服务器新建一个资源
PUT (UPDATE) 在服务器更新资源(客户端提供改变后的完整资源),PUT 更新整个对象
PATCH (UPDATE) 在服务器更新资源(客户端提供改变的属性[补丁]),PATCH 更新个别属性
DELETE (DELETE) 从服务器删除资源
HEAD 获得一个资源的元数据,比如一个资源的 hash 值或者最后修改日期
OPTIONS 获得客户端针对一个资源能够实施的操作:(获取该资源的 api 能够对资源做什么操作的描述)

例子:

URI 表示含义
GET /zoos 列出所有动物园 (数组或者集合)
POST /zoos 新建一个动物园
GET /zoos/ID 获取某个指定动物园的信息,/zoos/1 地址栏传参
PUT /zoos/ID 更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH /zoos/ID 更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE /zoos/ID 删除某个动物园
GET /zoos/ID/animals 列出某个指定动物园的所有动物

获取某个部门的员工

1
2
GET /depts/ID/employees 标准
/employees?deptId=1 也可以用

返回结果类型

URI 返回结果类型
GET /zoos 返回资源对象的列表(数组/集合)
POST /zoos 返回新生成的资源对象
PUT /zoos/ID 返回完整的资源对象
PATCH /zoos/ID 返回完整的资源对象
DELETE /zoos/ID 返回一个空文档

可以通过 URL 规定获取格式类型,但是建议使用 Accept 这个请求头:
Accept 表示客户端希望接受的数据类型,比如 Accept: application/json; 代表客户端希望接受的数据类型是 json 类型,后台返回 json 数据。
Content-Type 表示发送端(客户端|服务器) 发送的实体数据的数据类型,比如 ContextType: application/json; 表示发送端发送的数据格式是 json,后台就是要以这种格式来接受前端发送来的数据。
二者结合就是 及代表希望接受的数据类型是 json 格式,本次请求发送的数据的数据格式也是 json 格式。

Http 报头分为 通用包头、请求报头、响应报头和实体报头。请求方的 http 报头结构:通用报头|请求报头|实体报头。请求方的 http 报头结构:通用报头|请求报头|实体报头;响应方的 http 报头结构:通用报头|响应报头|实体报头

Accept 属于请求头,Content-Type 属于实体头。

设计误区


RESTful 架构最经典的设计误区,就是 URI 中包含动词。资源时一种实体,所以应该是名词。URI 中不应该有动词,动词应该放在 HTTP 协议中。

举例:某个 URI 是 /posts/show/1 其中 show 是动词,URI 就设计错了。正确的写法是 /posts/1,然后用 GET 方法表示 show。如果有些动作是 HTTP 动词表示不了的,比如网上汇款,从账户1向账户2汇款500元,错误的 URI 是 POST /accouts/1/transfer/500/to/2 正确的写法是把 transfer 改成名词 transaction,应该把动作做成一种资源,也可以是一种服务:

1
2
3
4
POST /transaction HTTP/1.1
Host: 127.0.0.1

from=1&to=2&amount=500.00

另一种设计误区是在 URI 中加入版本号

1
2
3
http://www.example.com/app/1.0/foo
http://www.example.com/app/1.1/foo
http://www.example.com/app/2.0/foo

由于版本不同,可以理解为同一种资源的不同表现形式,应该采用同一个 URI,版本号应当在 HTTP 请求头信息的 Accept 字段中区分。

1
2
3
Accept: vnd.example-com.foo+json; version=1.0
Accept: vnd.example-com.foo+json; version=1.1
Accept: vnd.example-com.foo+json; version=2.0

API 接口测试工具


Postman、Insomnia。

根据需求设计接口


  • 获取所有员工(集合)
    • 资源设计:/employees
    • 动作设计:get
    • 请求参数设计:无
    • 返回结构设计:集合,状态码 200
1
2
3
4
5
6
7
8
9
10
@RequestMapping(value = "employees", method = RequestMethod.GET)
@ResponseBody
public List<Employee> list() {
List<Employee> list = new ArrayList<>();
Employee e1 = new Employee(1l, "李朝1", 20);
Employee e2 = new Employee(2l, "李朝2", 21);
list.add(e1);
list.add(e2);
return list;
}

  • 获取某个员工的信息
    • 资源设计:/employees/{id} —-> 路径占位符
    • 动作设计:get
    • 请求参数设计:无
    • 返回结构设计:员工对象,状态码 200
1
2
3
4
5
@RequestMapping(value = "employees/{id}", method = RequestMethod.GET)
@ResponseBody
public Employee list(@PathVariable("id") Long id) {
return new Employee(1l, "李朝1", 20);
}

由于以后会遇到很多花括号,比如 “employees/{id}/{name}…” 所以必须明确参数是哪个,因此需要 @PathVariable 注解来注入值,可以使用 @PathVariable(“id”) 指定参数是 URI 路径上的哪个参数,如果不指定,则会用参数名去路径上匹配。

但其实这样写比较麻烦,由于将来开发的时候,前后端分离,所以基本只会写接口。因此每个方法上都要写 @ResponseBody,这个时候可以把 @ResponseBody 贴到类上,而且在 Spring4.0 之后,除了一个新的注解 @RestController,将 @ResponseBody 和 @Controller 合并。因此之后只要是只设计接口的 Controller 直接使用 @RestController 即可。

而 @GetMapping 实际上又是 Method=RequestMethod.GET 的 RequestMapping,下面是 GETMapping 的声明。+

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {

/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";

/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
...
...
  • 删除一个员工
    • 资源设计:/employees/{id} —-> 路径占位符
    • 动作设计:delete
    • 请求参数设计:无
    • 返回结构设计:空文档,状态码 204(需要手动设置,一般公司不会去主动设置,除非是大众公用的接口,才会规范)
1
2
3
4
5
@DeleteMapping("{id}")
public void delete(@PathVariable Long id, HttpServletResponse response) {
response.setStatus(204);
System.out.println("删除id="+ id + "的员工");
}

  • 获取某个员工某个月的薪资记录
    • 资源设计:/employees/{id}/salaries/{month}
    • 动作设计:get
    • 请求参数:员工id,月份
    • 返回结果:薪资对象,200
1
2
3
4
@DeleteMapping("{id}/salaries/{month}")
public Salary get(@PathVariable Long id, @PathVariable Date month) {
return new Salary(1l, new BigDecimal(1000), 1l, new Date());
}

前台传入时间格式,需要解析,一般有两种方式,一个是贴注解 @DateTimeFormat、或者写一个 ControllerAdvice
返回格式需要使用 @JsonFormat 注解在 domain 中去规定。

  • 给某个员工增加一条薪资记录
    • 资源设计:/employees/{empId}/salaries
    • 动作设计:post
    • 请求参数:员工id,salary对象
    • 返回结果:新生成薪资对象,201
1
2
3
4
5
6
@PostMapping("{employeeId}/salaries")
public Salary post(Salary salary) {
salary.setId(5l);
System.out.println("在 emp_sal 表中新增一条数据");
return salary;
}

2019-09-04 at 2.54 P

最终版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("employees")
public class EmployeeController {
@GetMapping
public List<Employee> list() {
List<Employee> list = new ArrayList<>();
Employee e1 = new Employee(1l, "李朝1", 20);
Employee e2 = new Employee(2l, "李朝2", 21);
list.add(e1);
list.add(e2);
return list;
}

@GetMapping("{id}")
public Employee list(@PathVariable Long id) {
return new Employee(1l, "李朝1", 20);
}
}

状态码


状态码 表示
200 OK - [GET] 服务器成功返回用户请求的数据
201 CREATED - [POST/PUT/PATCH] 用户新建或修改数据成功
202 Accepted - [*] 表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE] 用户删除数据成功
400 INVALID REQUEST - [POST/PUT/PATCH] 用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的
401 Unauthorized - [*] 表示用户没有权限(令牌、用户名、密码错误)
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的
404 NOT FOUND - [*] 用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的
406 Not Acceptable - [GET] 用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)
410 Gone -[GET] 用户请求的资源被永久删除,且不会再得到的
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误
500 INTERNAL SERVER ERROR - [*] 服务器发生错误,用户将无法判断发出的请求是否成功

状态码错误演示

405

1
2
3
4
5
6
7
8
9
10
@RequestMapping(value = "employees", method = RequestMethod.GET)
@ResponseBody
public List<Employee> list() {
List<Employee> list = new ArrayList<>();
Employee e1 = new Employee(1l, "李朝1", 20);
Employee e2 = new Employee(2l, "李朝2", 21);
list.add(e1);
list.add(e2);
return list;
}

后台限制了请求使用的 HTTPMethod,前台请求时使用的 Method 不匹配

RequestMapping 配置的路径的时候,是有数组特性的,可以配置多个路径以及多个请求方法

415

后台限制了接受的参数类型,前台传入参数类型不匹配。

RequestMapping


params

1
2
3
4
@RequestMapping(value = "list", params = "name=admin")
public void test() {

}

params 参数表示,请求时,必须带上指定参数名与参数值,否则报 400 错误。name=admin 或者 name!=admin

headers

1
2
3
4
@RequestMapping(value = "list", headers = "content-type=application/json")
public void test() {

}

headers 参数表示,要求请求时必须带有规定的头信息。

consumes(消费)、produces(生产)

1
2
3
4
5
6
7
8
9
10
11
// 表示后台方法专门消费某种格式的数据  输入格式定死,可以有多个资源,不同的输入格式。
@RequestMapping(value = "list", consumes = "application/json")
public void test() {

}

// 表示后台方法专门生产某种格式的数据 输出格式定死,可以给出多个资源的不同的输出格式
@RequestMapping(value = "list", produces = "application/json")
public void test() {

}

上面的含义与 headers 一样,因为服务器要消费客户端传过来的数据,因此指定传入的数据类型。consumes 是 headers 规定 ContentType 的简写,而且 consumes 只跟 ContentType 有关系。相当设置了 headers=”ContentType=application/json”

produces 规定的是 accept 接受的数据类型,前台希望接收什么格式,就生产什么格式。相当于配置了 headers = “Accept=application/json”

不同数据类型请求参数的封装


  • form-data 表单提交,其实是将表单数据序列化编程,URI?xx=xx&xx=xx 的形式。
    • SpringMVC 默认的封装格式,无需任何注解可以直接将 form-data 转化为 javaBean
  • JSON(application/json) 传输为 json 数据

格式为:

1
2
3
4
5
{
"id" : 1,
"name" : "李朝",
"age": 21
}
1
2
3
4
5
@PostMapping(value = "consumes", consumes = "application/json")
public Employee consumes(@RequestBody Employee e) {
System.out.println(e);
return e;
}

需要参数前贴 @RequestBody 注解,解析 Json 数据为 javaBean 对象。@RequestBody 注解默认是解析 json 数据。但如果请求数据为 xml 数据。

  • xml

如果请求数据为 xml,在 @RequestBody 注解解析数据时,要告诉解析方式,需要在 domain 的 javabean 类中告诉 xml 跟元素,和匹配属性还是字段 XmlAccessType.PROPERTY或者@XmlAccessorType(XmlAccessType.FIELD), @RequestBody 不可少

1
2
3
4
5
<Employee>
<id>1</id>
<name>李朝</name>
<age>21</age>
</Employee>

需要在 domain 中贴注解

1
2
3
4
5
6
7
@XmlRootElement(name = "Employee") // 根元素的名称
@XmlAccessorType(XmlAccessType.PROPERTY) // 匹配属性
public class Employee {
private Long id;
private String name;
private Integer age;
}

如果 xml 中名字不同

1
2
3
4
5
<Employee>
<id>1</id>
<nameaa>李朝</nameaa>
<age>21</age>
</Employee>
1
2
3
4
5
6
7
8
@XmlRootElement(name = "Employee") // 根元素的名称
@XmlAccessorType(XmlAccessType.PROPERTY) // 匹配字段
public class Employee {
private Long id;
@XmlElement(name = "nameaa")
private String name;
private Integer age;
}

Ajax 发送不同类型的请求


jquery 默认封装的 发送请求只有 get post,其他请求方式没有封装,需要使用 ajax 原生的发送请求方法

ajax 发送 DELETE 请求

1
2
3
4
5
6
$("#deleteBtn").click(function (){
$.ajax({
url: '/employees/1'
method: 'delete'
})
})

ajax 发送 PUT 请求

1
2
3
4
5
6
7
8
9
10
$("#deleteBtn").click(function (){
$.ajax({
url: '/employees/1'
method: 'put'
data:{
name: "xxx",
age : 21
}
})
})

PUT 请求 springMVC 不会帮我们将 data 参数封装到 后台的 javabean 中,只有 id = 1 封装到 Employee 了, name 和 age 都消失了,需要配置一个 springmvc 的 filter 才可以

1
2
3
4
5
6
7
8
9
10
<!-- 处理 put 或者 patch 请求方式的过滤器 -->
<filter>
<filter-name>httpPutFormContentFilter</filter-name>
<filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>httpPutFormContentFilter</filter-name>
<servlet-name>springDispatcherServlet</servlet-name>
</filter-mapping>

拦截到请求时,先执行 httpPutFormContentFilter,然后再执行 springDispatcherServlet。

form 表单提交 put/patch/delete 请求


解决问题:

因为表单无法提交 put/delete/patch 请求,所以后台 @PUTMapping 资源无法被表单提交访问。

1
2
3
4
<form action="/employees/1" method="post">
<input type="hidden" name="_method" value="put/delete/patch"/>
...
</form>
1
2
3
4
5
6
7
8
9
10
<!-- 专门处理 form 表单不能提交 put, delete, patch 请求的过滤器 -->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<servlet-name>springDispatcherServlet</servlet-name>
</filter-mapping>

此时配置了 web.xml 后就可以用表单提交 put/delete/patch 等请求了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

HttpServletRequest requestToUse = request;

if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}

filterChain.doFilter(requestToUse, response);
}

原理,将请求重新包装成新的请求再放行。

文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2019/09/03/Restful%E6%9E%B6%E6%9E%84/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论