近年来移动互联网的发展,前端设备层出不穷(手机、平板、桌面电脑、其他专用设备…),因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信,于是 RESTful 火了,它可以通过一套统一的接口为 Web,iOS和Android提供服务。
1. RESTful API 规范
协议
域名
应该尽量将API部署在专用域名之下。
https://api.example.com
如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。
https://example.org/api/
版本
将 API 的版本号放入 URL。https://api.example.com/v1/
路径
在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,API中的名词建议使用复数。
举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。
- https://api.example.com/v1/zoos
- https://api.example.com/v1/animals
https://api.example.com/v1/employees
HTTP动词
对于资源的具体操作类型,由HTTP动词表示。
常用的 HTTP 动词有下面五个(括号里是对应的SQL命令)。GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源。
- DELETE(DELETE):从服务器删除资源。
下面是一些例子。
- GET /v1/zoos:列出所有动物园
- POST /v1/zoos:新建一个动物园
- GET /v1/zoos/id:获取某个指定动物园的信息
- PUT /v1/zoos/id:更新某个指定动物园的信息(提供该动物园的全部信息)
- DELETE /v1/zoos/id:删除某个动物园
- GET /v1/zoos/id/animals:列出某个指定动物园的所有动物
DELETE /v1/zoos/id/animals/id:删除某个指定动物园的指定动物
过滤信息
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。
下面是一些常见的参数。?limit=10:指定返回记录的数量
- ?offset=10:指定返回记录的开始位置。
- ?page=2&per_page=100:指定第几页,以及每页的记录数。
- ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
- ?animal_type_id=1:指定筛选条件
参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。
比如,GET /v1/zoo/id/animals 与 GET /animals?zoo_id=id的含义是相同的。
状态码
服务器向用户返回的状态码和提示信息,常见的有以下:
- 200 OK - [GET]:服务器成功返回用户请求的数据。
- 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
- 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
- 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作。
- 500 INTERNAL SERVER ERROR - [*]:服务器发生错误。
错误处理
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{ error: “Invalid API key” }2. 公共接口规范
公共请求头
| 名称 | 必填 | 说明 | | —- | —- | —- | | Authorization | 否 | 用于验证请求合法性的认证信息。
使用场景:非匿名请求 |
公共响应头
头部名称 | 必填 | 说明 |
---|---|---|
Content-Type | 是 | 正常情况下该值将被设为 application/json,表示返回 JSON 格式的文本信息。 |
成功响应
成功响应格式
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 200,
"msg": "",
"data":
}
错误响应
当您请求出现错误时,响应头部信息包括:
- Content-Type: application/json
- 一个合适的 3xx,4xx,或者 5xx 的 HTTP 状态码
错误响应格式:
HTTP/1.1 状态码 信息
Content-Type: application/json
{
"code": <httpCode int>,
"error": "<errMsg string>"
}
响应状态码
HTTP状态码 | 说明 |
---|---|
200 | 请求成功 |
401 | 认证授权失败 |
403 | 权限不足,拒绝访问。 |
404 | 资源不存在 包括空间资源不存在;镜像源资源不存在。 |
405 | 请求方式错误 主要指非预期的请求方式。 |
自定义的其它错误码…. |
设计一组 RESTful API 完成用户信息的维护。
请求类型 | URL | 说明 |
---|---|---|
GET | /api/sys/users | 查询用户列表 |
POST | /api/sys/users | 创建用户 |
GET | /api/sys/users/{id} | 根据 id 查询用户 |
PUT | /api/sys/users/{id} | 根据 id 更新用户 |
DELETE | /api/sys/users/{id} | 根据 id 删除用户 |
3. 查询用户接口
描述
请求
请求语法
GET /api/sys/users HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: <AccessToken>
请求参数
参数名称 | 必填 | 说明 |
---|---|---|
name | 否 | 根据用户名模糊查询用户 |
否 | 根据用户邮箱模糊查询用户 | |
pageNum | 否 | 当前页 |
pageSize | 否 | 一页显示多少条 |
请求头
请求内容
响应
响应语法
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 200,
"msg": "查询用户列表成功",
"data": {}
}
响应头
响应内容
名称 | 必填 | 说明 |
---|---|---|
id | 是 | 用户id |
name | 是 | 用户名 |
age | 否 | 年龄 |
否 | 邮箱 |
响应状态码
该操作的实现不会返回特殊错误。有关错误和错误代码列表的一般信息。
示例
请求示例
GET /api/sys/users?name=jack&pageSize=10&pageNum=1 HTTP/1.1
Authorization: j853F3bLkWl59I5BOkWm6q1Z1mZClpr9Z9CLfDE0
响应示例
{
"msg": "请求成功",
"current": 1,
"total": 7,
"code": 200,
"size": 10,
"data": [
{
"id": 1,
"name": "jack",
"age": 18,
"email": "jack@126.com"
},
....
]
}
4. 新增用户接口
描述
请求
请求语法
POST /api/sys/users HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: <AccessToken>
请求参数
参数名称 | 必填 | 说明 |
---|---|---|
name | 是 | 用户名 |
是 | 邮箱 | |
age | 是 | 年龄 |
请求头
请求内容
响应
响应语法
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 200,
"msg": "新增用户成功"
}
响应头
响应内容
响应状态码
该操作的实现不会返回特殊错误。有关错误和错误代码列表的一般信息。
示例
请求示例
POST /api/sys/users HTTP/1.1
Authorization: j853F3bLkWl59I5BOkWm6q1Z1mZClpr9Z9CLfDE0
响应示例
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 200,
"msg": "新增用户成功",
"data": {}
}
5. 实现接口
引入依赖
<!-- springmvc 、servlet 、tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
公共类库
响应请求对象
package com.imcode.common.vo;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springframework.http.HttpStatus;
import java.util.concurrent.ConcurrentHashMap;
/**
* 响应对象
*/
public class Result extends ConcurrentHashMap<String, Object> {
public Result() {
this.put("code", HttpStatus.OK.value());
this.put("msg", "请求成功");
}
public static Result ok() {
return new Result();
}
public static Result ok(String msg) {
return Result.ok().put("msg", msg);
}
public static Result ok(Object data) {
return Result.ok().put("data", data);
}
public static Result ok(IPage<?> page) {
return Result.ok()
// 当前页数据
.put("data", page.getRecords())
// 当前页面码
.put("current", page.getCurrent())
// 一页显示多少条
.put("size", page.getSize())
// 满足查询条件的总记录数
.put("total", page.getTotal());
}
public static Result ok(String msg, Object data) {
return Result.ok(msg).put("data", data);
}
public static Result error(HttpStatus status, String error) {
return Result.ok()
.put("code", status)
.put("error", error)
.put("msg", "");
}
@Override
public Result put(String key, Object value) {
super.put(key, value);
return this;
}
}
分页对象
package com.imcode.common.vo;
import lombok.Data;
@Data
public class PageVO {
// 当前页码
private Integer current = 1;
// 每页显示多少条
private Integer size = 10;
}
用户接口
查询条件对象
package com.imcode.sys.vo;
import com.imcode.common.vo.PageVO;
import lombok.Data;
@Data
public class UserQuery extends PageVO {
private String name;
private String email;
}
实现查询接口
package com.imcode.sys.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imcode.common.vo.Result;
import com.imcode.sys.entity.User;
import com.imcode.sys.service.UserService;
import com.imcode.sys.vo.UserQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* </p>
*
* @author jack
* @since 2022-07-02
*/
@RestController
@RequestMapping("/api/sys")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public Result list(UserQuery param) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasLength(query.getName()), User::getName, query.getName());
wrapper.like(StringUtils.hasLength(query.getEmail()), User::getEmail, query.getEmail());
IPage<User> page =
userService.page(new Page<>(param.getCurrent(), param.getSize()), wrapper);
return Result.ok(page);
}
@PostMapping("/users")
public Result save(User user) {
userService.save(user);
return Result.ok("新增用户成功");
}
}
没有前端,如何测试?API文档和测试工具
6. Swagger 构建 API 文档
Swagger 介绍
- 轻松构建 RESTful API 文档。
- 文档和代码融合,修改代码逻辑的同时方便更新文档说明。
- 提供 API 接口调试功能。
官方界面有点丑,移步knife4j:https://doc.xiaominfo.com/
引入依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
配置类
package com.imcode.config;
import io.swagger.annotations.ApiOperation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {
@Bean
public Docket defaultApi() {
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("XX系统API接口")
.description("在线API文档")
.version("1.0")
.build())
.select()
//.apis(RequestHandlerSelectors.basePackage("com.imcode"))
.apis(RequestHandlerSelectors
.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
return docket;
}
}
重启应用,访问:http://localhost:8080/doc.html
如果报错,添加如下配置:
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
添加文档内容
控制器
@Api(tags = "用户模块")
@RestController
@RequestMapping("/api/sys")
public class UserController {
@Autowired
private UserService userService;
@ApiOperation(value = "分页查询")
@GetMapping("/users")
public Result list(UserQuery param) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(param.getName() != null, User::getName, param.getName());
wrapper.like(param.getEmail() != null, User::getEmail, param.getEmail());
IPage<User> page = userService.page(new Page<>(param.getCurrent(), param.getSize()), wrapper);
return Result.ok(page);
}
@ApiOperation(value = "新增用户")
@PostMapping("/users")
public Result save(User user) {
userService.save(user);
return Result.ok("新增用户成功");
}
}
实体类
@Getter
@Setter
@TableName("t_user")
@ApiModel(description="用户信息")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty("主键")
private Integer id;
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("年龄")
private Integer age;
@ApiModelProperty("邮箱")
private String email;
}
VO类
@Data
@ApiModel(description="用户查询条件")
public class UserQuery extends PageVO {
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("邮箱")
private String email;
}
@Data
@ApiModel(description="分页信息")
public class PageVO {
// 当前页码
@ApiModelProperty("页码")
private Integer current = 1;
// 一页显示多少条
@ApiModelProperty("一页显示多少条")
private Integer size = 10;
}
常用注解
@Api
:修饰整个类,描述Controller的作用;@ApiOperation
:描述一个类的一个方法,或者说一个接口;@ApiParam
:单个参数描述;用在方法参数上@ApiModel
:用对象来接收参数;@ApiProperty
:用对象接收参数时,描述对象的一个字段;@ApiIgnore
:使用该注解忽略这个API;@ApiImplicitParam
:一个请求参数;用在方法上@ApiImplicitParams
:多个请求参数。用在方法上-
参考示例
@Api(value = "MemberApi", description = "用户登录、注册、账户合法性校验") @RestController @RequestMapping("/api/member") public class MemberApi { /** * 会员注册 * @param * @return */ @ApiOperation(value = "会员注册", notes = "网站会员注册") @ApiImplicitParam(name = "member", value = "会员详细信息", dataType = "Member") @PostMapping("/register") public R register(@RequestBody Member member) { try { return R.success(); } catch (Exception e) { e.printStackTrace(); return R.error(e.getMessage()); } } /** * 登录 * @param * @return */ @ApiOperation(value = "会员登录", notes = "网站会员登录") @ApiImplicitParams({ @ApiImplicitParam(name = "account", value = "会员帐号", required = true, paramType = "query", dataType = "String"), @ApiImplicitParam(name = "password", value = "会员密码", required = true, paramType = "query", dataType = "String") }) @PostMapping("/login") public R login(String account, String password) { try { return R.success(); } catch (Exception e) { e.printStackTrace(); return R.error(e.getMessage()); } } /** * 检查帐号是否存在 * * @param * @return */ @ApiOperation(value = "检查帐号是否存在", notes = "根检查帐号是否存在") @GetMapping("/checkaccount/{account}") public R checkAccount(@ApiParam(name = "account",required = true,value = "用户帐号") @PathVariable String account) { try { return R.success(); } catch (Exception e) { e.printStackTrace(); return R.error(e.getMessage()); } } }
7. ApiPost
官方文档地址:https://wiki.apipost.cn/