本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务
RESTful与API设计&管理(本文)
未完待续......
在之前的文章 服务端模块化架构设计|项目结构与模块化构建思路 中,我们以掘金的部分功能为例,搭建了一个支持模块化的后端服务项目juejin
,其中包含三个模块:juejin-user(用户)
,juejin-pin(沸点)
,juejin-message(消息)
通过添加启动模块来任意组合和扩展功能模块
-
示例1:通过启动模块
juejin-appliaction-system
将juejin-user(用户)
和juejin-message(消息)
合并成一个服务减少服务器资源的消耗,通过启动模块juejin-appliaction-pin
来单独提供juejin-pin(沸点)
模块服务以支持大流量功能模块的精准扩容 -
示例2:通过启动模块
juejin-appliaction-single
将juejin-user(用户)
,juejin-message(消息)
,juejin-pin(沸点)
直接打包成一个单体应用来运行,适合项目前期体量较小的情况
PS:示例基于IDEA + Spring Cloud
为了能更好的理解本专栏中的模块化,建议读者先阅读 服务端模块化架构设计|项目结构与模块化构建思路
写在前面
其实API
这块内容对于模块化来说并没有需要特殊处理的地方,但是API
对于任何项目都是一块需要考量的内容,所以在这里单独用一篇文章来表达我的一些观点和想法
接口开发本身没有什么严格的规定,但是很多公司会有相关的规范,或者团队的开发人员会制定一些统一的规范以避免风格差异过大
当然了,也有完全自由发挥的情况,比如我司,咳咳咳。。。
从功能上来说,请求方法,请求路径,参数格式怎么样都无所谓,只要能和服务端进行数据交互就完全没有问题,但是对于后续的联调和维护而言就会增大难度,而且从视觉上来说也显得极其杂乱,毫无美感
所以我打算讲一讲我较为推荐的做法,当然如果各位对其中的一些看法有异议,也完全可以按照自己的方式来
RESTful
RESTful
想必大家都耳熟能详
什么?你不了解?不行啊小伙子,功课没做到位啊,赶紧花5分钟预习一下
RESTful
的核心其实就是资源,然后通过请求方法来定义增删改查
方法 | URI | 说明 |
---|---|---|
GET | /resources | 列表 |
GET | /resources/:id | 单个 |
POST | /resources | 增 |
PUT | /resources/:id | 改 |
DELETE | /resources/:id | 删 |
它的一个核心驱动理念其实是对所有的接口进行了资源抽象
我们可以这样理解,我们的业务最终还是落到数据库中(严谨的说,是某种数据结构,数据库只是其中的一种载体)
也就是说,我们的平台本质上就是一条一条数据的集合,我们的业务就是增删改查这些数据
所以,把这些数据作为资源进行增删改查,实际上是对数据处理的一种映射化抽象
从我个人的角度上来讲,我觉得这是一种非常 NB 的抽象思路
但是当我们应用到实际开发中时,可能就会觉得。。。duck不必?
比如登录登出功能,我们下意识就会想到/login
和/logout
那用RESTful
的方式呢?
登录就是添加一个会话或令牌,POST /sessions
或是POST /tokens
登出就是删除一个会话或令牌,DELETE /sessions
或是DELETE /tokens
emmm,好像可以理解,但又觉得还不如/login
和/logout
所以RESTful
的处境就比较尴尬,一边一群人普及和推荐使用,一边另一群人吐槽和拒绝使用
辩证看待
其实我们没必要搞得非黑即白、非是即否、非对即错,要么不用要么死搬硬套
打个不那么恰当的比喻,Spring Boot
和Spring Cloud
全家桶里面有那么多模块,我们搭建项目的时候肯定是需要哪个模块依赖哪个模块
怎么到了RESTful
就要全部都按照它的来呢,对吧?
我们很多时候都会进入一个误区,为了做某件事而做某件事,完全忘记我们为什么要做这件事
我们为什么要用RESTful
?
我们的目的是为了能让我们的API
设计出来更统一,更好理解,更好维护,比如:
GET /resources/:id
GET /resources?id=
在我看来这两种方式都没什么问题,只要用的人看得懂,是不是RESTful
又有什么关系呢
所以我们要思考的其实是如何设计出健壮的API
模式而不是要不要用RESTful
或是阿猫模式或是阿狗模式
我们完全可以在RESTful
中借鉴一些可以为我们所用的思想结合到我们平时的开发中
API设计
请求方法
我之前有见过几种不同的规范
全部用POST
这种方式其实直接就舍弃了请求方法所表达的含义,将所有的表达都放到了请求路径上,比如:
//发布沸点
/publishPin
//修改沸点
/updatePin
//删除沸点
/deletePin
//获得沸点列表
/getPinList
//获得沸点分页
/getPinPage
增删改用POST
,查询用GET
这种其实和上面那种没啥区别,就是把查询的用GET
区分了一下,具体的表达还是会放在请求路径上,不过这算是我之前用的比较久的一种方式
RESTful
方式,增POST
|删DELETE
|改PUT
|查GET
RESTful
的方式其实是把动词用请求方法来替代,这样我们的请求路径就只需要名词就行了,这也是我现在比较推荐的方式
//发布沸点
POST /pin
//修改沸点
PUT /pin
//删除沸点
DELETE /pin
//获得沸点列表
GET /pinList
//获得沸点分页
GET /pinPage
请求路径
请求路径一般也有两种情况,带有动词和不带动词,在上面的请求方法中也有示例,我这里就不举例了
然后我是不习惯在请求路径中用大写的,如果一个单词的中间需要大写,我会建议使用中划线来连接,比如工单work-order
而不是workOrder
所以如果全使用POST
的话,我的格式是这样的:
//发布沸点
/pin/publish
//修改沸点
/pin/update
//删除沸点
/pin/delete
//获得沸点列表
/pin/list
//获得沸点分页
/pin/page
如果是RESTful
风格的话:
//发布沸点
POST /pin
//修改沸点
PUT /pin
//删除沸点
DELETE /pin
//获得沸点列表
GET /pin/list
//获得沸点分页
GET /pin/page
我这里用的是单数而不是复数,当然这个大家可以按照自己的习惯来选择
对于查询数据的接口,因为我们可能会需要查询各种各样结构的数据,比如列表,分页,统计等等,所以我会在最后面加上一级查询的类型,这样查询的结构就一目了然
请求参数
在之前很长一段时间中,我会直接用数据模型(也就是数据库中的表结构对应的类)作为接口的参数,因为这样很方便,不用做各层级之间数据结构的转换
但是我现在完全不建议这样做,因为数据模型里面的字段是全量的,当你过三个月再去看这个接口,你就完全不记得前端需要传哪些字段,于是你只能再通过需求,UI
和之前的代码逻辑反推
所以我建议单独创建一个类来作为参数视图,同时使用json body
来接收
比如发布沸点就可以创建一个CreatePinVO
或是PublishPinVO
响应结果
对于返回的响应结果,我建议大家在最外面套一层数据结构,比如:
{
"success": true,
"message": "成功",
"object": null
}
或是
{
"code": 0,
"message": "成功",
"object": null
}
或是
{
"result": "SUCCESS",
"message": "成功",
"object": null
}
这三种的的区别就是,第一种使用布尔值来表示是否成功,第二种使用整数来表示成功或失败或其他情况,第三种使用字符串来表示成功或失败或其他情况
不过我是推荐大家选择第三种,用字符串表示
- 布尔值 vs 字符串
由于布尔类型只能表示成功或失败,当你想要表示更多状态的时候就无法支持,所以字符串有更好的扩展性
- 整数 vs 字符串
整数我们无法一眼就明白其含义,需要额外花费时间去到代码,数据库或其他的地方查找映射结果,而字符串本身就具有表达的能力,所见及所得
API示例
现在我们来给juejin-pin(沸点)
模块设计API
以发布沸点,修改沸点(沸点好像不能修改,那就假装能修改吧,其实我是希望沸点能出一个修改圈子的功能),删除沸点,分页查询沸点这几个功能为例
为了统一,我这里就固定使用Create
,Update
,Delete
,如发布沸点表达为Create Pin
而不用Publish Pin
,当然大家可以按照自己的习惯来
响应结果的包装可以用切面来处理,所以接口中就不体现了
同时结合DDD
中的CQRS
,我们用Command
来表示增删改的入参,用Query
表示查询的入参
- 创建
Controller
添加一个PinController
@RestController
@RequestMapping("/pin")
public class PinController {
}
- 发布沸点
创建一个参数类PinCreateCommand
/**
* 沸点创建命令
*/
@Data
public class PinCreateCommand {
/**
* 沸点内容
*/
private String content;
/**
* 沸点圈子ID
*/
private String clubId;
}
添加发布沸点的接口
@RestController
@RequestMapping("/pin")
public class PinController {
@PostMapping
public void create(@RequestBody PinCreateCommand create) {
}
}
- 修改沸点
假设沸点可以修改圈子,定义一个PinUpdateCommand
/**
* 沸点修改命令
*/
@Data
public class PinUpdateCommand {
/**
* 沸点ID
*/
private String id;
/**
* 沸点圈子ID
*/
private String clubId;
}
添加修改沸点的接口
@RestController
@RequestMapping("/pin")
public class PinController {
@PutMapping
public void update(@RequestBody PinUpdateCommand update) {
}
}
- 删除沸点
我这里为了保持视觉上的统一美感,单独创建了一个PinDeleteCommand
,如果大家觉得麻烦的话也可以直接用id
作为入参
/**
* 沸点删除命令
*/
@Data
public class PinDeleteCommand {
/**
* 沸点ID
*/
private String id;
}
添加删除沸点的接口
@RestController
@RequestMapping("/pin")
public class PinController {
@DeleteMapping
public void delete(@RequestBody PinDeleteCommand delete) {
}
}
- 分页查询沸点
定义沸点视图PinVO
/**
* 沸点视图
*/
@Data
public class PinVO {
/**
* 沸点ID
*/
private String id;
/**
* 沸点内容
*/
private String content;
/**
* 沸点圈子ID
*/
private String clubId;
/**
* 沸点圈子名称
*/
private String clubName;
//点赞和评论的数据先省略
}
定义一个分页参数PageQuery
/**
* 分页参数
*/
@Data
public class PageQuery {
/**
* 当前页
*/
private long current = 1;
/**
* 每页数量
*/
private long size = 10;
}
再定义一个分页视图PageVO
/**
* 分页视图
*
* @param <T> 数据类型
*/
@Data
public class PageVO<T> {
/**
* 数据
*/
private List<T> records = Collections.emptyList();
/**
* 总数
*/
private long total = 0;
/**
* 每页数量
*/
private long size = 10;
/**
* 当前页数
*/
private long current = 1;
/**
* 总页数
*/
private long pages = 1;
}
PageQuery
和PageVO
可以定义在juejin-basic
中,这样其他模块也能复用
添加分页查询沸点的接口
@RestController
@RequestMapping("/pin")
public class PinController {
@GetMapping("/page")
public PageVO<PinVO> page(PageQuery page) {
return new PageVO<>();
}
}
最终呈现
怎么样,是不是看着很简洁很舒服,而且功能一目了然
API管理
当我们开发完接口之后,肯定需要提供一个文档给前端用于联调
总不会有人直接用口头描述吧(难以置信.jpg)
Swagger
Swagger
作为最常用的接口规范,目前最新的版本为OpenAPI3
也有很多库实现了OpenAPI3
提供接口管理等功能
先在接口和实体类上添加注解,需要依赖io.swagger.core.v3
,不过很多库都自动集成了,所以任意配置一个库就行了
新的注解和旧的注解是不一样的,如果大家想迁移到OpenAPI3
的注解,可以参考下面这张表
Swagger2 | Swagger3 |
---|---|
@Api | @Tag |
@ApiOperation | @Operation |
@ApiImplicitParams | @Parameters |
@ApiImplicitParam | @Parameter |
@ApiParam | @Parameter |
@ApiIgnore | @Parameter(hidden = true) @Operation(hidden = true) @Hidden |
@ApiModel | @Schema |
@ApiModelProperty | @Schema |
@ApiResponse | @ApiResponse |
Springfox
Springfox
是比较早的Swagger
实现库,不过现在已经不太更新了
集成依赖
implementation 'io.springfox:springfox-boot-starter:3.0.0'
配置API
@EnableOpenApi
@Configuration
public class PinConfiguration {
@Bean
public Docket pinApi() {
return new Docket(DocumentationType.OAS_30)
.groupName("沸点")
.select()
.apis(RequestHandlerSelectors.basePackage("com.bytedance.juejin.pin"))
.paths(PathSelectors.any())
.build();
}
}
如果是2.6+
版本的Spring Boot
需要额外添加配置
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
启动项目,输入/swagger-ui/index.html
SpringDoc
由于Springfox
已经不太维护了,所以越来越多的人从Springfox
迁移到SpringDoc
集成依赖
implementation 'org.springdoc:springdoc-openapi-ui:1.6.11'
配置API
@Configuration
public class PinConfiguration {
@Bean
public GroupedOpenApi pinApi() {
return GroupedOpenApi.builder()
.group("沸点")
.packagesToScan("com.bytedance.juejin.pin")
.build();
}
}
启动项目,同样也是输入/swagger-ui/index.html
Knife4j
如果大家不喜欢Swagger
自带的页面样式的话,可以集成Knife4j
Springfox
+Knife4j
如果我们集成的是Springfox
,在配置完成之后,额外引入:
implementation 'com.github.shijingsh:knife4j-spring-boot-starter:3.0.5'
SpringDoc
+Knife4j
如果我们集成的是SpringDoc
,在配置完成之后,额外引入:
implementation 'com.github.shijingsh:knife4j-springdoc-ui:3.0.5'
启动项目,输入/doc.html
对使用API管理工具的建议
现在还有很多API
管理工具,都是在线服务或是需要单独部署服务
你可以用这些工具提供的测试功能,Mock
功能,文档导出功能等等,但是我不建议在这些工具上扩展文档内容
很多的API
管理工具都是支持Swagger
导入的,我们只用导入功能就好了
如果文档需要修改,那就在Swagger
的注解中修改然后再同步一次,这样我们可以任意替换更适合我们的API
管理工具,如果这个用了不好用,那就换一个试试,成本不大
而一旦我们在API
管理工具上添加了额外的文档内容,当我们想要更换API
管理工具时,你就要考虑如何迁移之前的文档内容或者是为了这一个项目单独维护整个API
管理工具的服务,最后项目越来越多,你只能维护一大堆各种各样的API
管理工具的服务
另外还会存在一些情况,比如在修改了接口之后忘记或者懒得去文档上进行修改,几个版本下来,文档和接口就对不上了,虽然这和公司的规范有关,但是不得不承认很多小公司还是容易发生这种情况
总结
不管是设计API
还是管理API
,都是为了更好的维护API
,所以不要被条条框框所限制
辩证的看待我们所接触到的思想和技术,看看能否取其精华去其糟粕,将其中的优势应用到我们的工作当中