服务端模块化架构设计|RESTful 与 API 设计 & 管理

2,910 阅读13分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务

项目结构与模块化构建思路

RESTful与API设计&管理(本文)

网关路由模块化支持与条件配置

DDD领域驱动设计与业务模块化(概念与理解)

DDD领域驱动设计与业务模块化(落地与实现)

DDD领域驱动设计与业务模块化(薛定谔模型)

DDD领域驱动设计与业务模块化(优化与重构)

RPC模块化设计与分布式事务

v2.0:项目结构优化升级

v2.0:项目构建+代码生成「插件篇」

v2.0:扩展模块实现技术解耦

v2.0:结合DDD与MVC的中庸之道(启发与思路)

v2.0:结合DDD与MVC的中庸之道(标准与实现)

v2.0:结合DDD与MVC的中庸之道(优化与插件)

未完待续......

在之前的文章 服务端模块化架构设计|项目结构与模块化构建思路 中,我们以掘金的部分功能为例,搭建了一个支持模块化的后端服务项目juejin,其中包含三个模块:juejin-user(用户)juejin-pin(沸点)juejin-message(消息)

通过添加启动模块来任意组合和扩展功能模块

  • 示例1:通过启动模块juejin-appliaction-systemjuejin-user(用户)juejin-message(消息)合并成一个服务减少服务器资源的消耗,通过启动模块juejin-appliaction-pin来单独提供juejin-pin(沸点)模块服务以支持大流量功能模块的精准扩容

  • 示例2:通过启动模块juejin-appliaction-singlejuejin-user(用户)juejin-message(消息)juejin-pin(沸点)直接打包成一个单体应用来运行,适合项目前期体量较小的情况

PS:示例基于IDEA + Spring Cloud

模块化项目结构.jpg

为了能更好的理解本专栏中的模块化,建议读者先阅读 服务端模块化架构设计|项目结构与模块化构建思路

写在前面

其实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 BootSpring 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

以发布沸点,修改沸点(沸点好像不能修改,那就假装能修改吧,其实我是希望沸点能出一个修改圈子的功能),删除沸点,分页查询沸点这几个功能为例

为了统一,我这里就固定使用CreateUpdateDelete,如发布沸点表达为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;
}

PageQueryPageVO可以定义在juejin-basic中,这样其他模块也能复用

添加分页查询沸点的接口

@RestController
@RequestMapping("/pin")
public class PinController {

    @GetMapping("/page")
    public PageVO<PinVO> page(PageQuery page) {
        return new PageVO<>();
    }
}

最终呈现

沸点API设计.jpg

怎么样,是不是看着很简洁很舒服,而且功能一目了然

API管理

当我们开发完接口之后,肯定需要提供一个文档给前端用于联调

总不会有人直接用口头描述吧(难以置信.jpg)

Swagger

Swagger作为最常用的接口规范,目前最新的版本为OpenAPI3

也有很多库实现了OpenAPI3提供接口管理等功能

先在接口和实体类上添加注解,需要依赖io.swagger.core.v3,不过很多库都自动集成了,所以任意配置一个库就行了

OpenAPI注解.jpg

视图注解.jpg

新的注解和旧的注解是不一样的,如果大家想迁移到OpenAPI3的注解,可以参考下面这张表

Swagger2Swagger3
@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

Swagger.jpg

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

SpringDoc.jpg

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

Knife4j.jpg

对使用API管理工具的建议

现在还有很多API管理工具,都是在线服务或是需要单独部署服务

你可以用这些工具提供的测试功能,Mock功能,文档导出功能等等,但是我不建议在这些工具上扩展文档内容

很多的API管理工具都是支持Swagger导入的,我们只用导入功能就好了

如果文档需要修改,那就在Swagger的注解中修改然后再同步一次,这样我们可以任意替换更适合我们的API管理工具,如果这个用了不好用,那就换一个试试,成本不大

而一旦我们在API管理工具上添加了额外的文档内容,当我们想要更换API管理工具时,你就要考虑如何迁移之前的文档内容或者是为了这一个项目单独维护整个API管理工具的服务,最后项目越来越多,你只能维护一大堆各种各样的API管理工具的服务

另外还会存在一些情况,比如在修改了接口之后忘记或者懒得去文档上进行修改,几个版本下来,文档和接口就对不上了,虽然这和公司的规范有关,但是不得不承认很多小公司还是容易发生这种情况

总结

不管是设计API还是管理API,都是为了更好的维护API,所以不要被条条框框所限制

辩证的看待我们所接触到的思想和技术,看看能否取其精华去其糟粕,将其中的优势应用到我们的工作当中

源码

上一篇:项目结构与模块化构建思路

下一篇:网关路由模块化支持与条件配置