REST与RESTful介绍
REST
翻译为表现层状态转移(Representational State Transfer
,REST
),是一种软件架构风格,是Roy Thomas Fielding
在他2000年的博士论文中提出的,该结构风格提供一些指导原则和最优方法来帮助我创建可扩展的web服务REST
是应用于分布式系统组件设计的一套协调的约束,它可以带来更高性能和更可维护的体系结构RESTful
风格的API是遵循REST风格构建的API,它可以通过一套统一的接口为Web,IOS和Android
提供服服务RESTful
架构风格的本质就是设计出结构清晰、符合标准的URI
,用URI
定位资源、用HTTP
动词(GET、POST、PUT、DELETE
)描述操作,以便于理解和方便扩展
URI命名规范
REST风格对于资源的定义,即URL的定义,是最重要的,根据查阅的相关资料,总结以下规则作为基本遵循
1.URL中不能有动词
-
在
Restful
架构中,每个网址代表的是一种资源,所以网址中没有有动词只有名词,而且所用的名词往往与数据库的表格名对应 -
一般来说,数据库中的表都是同种记录的集合,所以API中的名词也应该使用复数
例如:
https://api.example.com/v1/employees
表示所有员工
2.URL结尾不应该包含斜杠"/"
-
作为URL路径中处理中最重要的规则之一,正斜杠(
"/"
)不会增加语义值,可能导致混淆 -
REST API
不允许一个尾部的斜杠,不应该将它们包含在提供给客户端的链接的结尾处 -
URI
中的每个字符都会计入资源的唯一身份的识别中,两个不同的URI映射到两个不同的资源 -
REST API
必须生成和传递精确的URI
,不能容忍任何的客户端尝试不精确的资源定位
3.正斜杠分隔符"/"
用来指示层级关系
URI
路径中的正斜杠"/"
字符用于指示资源之间的层次关系
例如:
https://api.example.com/v1/employees/101
表示工号为101的员工
https://api.example.com/v1/employees/101/departments
表示101号员工的部门
4.API的版本号
https://api.example.com/v1/employees
URI请求中可使用版本号以区分版本,取决于自己开发团队的习惯和业务的需要,不是强制的
有的做法是将版本号放在HTTP头信息中,但不如放入URL方便和直观
5.URI路径一般要求
- URI路径区分大小写,一般选用小写字母
- URI多个字符串下加下划线(
_
)字符可能可能被这个下划线部分地遮蔽,因此常用连字符(-
) - 对于URI后面的携带传递的字符串参数部分,字符串参数名称通常与后端接口参数一致即可
6.资源过滤
在获取资源的时候,有可能需要获取某些过滤后的资源,例如指定前10行数据:
https://api.example.com/employees?page=1&pageSize=10
使用HTTP状态码
有很多服务器将返回状态码一直设为200,然后在返回body里面自定义一些状态码来表示服务器返回结果的状态码。RESTful Web API
是直接使用的HTTP协议,所以它的状态码也要尽量使用HTTP协议的状态码
服务器向用户返回的状态码和提示信息,常见的如下:
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
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 - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
使用RESTful风格的HTTP动词
对于REST API
资源的操作类型,由HTTP
动词表示,常用的动词是GET,POST,PUT,DELETE
,包括如下内容:
GET: 获取资源
POST: 新建资源
PUT:在服务器更新资源(客户端提供改变后的所有资源)
PATCH: 在服务器更新资源(客户端提供改变的属性,一般常用PUT)
DELETE:删除资源
HEAD:获取资源的元数据
OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的
RESTful风格的URI设计典例
URI体现层次结构,但是没有任何硬性规定,只能确保这种结构对使用者有意义,与软件开发过程中的所有内容一样,命名对于成功至关重要,以下是一些典型例子
对于客户资源建议的URI:
POST http://www.example.com/customers 插入(创建)新客户
GET http://www.example.com/customers/33245 读取客户ID为33245的客户
PUT http://www.example.com/customers/33245 用于更新ID为33245的客户
DELETE http://www.example.com/customers/33245 用于更新ID为33245的客户
对于产品的建议的URI:
POST http://www.example.com/products 创建新产品
GET | PUT | DELETE http://www.example.com/products/66432 分别用于读取,更新和删除产品66432
对于存在客户与产品关系的URI:
POST http://www.example.com/orders 创建订单,但是可以说它不在客户范围内,为了URI更直观,可以这样设计:
POST http://www.example.com/customers/33245/orders 这样更清晰,表示正在为客户ID#33245创建订单
GET http://www.example.com/customers/33245/orders 读取客户#33245已创建或拥有的订单的列表,
由于该网址在集合上运行,因此我们可能选择不支持该网址的DELETE或PUT
提现层次结构URI:
POST http://www.example.com/customers/33245/orders/8769/lineitems 给客户33245添加订单8769的订单项
GET http://www.example.com/customers/33245/orders/8769/lineitems 返回客户该订单的所有订单项
POST www.example.com/orders/8769/lineitems 单项仅在客户环境中没有有意义,或者在客户环境之外也无意义,就可以这样设计
GET http://www.example.com/orders/8769 若给定资源可能有多个URI,则只需提供一个URI支持按编号检索订单,而不必知道客户编号
更层次结构的URI:
GET http://www.example.com/customers/33245/orders/8769/lineitems/1 在相同的订单顺序中序返回第一个订单项
使用Restful风格接口案例
1.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>simple</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<version.swagger>2.8.0</version.swagger>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.User.java
package com.modle;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 简单的模型类用于测试
* @Author: wongzeng
*/
@Slf4j
@Data
public class User{
private Long id;
private String username;
private String password;
static List<User> userList = new LinkedList<>();
static {
for(int i=0;i<30;i++){
userList.add(new User((long)i+1,"user-"+i,"passwd-"+i));
}
}
//测试
public static List<User> getUserList(){
return userList;
}
public static boolean addUser(User user){
if(user!=null){
userList.add(user);
return true;
}
return false;
}
//测试条件删除
public static boolean removeUser(Long id){
return userList.removeIf(user -> user.getId().equals(id));
}
//测试更新
public static boolean updateUser(User user){
if(userList.stream().filter(obj->obj.getId().equals(user.getId())).findAny().orElse(null)==null){
return false;
}
userList = userList.stream().map(obj->obj.getId().equals(user.getId())?user:obj).collect(Collectors.toList());
return true;
}
public User(Long id,String username, String password) {
this.username = username;
this.password = password;
this.id = id;
}
//测试
public static void main(String[] args) {
userList.add(new User(210303193443566L,"xx","xx"));
Long id = 210303193443566L;
long id2 = 210303193443566L;
//基本类型long与对象Long使用==比较,Long会转为基本类型,所以相等
System.out.println(id==id2);
//true
Long id3 = 210303193443566L;
//对象与对象判断相等,不能用==,而应该用equals方法
System.out.println(id==id3);
//false
System.out.println(id.equals(id3));
//true
String username = "fly-sky";
String password = "666666";
User user = new User(id,username,password);
//查找与指定id相等的对象
for (User p:getUserList().stream().filter(o->o.getId().equals(id)).collect(Collectors.toList())){
System.out.println(p);
}
//更新对象
System.out.println("result="+updateUser(user));
}
}
3.UserController.java
package com.controller;
import com.modle.User;
import org.springframework.web.bind.annotation.*;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* Restful风格接口的测试
* @Author wongzeng
*/
@ResponseBody
@RestController
//为避免每个方法中都频繁映射/users/...,可直接提取到类上,表示一类资源
@RequestMapping("/users")
public class UserController {
//查询所有用户使用类上映射,uri:/users
@GetMapping
public List<User> getUserByPage(int page, int pageSize){
int beginIndex = (page-1) * pageSize;
int endIndex = page * pageSize;
List<User> users = User.getUserList();
List<User> result = new LinkedList<>();
for(int i=0;i<users.size();i++){
if(i>=beginIndex && i<endIndex){
result.add(users.get(i));
}
}
return result;
}
//添加用户,uri:/users
@PostMapping
public User addUser(String username,String password) {
//生成一个ID:例如210303214107445
Long id = Long.parseLong(new SimpleDateFormat("yyMMddHHmmssSSS").format(new Date()));
User user= new User(id,username,password);
System.out.println(user);
return User.addUser(user)?user:null;
}
//查询指定用户 uri:/users/{id}
@GetMapping("/{id}")
public User getUser(@PathVariable("id") Long id) {
System.out.println("getUser...");
Optional<User> user = User.getUserList().stream().filter(obj->obj.getId().equals(id)).findFirst();
//orElseGet(null)直接会抛异常,而user.orElse(null)不会抛异常而是返回null
System.out.println(user.orElse(null).toString());
return user.orElse(null);
}
//删除用户
@DeleteMapping("/{id}")
public boolean delUser(@PathVariable("id") Long id) {
System.out.println("delUser...");
return User.removeUser(id);
}
//修改用户信息
@PutMapping("/{id}")
public boolean modifyUser(@PathVariable("id")Long id, String username, String password) {
System.out.println("modifyUser...");
System.out.println("id = " + id + ", username = " + username + ", password = " + password);
return User.updateUser(new User(id,username,password));
}
}