项目笔记(四)
一、项目概述
本项目是一个面向开发者的 API 平台,提供 API 接口供开发者调用。用户通过注册登录,可以开通接口调用权限,并可以浏览和调用接口。每次调用都会进行统计,用户可以根据统计数据进行分析和优化。管理员可以发布接口、下线接口、接入接口,并可视化接口的调用情况和数据。本项目侧重于后端,涉及多种编程技巧和架构设计层面的知识。
三、项目源码
四、本期时间点
五、本期计划(时间点 03:38-06:59)
- 开发接口调用次数的统计 20 min
- 优化整个系统的架构 - (API 网关) 60 min
-
- 网关是什么?
- 网关的作用?
- 网关的应用场景及实现?
- 结合业务去应用网关
六、开发接口调用次数的统计(时间点 06:59-01:08:28)
1. 思考(时间点 07:56-09:25)
我们的后端项目现在有 yuapi-backend、yuapi-interface,为什么不在一个工程呢?
因为他们本来就不应该在一个工程。举个例子,假设你们团队中有一个负责开发平台的团队,还有一个负责提供接口的团队。这两个团队有必要将项目放在同一个目录下吗?其实并没有必要。因为我们的平台可以接入任何同学开发的接口,不一定是同一个团队或者公司内部的项目。而且你可以将设计的范围扩大一点,除了提供给用户使用,还可以帮助开发者实现变现。例如,我开发了一个图片接口,我可以将我的接口接入到你的平台上,然后向其他用户提供服务,从中获取收益。当然,这样的系统规模会更大一些。但是对于缺乏企业开发经验的同学来说,不建议去做这样的扩展。
这样做涉及到的安全性和不确定性太多了。你需要审核对方的接口,但你无法确定他们是否会给你带来问题,例如泄露敏感信息或敏感数据。因此,在开发网站时,我们需要考虑的不仅仅是技术方面,更多的是业务方面和合法合规性。尤其是在公司内部开发这样的系统时,需要格外小心。
2. 接口调用次数的业务流程讲解(时间点 10:29-15:04)
需求:
- 用户每次调用接口成功,次数 + 1
- 给用户分配或者用户自主申请接口调用次数
业务流程:
- 用户调用接口(之前已完成)
- 修改数据库,调用次数 +1
进一步说明:
让我们思考一下关于接口调用次数统计的功能。从需求出发,我们需要考虑这个功能应该具有怎样的流程。需求是每次用户成功调用接口,次数加 1;如果是由于网络原因导致的失败,而不是参数错误等问题,也应该算作成功。所以这是我们的一个需求,用户调用接口后次数加 1。但是需求写得越明确,我们在进行设计时就会更清晰。
接下来让我们讨论业务流程。实际上,整个接口调用次数加 1 的业务流程是嵌入在我们调用接口的业务流程中的。我们还是参考之前第一期绘制的业务流程图:
首先用户在前端看到接口,然后他要开通接口获取调用次数,获取调用次数后发起调用。通常情况下,我们只需完成调用即可,但现在在调用之后需要额外进行一次统计,即增加次数记录,这个步骤实际上是在进行统计次数的位置。我们只需要在接口调用成功后,在数据库中保存它的接口调用次数加 1 即可。
实际上,这是一个相当简单的流程。在原有的调用接口成功基础上,再添加一个步骤,即在统计次数的位置进行记录。大致就是这样。
同时,刚刚提到了一个额外的需求。既然每次接口调用成功次数都加 1,那么我们是否可以考虑给用户分配或者用户自主申请接口调用次数。这个需求是后续的需求,我们的核心是先实现接口调用次数的统计。因为如果不进行统计,即使给他们分配这些次数也没有多大意义和用处。即使不限制次数,统计接口调用次数仍然是有用的。如果有人对你发起攻击,刷了 100 万次,你能追踪出是谁吗?所以,我们的核心是统计这个次数。
💡 有同学说:不应该是减 1 吗?用户买了 1000,每次减 1。
对于加 1 或减 1 的问题,实际上可以根据你的需求进行选择,这是灵活的。如果你打算进行计费,无论是加 1 还是减 1 都可以判断调用次数是否达到了限额或超出了限额。你可以记录上一次开通和当前开通的调用次数,以及总的限额,从而计算出当前的调用次数。加 1 的话,可以从 0 增加到 1 万,根据你的业务逻辑进行操作。
减 1 也是可以的,但是减 1 会带来一个问题,那就是你好像无法记录总的调用次数,可能需要再添加一个字段。实际上,这个取决于你的具体需求和业务逻辑。在这方面,每个人都可以有自己的思考,不必完全按照🐟的方式来。🐟只是按照他的业务流程给大家提供了一个参考,大家可以根据自己的情况来决定,不用纠结这些。
3. 设计库表(时间点 15:04-21:33)
接下来,我们需要设计数据库表结构以满足需求。既然每次调用接口成功都要加 1 次数,那我们需要区分是哪个用户调用了哪个接口,这意味着用户和接口之间存在一种关系。根据需求分析,这是一个多对多的关系,因为一个用户可以调用多个接口,而一个接口也可以被多个用户调用。
因此,我们需要设计一个新的表来存储用户和接口之间的关系,可以称之为"用户调用接口关系表"。
ps.这也不是分表,分表我们是把一个数据量级很大的表给它拆成多个小表。
-- 用户调用接口关系表 create table if not exists yuapi.`user_interface_info` ( `id` bigint not null auto_increment comment '主键' primary key, `userId` bigint not null comment '调用用户 id', `interfaceInfoId` bigint not null comment '接口 id', `totalNum` int default 0 not null comment '总调用次数', `leftNum` int default 0 not null comment '剩余调用次数', `status` int default 0 not null comment '0-正常,1-禁用', `createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间', `updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', `isDelete` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)' ) comment '用户调用接口关系';
💡 有同学提到了调用时间,如果我们还要在数据库中添加调用时间字段的话,那会更加复杂。不建议将调用时间直接写入数据库。原因是,如果每个用户每次调用接口都要在数据库中新增一条数据,那么数据库表可能会变得非常庞大。建议使用日志来存储这些调用信息,可以将其记录在文件中,使用类似 ELK 等工具进行日志存储和分析。这样,我们不用将这些调用信息直接存储在数据库中。
当然,是否使用日志存储还要考虑系统的规模和量级。如果你的系统非常庞大,每天有大量的接口调用记录产生,那么使用日志存储是更为合适的选择。日志存储可以更好地处理大量的数据,并提供更强大的查询和分析功能。
综上所述,根据系统的量级和需求,我们可以选择使用日志存储来记录接口调用信息,而不是直接存储在数据库中。这样可以更好地管理和分析接口调用数据。
**总调用次数:**指用户从第一次开通接口开始至今累计的调用次数。
**剩余调用次数:**指用户每次购买接口后剩余的可调用次数。
**状态字段(status):**决定是否允许其调用特定接口。
- 总调用次数是一直累加的,记录了用户在整个使用期间的累计调用次数。无论用户购买多少次接口,总调用次数都会随着每次调用的增加而增加。
- 剩余调用次数则是在用户购买接口后,根据购买的次数和已使用的次数计算得出的。每次购买接口都会增加一定的调用次数,而每次实际调用接口后,剩余调用次数会相应减少。
- 为了增加安全性,我们可以考虑为每个用户设置一个状态字段(status),来决定是否允许其调用特定接口。这样,如果用户触发了某些规则或违反了规定,我们可以将其状态设置为不允许调用该接口。通过添加一个状态字段,我们可以灵活地管理用户对接口的访问权限。例如,当用户违反规则时,我们可以限制其对某些接口的调用,而对其他接口仍然保持开放。这个状态字段可以帮助我们实现精确的接口访问控制。
4. 创建表(时间点 21:33-22:30)
打开后端项目 yuapi-backend,把建表语句粘贴到 db.sql 中。
鼠标左键从上往下选中建表语句,然后点击鼠标右键 → Execute。
点击 New Session。
出现 @localhost,那就选择 @localhost(本地的数据库) → New Session。
表就创建成功了。
5. mybatisX 插件生成代码(时间点 22:30-35:44)
5.1 mybatisX 插件使用(时间点 22:30-27:17)
表创建完成之后,现在去生成增删改查代码;
鼠标右键 user_interface_info 表 → MybatisX-Generator。
模块路径选择当前路径即可,它会生成到 src/main/java 下,包名为generator。
点击Next。
如果发现点击 Next 出现的还是和上图一样,就点击Previous(往前一步),再点击 Next(下一步)。
ps.这个插件有一点 bug ╮(╯▽╰)╭
如下图所示选择后,点击Finish。
ps.之前没有 model,应该是更新了。
多了一个 generator包,代码都生成在这里。
然后给实体对象中的 isDelete 加上 @TableLogic,表示是逻辑删除的字段。
5.2 迁移生成的代码(时间点 23:17-25:41)
把 UserInterfaceInfo.java。
拖到entity包下。
把 UserInterfaceInfoMapper.java。
拖到mapper包下。
把 UserInterfaceInfoServiceImpl.java。
拖到impl包下。
把 UserInterfaceInfoService.java。
拖到service包下。
迁移完了,把generator包删除,鼠标右键generator包 → Delete。
这里还生成了 UserInterfaceInfoMapper.xml。
然后我们再写一下 controller,复制 InterfaceInfoController.java;
粘贴到controller包下,并重命名为 UserInterfaceInfoController。
按[Ctrl+R]替换,把 interfaceInfo 替换成userInterfaceInfo。
把 InterfaceInfo 替换成UserInterfaceInfo。
现在替换的东西,就是把原来对于其他类的增删改查替换成基于现在这个类的增删改查。
5.3 修改代码逻辑(时间点 25:41-35:44)
为了确保用户每次调用接口成功时次数加一,而不是随意增加某个接口的调用次数,我们应该将增加调用次数的逻辑放在接口调用成功的业务逻辑之后,而不是将其对外暴露。
通过这样的设计,我们可以控制用户只能通过正确的接口调用来增加次数,而不允许随意调用接口进行次数操作。这样可以保证接口调用的准确性和安全性,防止滥用和不当操作。
对于这个添加功能,也可以给它添加管理员权限。我们先完整地开发这个 controller,保留添加功能。这样,如果将来需要扩展功能,也会更方便一些。
像这种更新接口调用关系方面,可能在将来需要这样的功能。例如,管理员需要为用户增加接口调用次数或者分配额外的次数。
去开发添加用户接口调用关系的请求,把这些请求类都建好。
在 dto 包下新建userinterfaceinfo包。
复制 interfaceinfo 包下的添加、查询、更新请求。
粘贴进 userinterfaceinfo 包下。
把 userinterfaceinfo 包下的三个请求名前面加上 User。
💡 有按用户限制 QPS 的思路吗?
很简单,既然我们已经能够统计用户调用接口的次数,我们再统计用户调用接口的调用时间,然后根据用户在某一个时间段内的调用次数来限制。例如,我们可以每隔一秒统计一次用户的接口调用次数,如果用户在这一秒内的调用次数超过了某个设定的频率,那么我们就给他禁用接口调用。
把添加请求的字段改成 UserInterfaceInfo 的字段;
将 UserInterfaceInfoAddRequest 内的字段全删掉。
复制 UserInterfaceInfo 的字段。
想一下如果是管理员,他要去给用户开通内的接口调用关系,接口调用次数的话,需要他填写什么呢?
- 用户 id,知道哪个用户开通
- 接口 id
- 总调用次数
- 剩余调用次数
- 状态,状态默认是正常(所以可以不用填)
粘贴到 UserInterfaceInfoAddRequest。
把查询请求的字段改成 UserInterfaceInfo 的字段;
将 UserInterfaceInfoQueryRequest 内的字段全删掉。
复制 UserInterfaceInfo 的字段。
管理员会根据哪些字段查询用户和接口的调用关系呢?
- id,根据 id 查询,但次数可能比较少,保留一下
- 用户 id,查某个用户开通哪些接口的调用权限
- 接口 id,这个接口有哪些用户调用
- 总调用次数,可以留着,一般是用范围查询
- 剩余调用次数
- 状态,常用的查询的状态
粘贴到 UserInterfaceInfoQueryRequest。
把更新请求的字段改成 UserInterfaceInfo 的字段;
将 UserInterfaceInfoUpdateRequest 内的字段全删掉。
复制 UserInterfaceInfo 的字段。
管理员会去修改用户接口调用关系的哪些内容呢?
- id,更新必须指定一条数据
- 用户 id(不会改这个,都给用户开通,再改用户就不合理了)
- 总调用次数
- 剩余调用次数
- 状态
粘贴到 UserInterfaceInfoUpdateRequest。
回到 UserInterfaceInfoController 中,导入相应的请求。
然后这里面还有下线、发布接口、测试调用,我们的接口管理不需要这些,删掉。
分页获取列表时,也不用设置描述,删掉。
我们还需要补充这个方法,即使是管理员也需要进行一些校验,确保管理员填写的信息是有效的。
复制之前现成的方法。
粘贴到 UserInterfaceInfoService。
按[Ctrl+R]替换,把 InterfaceInfo 替换成UserInterfaceInfo。
把 interfaceInfo 替换成userInterfaceInfo。
把 UserUserInterfaceInfo 改成 UserInterfaceInfo,删掉多余的内容。
修改后:
然后复制实现类。
粘贴到 UserInterfaceInfoServiceImpl。
改一下名称。
之前是校验接口名称,但我们这个表里没有接口字段,就校验它的次数即可。
回到 UserInterfaceInfoController,把多余的内容去掉。
给这些接口加上管理员权限,这里使用🐟开发的注解,非管理员不能随便添加调用次数。
非管理员不能随便删除调用次数。
非管理员不能随便更新调用次数。
根据 id 获取、获取列表、分页获取列表都是管理员才能用。
OK~ 基本的东西完成了。
6. 实现用户调用成功次数加一(时间点 35:44-52:20)
6.1 调用成功(时间点 35:44-38:10)
接下来要做一个事情,就是用户每次调用接口成功次数要加一。
找到用户调用接口成功的位置。大家还记得我们之前用户调用接口的逻辑是写在哪里了吗?
应该是在 yuapi-interface 项目里。
我们之前校验用户是否有权限调用,都直接写在了模拟接口项目里。
现在我们还是优先写在这里,把整个业务逻辑写完,然后再去做优化。实际上,在这个位置统计调用次数并不太合理,等会儿说为什么。
大家还记得上次我们开发的这段代码吗?一个模拟接口,根据用户名获取字符串,获取用户的名字。这个接口很简单,我们在这里判断了用户是否有权限来调用它。
这个地方原本要返回的用户的值。
修改一下。
到这一步已经调用成功了,接下来就补充,当它调用成功之后,次数加一。
我们要做的事情是什么?是不是就是去调用我们的 userInterfaceInfoService.save,把这条调用记录添加到数据库中,或者调用 updateUserInterfaceInfo 在原有的调用次数的统计之上再加一。
所以我们可以发现有两种情况:
- 第一种情况是用户没有这个调用次数记录,那么我们需要创建一条新的记录。
- 第二种情况是用户已经有了调用次数记录,我们需要在现有的次数基础上加 1。
现在我们要开发调用次数加一的功能。之前的updateUserInterfaceInfo方法是以管理员的视角去更新调用次数记录,但并没有包含调用次数加一的逻辑,所以我们需要在这里进行开发。
6.2 步骤(时间点 38:10-38:41)
- 开发基本增删改查(给管理员用)
- 开发用户调用接口次数 +1 的功能(service)
6.3 实现调用接口次数加一(时间点 38:41-52:20)
之前我们曾一起开发过伙伴匹配系统,其中有一个功能是加入队伍。加入队伍的流程与现在的调用次数加一非常相似,都是将当前数值加一的逻辑。
在 UserInterfaceInfoService 中补充次数加一的功能。
光标放到 invokeCount 中,按[Alt + Enter],选择Implement method 'incokeCount'。
实现它。
这里我们开始写业务逻辑。
@Override public boolean invokeCount(long interfaceInfoId, long userId) { // 判断(其实这里还应该校验存不存在,这里就不用校验了,因为它不存在,也更新不到那条记录) if (interfaceInfoId <= 0 || userId <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 使用 UpdateWrapper 对象来构建更新条件 UpdateWrapper<UserInterfaceInfo> updateWrapper = new UpdateWrapper<>(); // 在 updateWrapper 中设置了两个条件:interfaceInfoId 等于给定的 interfaceInfoId 和 userId 等于给定的 userId。 updateWrapper.eq("interfaceInfoId", interfaceInfoId); updateWrapper.eq("userId", userId); // setSql 方法用于设置要更新的 SQL 语句。这里通过 SQL 表达式实现了两个字段的更新操作: // leftNum=leftNum-1和totalNum=totalNum+1。意思是将leftNum字段减一,totalNum字段加一。 updateWrapper.setSql("leftNum = leftNum - 1, totalNum = totalNum + 1"); // 最后,调用update方法执行更新操作,并返回更新是否成功的结果 return this.update(updateWrapper); }
在这里需要注意的是,由于用户可能会瞬间调用大量接口次数,为了避免统计出错,需要涉及到事务和锁的知识。在这种情况下,如果我们是在分布式环境中运行的,那么可能需要使用分布式锁来保证数据的一致性。
事务是一组操作的集合,要么全部成功,要么全部失败回滚。在这个场景中,我们希望在更新用户接口信息的时候,保证原子性,即要么用户接口信息全部更新成功,要么全部不更新。
锁的作用是为了防止多个线程或进程同时修改同一个数据,造成数据不一致的情况。在分布式环境中,我们需要使用分布式锁来确保在多个节点上对数据的访问是互斥的。
然而,在这里的代码中,并没有实现事务和锁的逻辑。这里只是演示了整体的流程,并没有具体实现细节。所以,如果要在实际项目中应用这个功能,还需要进一步考虑并实现事务和锁的机制,以确保数据的一致性和安全性。
这一块的知识,因为之前在伙伴匹配系统里给大家讲过了,就不带大家再把这个地方实现一遍了。
来插入一条假数据,打开 user_interface_info 表。
打开查询控制台。
打开默认的控制台。
查询一下,这个就是我们刚刚写的业务逻辑。
按[Ctrl+A]全选,点击运行按钮。
运行成功。
回到 user_interface_info 表查询,点击刷新按钮。
更新成功。
光标放到 UserInterfaceInfoService 上,按[Alt+Enter],选择 Create Test。
选择 JUnit4。
创建一个单元测试。
编写单元测试。
package com.yupi.project.service; import org.junit.jupiter.api.Assertions; // 自动生成的包不对,要改成这个 import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import javax.annotation.Resource; @SpringBootTest public class UserInterfaceInfoServiceTest { @Resource private UserInterfaceInfoService userInterfaceInfoService; @Test public void invokeCount() { // 调用了userInterfaceInfoService的invokeCount方法,并传入两个参数(1L, 1L) boolean b = userInterfaceInfoService.invokeCount(1L, 1L); // 表示断言b的值为true,即测试用例期望invokeCount方法返回true Assertions.assertTrue(b); } }
启动 redis、后端 yuapi-backend 项目后,运行单元测试。
运行成功。
查看 user_interface_info 表,说明这个接口就可以用了。
接下来的话我们就想一下,现在我们就是在用户调用每个接口成功之后,是不是就要开始调用咱们刚刚的这个方法 invokeCount,然后给当前接口的这个次数加 1。但是有了问题,大家想一下,如果说我们每一个方法的调用成功之后,返回结果之前,我们都在这里调用一下这个方法是不是会比较麻烦。
接下来我们来考虑一下,现在的需求是在用户调用每个接口成功后,需要调用invokeCount方法,来给当前接口的调用次数加
1。但是这样做可能会带来一个问题:如果每个方法调用成功后,返回结果之前都要调用一次invokeCount方法,会显得非常繁琐。
7. AOP 切面(时间点 52:20-01:08:28)
7.1 思考(时间点 52:20-57:09)
**问题:**如果每个接口的方法都写调用次数 + 1,是不是比较麻烦?
**致命问题:**接口开发者需要自己去添加统计代码。
**AOP 切面的优点:**独立于接口,在每个接口调用后统计次数 + 1
**AOP 切面的缺点:**只存在于单个项目中,如果每个团队都要开发自己的模拟接口,那么都要写一个切面。
进一步说明:
我们现在面临一个问题:我们有很多接口A、接口B、接口C 等,每个接口都需要调用成功后进行调用次数加 1 的操作。如果我们每个接口的实现都手动去添加这个统计次数的逻辑,那将非常繁琐,而且容易出错。
为了解决这个问题,我们需要将统计次数的逻辑抽取出来,让它成为一个通用的功能,最大的问题是对于接口提供者来说,他们是无感知的,会出现一些问题。在这里,我们可以使用 AOP(面向切面编程)来实现这个功能。AOP 允许我们在原有业务逻辑的基础上,增加额外的操作,而不需要改动原有代码。
具体来说,我们可以通过 AOP 切面、拦截器或者过滤器来实现这个统计次数的逻辑。在接口调用成功后,AOP 切面或拦截器可以自动触发调用次数加 1 的方法,从而实现统一的统计功能。
此外,有同学提到了创建一个通用的方法来处理统计次数的逻辑,这也是一种可行的方法。但在我们的项目中,调用接口的代码已经很简洁,只有一行代码,因此不需要额外创建一个通用方法。对于其他复杂的场景,创建通用方法是一个不错的选择。
总结一下,AOP 切面是我们推荐的方案,它可以将统计次数的逻辑从业务逻辑中解耦出来,并实现统一的处理。在学习 AOP 切面的过程中,也要重点学习 Spring 的核心特性之一。
7.2 演示(时间点 57:09-01:01:14)
在 yuapi-interface 项目,创建aop包。
在 aop 包下创建InvokeCountAOP.java。
这里就写一下伪代码,不实际演示。
这里的 UserInterfaceInfoService 爆红,是因为不在 yuapi-interface 项目中,在 yuapi-backend 项目中,这个没关系,我们等会不把这个逻辑写到这里,所以这段是伪代码,了解即可,然后把伪代码注释掉。
AOP 切面的具体语法和实现细节可以通过上网查找资料来学习,这里不用过于关注具体的实现方式。关键是要理解 AOP 切面的作用,它可以在调用每个接口前或者后,帮助我们执行一些额外的操作,比如统计调用次数,它本质底层的实现就是一个动态代理。听不懂的同学先把它理解为在调用每个接口之后,为接口添加一段额外的代码逻辑。
7.3 讲解(时间点 01:01:14-01:08:28)
在我们当前的项目中,我们需要实现的统计功能涉及多个项目之间的调用,而不仅仅是单个项目内的统计。虽然 AOP 切面是一个不错的解决方案,但它有一个缺点:它是独立于单个项目的,每个项目都需要自己实现统计逻辑,并引入相应的 AOP切面包。
考虑到我们的项目架构,我们希望实现一种通用的统计方案,可以统一处理所有项目的接口调用情况。因此,我们决定采用网关来实现这个功能。修改刚刚的架构图:
按照之前提到的 AOP 切面方案,项目A 的开发者需要引入 AOP 切面并编写相关代码,同样地,项目B 的开发者也需要进行类似的操作。这样的做法会导致每个开发者都要关注统计功能的实现,需要引入一些代码并编写额外的逻辑。
为了避免这种情况,将统计次数的功能再抽出来一层。我们可以将统计次数的逻辑放在一个公共的位置,就像进入火车站一样,无论你乘坐哪趟列车,都需要经过这个统一的检票口。同样地,无论哪个模拟接口被调用,都会经过这个统一的统计次数逻辑。
我们现在做的这些事情,有同学说没有什么是加一层解决不了的。现在大家会发现,只要在同一个项目或者某一个层级中,存在相似或重复的东西,我们都可以将其抽象成一层,将其往上或往后抽出。这个抽象过程就是我们的网关。网关位于项目的最前面,统一处理不同项目之间的请求,是一个不断抽象的过程。
接下来,向大家介绍一下网关:网关就像刚才提到的火车站,我们在进站前必须经过一个统一的检票口,这个检票口就是网关。通过网关进行检票后,我们再去找到不同的车厢。
现在同理,假设我是一个普通的小用户,我想调用接口 A。如果没有网关,我要这样操作:先调用接口 A,然后接口 A 再调用统计次数方法,接着调用接口 B,再去调用统计次数方法……现在用户直接调用网关,由网关负责根据用户请求的地址,找到对应的接口,比如接口 A,然后调用接口 A,并在调用后统计次数加 1。同样,用户调用接口 B 或接口 C,也是先调用网关,然后网关再去找相应的接口并进行调用。
现在有一个问题,用户是否需要关心自己调用的接口是由项目 A 或团队 A 开发的,还是团队 B 开发的?实际上,用户根本不需要关心。他只需要知道自己需要什么功能,然后调用对应的网关即可。网关负责找到对应的接口并返回结果。对于开发者来说,他们也不需要关心统计次数。只要把自己的接口接入到网关中,让网关能找到并调用即可,网关会自动帮他们统计次数。
通过这种设计,我们实现了一个统一的网关来处理不同项目的请求,用户和开发者都不需要关心具体的细节,简化了操作,提高了系统的可用性和可维护性。
七、网关(时间点 01:08:28-02:48:22)
1. 网关讲解(时间点 01:08:28-01:31:43)
什么是网关?理解成火车站的检票口,统一 去检票。
**作用:**统一去进行一些操作、处理一些问题。
比如:
- 路由
- 负载均衡
- 统一鉴权
- 跨域
- 统一业务处理(缓存)
- 访问控制
- 发布控制
- 流量染色
- 接口保护
-
- 限制请求
- 信息脱敏
- 降级(熔断)
- 限流:学习令牌桶算法、学习漏桶算法,学习一下 RedisLimitHandler
- 超时时间
- 限制请求
- 统一日志
- 统一文档
简略说明:
**路由:**起到转发的作用,比如有接口 A 和接口 B,网关会记录这些信息,根据用户访问的地址和参数,转发请求到对应的接口(服务器 / 集群)。
/a => 接口A
/b => 接口B
参考文档:The After Route Predicate Factory。
**负载均衡:**在路由的基础上。
/c => 服务 A / 集群 A(随机转发到其中的某一个机器)
uri 从固定地址改成 lb:xxxx
**统一处理跨域:**网关统一处理跨域,不用在每个项目里单独处理。
参考文档:Global CORS Configuration。
**发布控制:**灰度发布,比如上线新接口,先给新接口分配 20% 的流量,老接口 80%,再慢慢调整比重。
参考文档:The Weight Route Predicate Factory。
**流量染色:**给请求(流量)添加一些标识,一般是设置请求头中,添加新的请求头。
参考文档:TheAddRequestHeaderGatewayFilterFactory。
全局染色:Default Filters。
统一接口保护:
- 限制请求:requestheadersize-gatewayfilter-factory。
- 信息脱敏:the-removerequestheader-gatewayfilter-factory。
- 降级(熔断):fallback-headers。
- 限流:the-requestratelimiter-gatewayfilter-factory。
- 超时时间:http-timeouts-configuration。
- 重试(业务保护):the-retry-gatewayfilter-factory。
**统一业务处理:**把一些每个项目中都要做的通用逻辑放到上层(网关),统一处理,比如本项目的次数统计。
**统一鉴权:**判断用户是否有权限进行操作,无论访问什么接口,我都统一去判断权限,不用重复写。
**访问控制:**黑白名单,比如限制 DDOS IP。
**统一日志:**统一的请求、响应信息记录。
**统一文档:**将下游项目的文档进行聚合,在一个页面统一查看。
建议用:knife4j 文档。
进一步说明:
在前面我们已经引入了今天的主角 —— 网关。那什么是网关呢?可以将网关看作是网络端口,类似于火车站或者机场的检票口,它负责统一进行检票。在火车站里,不同车厢需要单独检票吗?实际上不需要,因为网关在入口处已经完成了检票,节省了很多人力成本。之后你可以自己去找对应的车厢。这里有一个关键词,网关的主要作用就是统一,它可以统一执行很多操作。
接下来,我们将介绍网关的应用场景。通过应用场景,大家将逐渐了解为什么需要网关。从刚才的图中,你应该能够了解网关的优势。它对于用户来说屏蔽了底层的调用细节,并能保护接口。用户不需要直接调用接口,也不需要关心统计次数,只需在调用之前进入网关才能成功调用。
网关的应用场景:
**路由:**路由实际上就像一个中转站,类似于我们的路由器。
- 现在我们再看一下上面的图示。假设用户要访问某个接口 A,但现在用户不需要直接调用接口 A,而是通过我们的网关统一接收用户的请求。网关记录了用户调用的接口,并将其转发到对应的项目和接口进行处理,有点类似于前台接待。
- 路由在这里起到了转发的作用。举个例子,假设我们有接口 A 和接口 B,网关会记录这些信息,并根据用户访问的地址和参数,将请求转发到对应的接口(服务器/集群)。为了更好地理解,我们可以设置以下示例路由:如果用户访问接口 A,网关将转发请求到接口 A;如果用户访问接口 B,网关将转发请求到接口 B。这种转发过程就叫做路由。此外,还有一种情况是我们后面可能对接到一个集群。比如,当用户访问接口 C 时,网关可以将请求转发给服务 A 或者集群中的某个机器。在集群中,请求可能会随机转发到其中的某个机器上。
**统一鉴权:**判断用户是否有权限进行操作,无论访问什么接口,我都统一去判断权限,不用重复写。
- 之前的鉴权逻辑写在 yuapi-interface 这个项目中的方法里,用于判断用户是否有权限进行操作。但是如果每个方法都要单独写鉴权逻辑,显然是不可行的。所以我们决定将鉴权逻辑和统计次数一样,抽取出来放到网关里面。
- 在网关中,鉴权的重点是实现统一鉴权。无论用户要访问哪个接口,网关都会统一判断权限,不需要重复编写鉴权逻辑,这是网关的强调点之一。网关的作用在很多方面都是强调统一性,将重复的逻辑进行抽象和集中。
**统一处理跨域:**网关统一处理跨域,不用在每个项目里单独处理。
- 在开发单个 Spring Boot 项目或 Web 项目时,跨域问题是一个常见的挑战。特别是在接口项目中,可能存在多个项目如项目 A、项目 B 等,每个项目都可能面临跨域问题。如果每个项目都要单独处理跨域,就会出现重复劳动的情况。
- 为了避免重复的跨域处理,我们可以将跨域处理逻辑统一放到网关中,让网关来帮助我们处理跨域问题。这样,项目 A 和项目 B 就不再需要单独处理跨域,而是统一由网关处理。这是一种统一处理跨域的方法。
**统一业务处理:**把一些每个项目中都要做的通用逻辑放到上层(网关),统一处理,比如本项目的次数统计。
- 在我们的项目中,统一的业务处理指的是将项目中重复的业务逻辑抽取出来,放到网关这一层进行处理。例如,项目中可能存在一些通用的逻辑,比如统计调用次数和鉴权等。如果我们把这些逻辑写在每个项目的方法里,就会导致重复代码和维护困难。
- 为了避免重复的代码,我们可以将这些统一的业务逻辑放到网关层面进行处理。也就是说,我们可以把统计调用次数和鉴权的代码直接写在网关中,而不是每个方法里。这样,我们只需要在网关里写一次这些逻辑,就可以统一处理所有项目的调用次数统计和鉴权需求。通过网关的统一处理,我们可以将一些通用的业务逻辑进行封装,让项目中的方法更加清晰、简洁,同时也避免了重复劳动。
**访问控制:**黑白名单,比如限制 DDOS IP。
- 访问控制,又称为黑白名单,实际上也是一种权限控制机制。它与鉴权有一些区别。鉴权通常指授权,即判断用户是否有访问某种资源的权限。而黑白名单则主要用于判断每个用户是否可以访问特定资源,它是一种与业务逻辑独立的控制方式。
- 举个例子,如果有人恶意刷我们的流量,进行 DDOS 攻击,我们可以将这些恶意 IP 加入黑名单,限制它们的访问。这样,这些 IP 就无法访问我们的服务,从而保护了我们的接口和服务不受恶意攻击。
**发布控制:**灰度发布,比如上线新接口,先给新接口分配 20% 的流量,老接口 80%,再慢慢调整比重。
- 举个例子,假设我们的团队开发了一个名为项目 A 的接口 A,现在我们要对接口 A 进行升级,推出一个新版本的接口 A-V2。但我们并不确定新版本是否稳定可靠,所以我们想先让一部分用户试用这个新接口。我们可以将流量按照比例划分,比如 80% 的流量继续访问旧版本的接口 A,而 20% 的流量则引导到新版本的接口 A-V2。这样就实现了灰度测试的效果。然后我们会观察 V2 的表现,如果测试没有问题,我们可以逐步增加流量比例,比如 50%、70%、80%,直到 100%。最后,当我们确认新版本的接口稳定可靠时,就可以完全替换掉旧版本,下线接口 A。
- 这个流量分配的过程就是发布控制,而它通常是在网关层进行。因为网关是整个流量的入口,所以它可以担当请求流量分配的角色。通过在网关层进行发布控制,我们能够更加灵活地控制用户访问不同版本接口的比例,而无需在每个服务中进行单独处理。这种方式让我们能够更加安全和可靠地进行接口的升级和发布。
**流量染色:**给请求(流量)添加一些标识,一般是设置请求头中,添加新的请求头。
流量染色是什么意思呢?我们举个例子来解释。假设现在有一个用户要访问我的接口。但是有一个问题,我希望用户不能绕过网关直接调用我的接口,我想要防止这种情况发生。那么我应该如何防止绕过网关呢?
- 一个方法是要确定请求的来源。我们可以为用户通过网关来的请求打上一个标识,比如添加一个请求头 source=gateway。只要经过网关的请求,网关就会给它打上 source=gateway 的标识。接口 A 就可以根据这个请求头来判断,如果请求没有 source=gateway 这个标识,就直接拒绝掉它。这样,如果用户尝试绕过网关,没有这个请求头的话,我们的项目就不会认可它。这就是流量染色的一种应用。流量染色还有其他应用,比如区分用户的来源,这和鉴权是不同的概念,属于不同的应用场景。
- 另外一个常见的应用是用于排查用户调用接口时出现的问题。我们为每个用户的每次调用都打上一个唯一的 traceid,这是分布式链路追踪的概念。通过这个 traceid,当出现问题时,下游服务可以根据 traceid 追踪到具体的请求,从而逐层排查问题。这也是流量染色的作用之一。
总的来说,流量染色的作用就是给请求添加一些标识,通常通过设置请求头来实现。这样我们可以更好地控制请求的访问和识别请求的来源,同时方便排查问题。流量染色还有许多其他的应用,但它们都是基于给请求添加标识这个基本概念。
💡 **用户怎么绕过网关?**用户只要知道服务器的 IP 地址,尤其你的服务又在外网上公开时,用户就可以直接绕过网关进行访问。这种情况下,我们不能让这些接口直接对外暴露,而需要网关来隐藏这些接口信息。
**统一接口保护:**接口保护涉及多种方式,例如限制请求信息、数据脱敏、降级、限流、超时时间等措施。在网关中,我们可以统一进行请求大小的限制和数据脱敏处理,强调了统一的管理。
- 对于接口保护,我们可以通过网关统一限制请求的大小,确保接收到的请求在合理的范围内,避免恶意请求或者大量请求对后端服务造成不必要的负担。同时,我们也可以统一对请求和响应头进行处理。举个例子,有些接口原本会在响应头中返回服务器的 IP 地址等敏感信息,但通过网关的操作,我们可以将这些敏感信息抹掉或删除,保护服务器的隐私和安全。
- 另一个重要的保护机制是降级。当接口调用失败或接口下线时,我们可以采取降级逻辑,比如向用户提示接口已下线,或引导用户访问其他功能,从而确保用户始终能够得到有意义的响应。降级也被称为兜底,作为一种保险措施,即使正式服务不可用,仍能提供有用的反馈。
- 限流也是接口保护的重要手段。通过限制用户每分钟或每秒钟访问接口的次数,可以避免过多的请求对服务器造成压力。此外,设置超时时间也是保护服务器的一种方式,当接口调用时长超过设定时间,强制中断请求,保证服务器的稳定性。这些保护措施都是在网关层面进行统一处理,确保接口的安全和可靠性。
**统一日志:**统一的请求、响应信息记录。
在微服务项目中,这样的情况是相当常见的。例如,我们有两个项目:项目 A 和项目 B,它们各自可能记录一些日志。而在网关层,我们可以再加一层日志记录,以便记录每个用户请求的详细信息,包括鉴权情况以及每次响应的成功或失败信息。这种做法类似于我们之前提到的 AOP 切面,只不过现在我们把这个逻辑放到了网关层而已。
**统一文档:**将下游项目的文档进行聚合,在一个页面统一查看。
举个例子,假设我们有两个项目,分别是项目 A 和项目 B,并且每个项目都有一个对应的接口文档。通常情况下,如果我们要访问这两个项目的接口文档,我们需要分别访问项目 A 和项目 B 的接口文档地址,对吧?
但是,如果我们在网关层将这两个接口文档聚合在一起,会有什么作用呢?这实际上类似于语雀(一个在线文档协作平台)的作用。通过在网关层聚合接口文档,我们可以在一个统一的地方查看和管理所有项目的接口文档,不再需要分别去访问不同的地址。
假设我们原本每个项目都有一个独立的接口文档,现在我们可以在网关层将这些文档进行聚合,统一放到一起。这样用户在查看接口文档时就会更加方便,不再需要关注项目 A 的文档在哪个地址,项目 B 的文档在哪个地址,而是可以直接在网关处找到所有项目的接口文档。这种做法简化了文档查阅的步骤,让用户不再需要分别去查找每个项目的文档,从而提高了工作效率。总体来说,网关层的接口文档聚合功能为用户提供了更便捷的文档管理和查阅体验。
2. 分类讲解(时间点 01:31:43-01:34:40)
- 全局网关(接入层网关):作用是负载均衡、请求日志等,不和业务逻辑绑定。
- 业务网关(微服务网关):会有一些业务逻辑,作用是将请求转发到不同的业务 / 项目 / 接口 / 服务。
参考文章:网关的介绍
进一步说明:
当涉及网关分类时,一般情况下有**两种主要类型:**业务网关和全局网关,其中接入层网关也是全局网关的一种。它们有着一些区别。业务网关通常位于多个项目或微服务之上,负责根据用户请求将其转发到不同的业务、项目、接口或服务。另一方面,全局网关更多地关注请求本身,主要用于负载均衡。全局网关在大多数情况下并不涉及复杂的业务逻辑。相比之下,业务网关可能会包含一定的业务逻辑,比如之前提到的统计次数功能,这会影响你在技术选型上的决策。
然而,实际上你并不一定要明确区分业务网关和全局网关,因为它们的分类对于技术选型并不是绝对的要求。重要的是根据系统需求来选择适合的网关类型。全局网关的主要功能是负载均衡,将大量的请求平均分摊到系统中的多台机器上。它通常不涉及过多的业务逻辑,而更注重处理请求日志等任务。业务网关则更多地关注业务逻辑,例如统计次数、请求鉴权等,同时也会负责转发请求到具体的业务处理单元。
3. 实现讲解(时间点 01:34:40-01:38:49)
- Nginx(全局网关)、Kong 网关(API 网关,Kong),编程成本相对高一点。
- Spring Cloud Gateway(取代了 Zuul)性能高、可以用 Java 代码来写逻辑,适于学习。
参考文章:网关技术选型
进一步说明:
**为什么要做网关的分类?**是因为接下来我们就要讲实现了,我们之前的内容都是理论,接下来将着重讲解实际实现。在实现网关时,一般情况下,我们会遇到两种网关:业务网关和全局网关,而接入层网关其实是全局网关的一种。
**这两种网关有什么区别呢?**全局网关通常层级较高,可能覆盖多个项目或微服务,并负责将用户的请求转发到不同的业务、项目、接口或服务。它主要用于请求的负载均衡等功能,较少涉及具体的业务逻辑。我们可以使用 Nginx 或类似的网关,例如 Kong,Kong 是专门为 API 服务提供的网关。但是不推荐使用 Kong 的原因是,它有商业版本和免费版本,而免费版本可能会有一些限制。如果没有必要使用商业版本的特性,不建议个人用户使用 Kong,因为它可能会限制一些自由度和灵活性。
相比之下,Nginx 是比较推荐的全局网关,也称为接入层网关。Nginx 可以部署前端和后端,还能提供文件访问服务等多种功能,非常灵活。我们甚至可以在 Nginx 中编写业务逻辑,但是并不推荐这样做,因为它并不像 Spring Cloud Gateway 那样方便。
对于业务网关,特别是我们使用 Spring Boot 技术栈的情况下,强烈推荐使用 Spring Cloud Gateway ,它可以说是取代了 Zuul。Zuul 的架构设计有一些问题,例如并发量有限。而 Spring Cloud Gateway 则使用了 NIO 和多路复用等技术,底层采用了 native 和 react 模型,因此性能更高。
Spring Cloud Gateway 的最大优点是它允许我们使用 Java 代码来编写逻辑。相比于 Nginx 或 Kong,需要学习额外的语言和编程,但在现阶段来说并不是必要的。使用 Spring Cloud Gateway,你只需要掌握 Java 编程就足够了,这也是为什么推荐使用它的原因。而如果你对 Nginx 或 Kong 感兴趣,你只要知道 Spring Cloud Gateway 的实现,再去查阅 Nginx 或 Kong 的文档就能快速上手。最重要的是理解网关的实现思想,这对于我们实际的开发非常关键。
4. 官网阅读(时间点 01:38:49-01:41:21)
接下来,将带大家一步步使用 Spring Cloud Gateway 来实现之前讲解的特性。虽然不一定会涵盖每个特性的具体实现,但会为大家解释每个特性的实现方式。这样,大家就能了解如何去实现这些功能。那大家知道怎么去实现呢?
- 百度
- 看官方文档(推荐)
访问:
首先访问 官网。
具体怎么用呢?往下滑,这里有个 Getting Started。
5. 核心概念
在这里我们定义的是一个匹配器,或者更明确地说,在 Spring Cloud Gateway 中它被称为"断言"。
路由(根据什么条件,转发请求到哪里)
**断言:**一组规则、条件,用来确定如何转发路由
**过滤器:**对请求进行一系列的处理,比如添加请求头、添加请求参数
请求流程:
- 客户端发起请求
- Handler Mapping:根据断言,去将请求转发到对应的路由
- Web Handler:处理请求(一层层经过过滤器)
- 实际调用服务
两种配置方式:
- 配置式(方便、规范,推荐)
-
- 简化版
- 全称版
- 编程式(灵活、相对麻烦)
断言:
- After 在 xx 时间之后
- Before 在 xx 时间之前
- Between 在 xx 时间之间
- 请求类别
- 请求头(包含 Cookie)
- 查询参数
- 客户端地址
- 权重
过滤器:
基本功能:对请求头、请求参数、响应头的增删改查。
- 添加请求头
- 添加请求参数
- 添加响应头
- 降级
- 限流
- 重试
6. 实现示例场景(时间点 01:41:21-01:49:43)
点击左上角 File → New。
来创建一个新的 SpringBoot 项目。
选择 Gateway、Lombok、Spring Boot DevTools 依赖。
加载完后退出一个小窗,选择This Window(此窗口打开)。
底下的蓝条咻咻地加载依赖,稍等片刻。
把 application.properties 改成 application.yml,简洁一点。
大家看它的依赖,就这几个,最核心的就是这个。
继续阅读官网上的 Getting Started,它是一种编程式的网关配置方式,通过编写代码来实现的。等会我们还会学习另一种方式,叫声明式或者配置式。Spring Cloud Gateway 提供了这两种定义网关功能的方式,我们后面会讲。
复制代码。
这些代码定义了一个路由器,它的作用是当用户访问某个地址时,将其重定向到指定的网址。
@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("path_route", r -> r.path("/get") .uri("http://httpbin.org")) .route("host_route", r -> r.host("*.myhost.org") .uri("http://httpbin.org")) .route("rewrite_route", r -> r.host("*.rewrite.org") .filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}")) .uri("http://httpbin.org")) .route("hystrix_route", r -> r.host("*.hystrix.org") .filters(f -> f.hystrix(c -> c.setName("slowcmd"))) .uri("http://httpbin.org")) .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org") .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback"))) .uri("http://httpbin.org")) .route("limit_route", r -> r .host("*.limited.org").and().path("/anything/**") .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter()))) .uri("http://httpbin.org")) .build(); }
粘贴到 YuapiGatewayApplication.java。
底下的代码不用管,删掉,写一个最简单的。
然后,让我们看一下这里的代码,实际上它定义了一个路由器。路由器的作用是当用户访问某个地址时,将请求转发到另一个地址,或者说转发到对应的项目或服务。在这段代码中,首先我们给路由规则标识了一个唯一的 id,这个 id 可以任意取名,只要保证唯一性即可。
package com.yupi.yuapigateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; @SpringBootApplication public class YuapiGatewayApplication { public static void main(String[] args) { SpringApplication.run(YuapiGatewayApplication.class, args); } // 这个注解用于创建一个 Spring Bean,即一个路由规则的构建器 @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { // 创建路由规则的构建器 return builder.routes() // 定义路由规则,给该规则起一个名字 "tobaidu" .route("tobaidu", r -> r.path("/baidu") // 将满足 "/baidu" 路径的请求转发到 "https://www.baidu.com" .uri("https://www.baidu.com")) // 定义路由规则,给该规则起一个名字 "toyupiicu" .route("toyupiicu", r -> r.path("/yupiicu") // 将满足 "/yupiicu" 路径的请求转发到 "http://yupi.icu" .uri("http://yupi.icu")) // 创建并返回路由规则配置对象 .build(); } }
修改端口的地址。
启动项目。
访问 http://localhost:8090/ 报错,因为没有找到对应的路由,我们没有配置它要访问什么。
访问 http://localhost:8090/yupiicu,地址栏的地址变了,重定向成功。
ps.重定向到百度不行,可能百度有一些限制🐶。
7. 官方文档阅读 1 - 开始(时间点 01:49:43-01:58:13)
7.1 官方文档阅读(时间点 01:49:43-01:58:13)
继续阅读官网,一定要看GA版本的,不要看 SNAPSHOT 快照版本的,选公开的。
进入官方文档,大家一定要养成看官方文档的习惯ヾ(´・ω・`)ノ。
这里讲了怎么去引入 Spring Cloud Gateway,刚刚已经引入过了,略过,往下滑。
这里是核心概念,总共有三个。首先是路由,我们之前已经讲过了,它用于根据请求的网址进行转发。第二个是 predicate(断言),它的作用是根据一组条件来进行路由,可以将它理解为一组规则。
比如,我们刚刚所说的根据请求的地址来进行转发,实际上就是一个断言。
第三个核心概念是 filter,就是过滤器。过滤器的作用类似于我们学过的 Servlet 过滤器。它可以对请求进行集中处理,例如进行一些校验或添加请求头等操作。实际上,过滤器的功能有点类似于 AOP 切面或 Java 拦截器,只不过在这里被称为网关的过滤器,但作用和处理请求的目的是一样的。
往下滑,我们看一下 Spring Cloud Gateway 它是怎么工作的。
这个东西其实非常简单,看一下这个客户端,客户端就是我们的用户,就是要请求网站的人。首先,请求经过了这个 Gateway Handler Mapping。这是什么东西呢?它的作用就是根据你的请求找到对应的路由。也就是说,当用户发送请求时,Gateway Handler Mapping 会根据请求的信息来确定该请求应该被路由到哪个地方。
Gateway Handler Mapping 它的作用就是处理请求,它要经过一系列的过滤器,也就是说这个过滤器可以去定义多个。比如说我定义一个鉴权过滤器,专门用来从请求头中去鉴权。再定一个日志过滤器,专门用来去记日志。再定一个跨域过滤器都是可以的。它可以经过多种过滤器,过滤器之间也可以有顺序,先经过哪个后经过哪个。
最后,就是实际调用我们的服务。那实际调用服务,调用的就是我们的真实提供的接口,就是咱们的这个 yuapi-interface 项目,它就不是网关做的事情了。他最后就真正的去调用这个接口,这个项目的接口,这个流程其实就是这样,蛮简单的。
再往下滑,我们接着讲述了刚刚提到的两种配置网关的方式。那么我们**如何配置这个路由、过滤器和断言呢?**Spring Cloud Gateway
提供了两种方式。第一种方式是配置式或者叫声明式配置,就是在 application.yml文件中写配置(推荐)。
而第二种方式就是像刚才我们所演示的编程式配置,也就是通过编写代码来实现配置。这两种方式各有特点。
然后仔细看看它教你的配置方式,它的配置式分为两种,一种是简单的参数,像下图这样(推荐):
- spring.cloud.gateway.routes: 这是配置路由的属性。
- - id: after_route: 这是路由的唯一标识符,用于区分不同的路由。
- uri: https://example.org: 这是路由将请求转发到的目标 URI,即请求经过此路由后将被转发到 https://example.org 这个地址。
- predicates: 这是断言的配置属性,用于定义请求是否满足路由条件。
- - Cookie=mycookie,mycookievalue: 这是一个断言条件,它指定了请求必须具有名为 mycookie 的 Cookie,且其值必须为 mycookievalue,才能匹配这个路由。
通过这个配置,当满足请求带有特定mycookie 的 Cookie 并且其值为mycookievalue时,请求将被路由到https://example.org这个目标
URI。
复杂一点的配置就像下图这样,前面都没变,到 predicates 就变了(如果同事不熟悉这个,推荐用这种方式):
- - name: Cookie: 这是一个断言条件,它指定了使用 Cookie 作为断言类型来检查请求的 Cookie。
- args: 这是断言条件的参数配置。
- name: mycookie: 这个参数配置表示要匹配名为 mycookie 的 Cookie。
- regexp: mycookievalue: 这个参数配置表示要使用正则表达式匹配 mycookie 的值是否为 mycookievalue,只有当匹配成功时才会满足该路由条件。
通过这个配置,当请求带有名为 mycookie 的 Cookie,并且其值为 mycookievalue 时,请求将被路由到 https://example.org 这个目标 URI。注意这里使用了正则表达式来匹配 Cookie 的值。
继续阅读官方文档,第五大章就是路由的各种断言,不用管什么断言工程,就看代码。
第一个 The After Route Predicate Factory,这个断言它改了什么?
就是当你的当前时间,在它设置的时间之后,它就会访问 https://example.org 这个路由。
The Before Route Predicate Factory 就是当你的当前时间,在它设置的时间之前,它就会访问 https://example.org 这个路由。
这个类似于现实生活中某些活动的开始和结束。举个例子,假设你提供了两个接口:
接口 A 用于在活动结束前进行访问,而接口 B 则在今天零点活动结束后提供访问。
换句话说,如果用户在活动结束前访问接口 A,他将获得活动相关的内容和服务。然而,一旦活动结束,用户将被引导到接口 B,从而获得其他类型的服务或内容。这样的接口设计可以根据活动时间的变化提供不同的功能和响应,为用户带来更好的体验。
7.2 实现示例配置(时间点 01:58:13-02:03:06)
可以试一下这个配置,复制配置。
spring: cloud: gateway: routes: - id: after_route uri: https://example.org predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver]
粘贴到 application.yml 中,修改一下;
现在我们处于 2017 年之后,因此不论访问任何地址,都会被重定向到 https://yupi.icu。
💡 这里大家要注意,predicate 它用的是复数,说明什么呢?
(这里的 "-" 代表列表) 说明可以加多个规则,我们可以 after、before 同时运用,比如昨天之后,今天之前。
重启项目。
访问 http://localhost:8090/(可能会访问超时,重新访问即可)。
就重定向到 https://yupi.icu。
8. 官方文档阅读 2 - 断言(时间点 02:03:06-02:11:51)
8.1 官方文档阅读 1(时间点 02:03:06-02:04:11)
继续阅读官方文档,你会发现下面的这些东西,都和之前的是一样的写法,这里的 between 就是在两个时间之间。
再往下 Cookie Route 刚刚讲过,如果如果你的请求头中有了这个 cookie 而且 cookie 的值是 ch.p,那么就给你重定向到这个地址 https://example.org。
再往下 Header Route,如果你的请求头中包含了一个名为 X-Request-Id 的请求头,并且它的值符合正则表达式 \d+(即为一个或多个数字),那么你将被重定向到相应的地址。
再往下 Host Route,如果你访问的是指定的域名,那么就会被重定向到对应的地址。同时,这里可以使用通配符,即如果访问的是这两个域名中的任何一个,都会被重定向到相应的地址。
再往下 Method Route,如果你的请求方法,请求类别,它是 get 或者 post 的请求,就给你重定向到相应地址。
再往下 Path Route,如果你访问的地址是以对应路径作为前缀的,那么就给你访问到对应的地址。
8.2 实现示例配置(时间点 02:04:11-02:09:19)
这个 Path Route 比较好演示,之前的不好演示,因为我们的 host 只有 localhost,复制配置。
spring: cloud: gateway: routes: - id: path_route uri: https://example.org predicates: - Path=/red/{segment},/blue/{segment}
粘贴到 application.yml,修改一下。
server: port: 8090 spring: cloud: gateway: routes: # 将请求路径以/api/**开头的请求转发到目标URI https://yupi.icu - id: path_route uri: https://yupi.icu predicates: - Path=/api/** # 将请求路径以/baidu/**开头的请求转发到目标URI https://baidu.com - id: path_route2 uri: https://baidu.com predicates: - Path=/baidu/**
重启项目。
访问 http://localhost:8090/api/name,返回 404,因为 https://yupi.icu没有这个路径。
访问 http://localhost:8090/baidu/xxx,同理。
💡 有个问题,当我输入一个地址后,不知道它重定向到哪个规则进行处理,即无法确定应用了哪个路由规则。有时候只能通过输入类似百度这种特别明显的网址来区分。因此,在开发过程中,大家可以加上这个(如下图所示):
以下配置是将 Spring Cloud Gateway 的日志级别设置为 "trace",这意味着 Spring Cloud Gateway 将输出最详细的日志信息,包括所有的跟踪信息。通过设置这个日志级别,我们可以查看每个请求在网关中的处理流程、断言、过滤器的执行情况以及最终路由的结果,有助于调试和排查问题。
需要注意的是,"trace" 级别会产生大量的日志,仅在调试和排查问题时使用,生产环境应该将日志级别设置为更适合的水平,以避免过多的日志输出影响性能。
重启项目。
查看控制台,现在的日志就多了一大堆 DEBUG、TRACE 级别的。

访问 http://localhost:8090/baidu/xxx。
查看控制台,通过这个日志,可以看出它转向到哪。
- Pattern "[/baidu]" does not match against value "/baidu/xxx" 这条日志表示请求的路径/baidu/xxx与定义的路由路径/baidu不匹配。
- Pattern "[/yupiicu]" does not match against value "/baidu/xxx" 这条日志表示请求的路径/baidu/xxx与定义的路由路径/yupiicu不匹配。
- Pattern "[/api/**]" does not match against value "/baidu/xxx" 这条日志表示请求的路径/baidu/xxx与定义的路由路径/api/**不匹配。
- Pattern "/baidu/**" matches against value "/baidu/xxx" 这条日志表示请求的路径/baidu/xxx与定义的路由路径/baidu/**匹配成功。
8.3 官方文档阅读 2(时间点 02:09:19-02:11:51)
继续阅读官方文档,这个 Path Route 刚刚测试过了,它还可以支持传递一些参数。这些参数可以在编程的地方,也就是在过滤器中获取到。
再往下 Query Route,通过它可以根据查询条件进行路由匹配,比如说你的请求参数中包含一个名为 "green" 的查询参数,那它就会匹配到这个地址。
再往下 Remoteaddr Route,它允许我们根据远程地址来进行路由匹配。比如访问用户的 IP 地址是 192.168.1.1/24,就重定向到指定的地址 https://example.org。
再往下 Weight Route,这个功能非常重要,它允许我们根据权重来将请求重定向到不同的目标。
看一下它给我们的例子,首先定义了两个 URL,一个是 weight_high(高权重的目标),另一个是 weight_low(低权重的目标)。然后,设置了一组权重断言,其中高权重为 8(百分之八十),低权重为 2(百分之二十)。最后,我们只需定义这个规则,假设用户访问了十次,那么就会有八次请求导向高权重的目标,而有两次请求导向低权重的目标。通过这种方式,我们轻松实现了刚刚提到的发布控制或者灰度发布的需求。
再往下 XForwarded Remote Addr Route,这个功能允许我们从请求头中获取 X-Forwarded-Remote-Addr 这个字段,如果它的值是 192.168.1.1/24,那么就会将请求重定向到地址 https://example.org。
第五章看完咯~ 这种文档实际上看起来并不难。你会发现其中几乎没有几行英文,而是主要由示例组成。因此,学习这种文档的方法就是将这些示例拿去尝试运行一遍,这样基本上就能理解了。
9. 官方文档阅读 3 - 拦截器(时间点 02:11:51-02:42:40)
9.1 官方文档阅读 1(时间点 02:11:51-02:12:35)
来看下一部分,刚刚讲到**拦截器它的作用是什么?**对请求进行处理。我们来看怎么对请求进行处理:
首先来看一下拦截器的描述,拦截器的主要作用是对请求进行修改或处理响应等操作。
接着继续看文档,第一个拦截器叫做 AddRequestHeader,从名字上我们可以理解它的作用是添加请求头。
9.2 实现示例配置(时间点 02:12:35-02:18:27)
下面给大家演示一下具体操作。举个例子,我们先修改模拟接口项目的接口,并给它一个前缀叫"name",修改后打一个断点;
模拟接口项目的端口是 8123,所有请求要加上 /api 前缀,所以我们新修改的接口地址就是 http://localhost:8123/api/name。
以debug模式启动项目。
我们现在去配置路由,复制配置。
spring: cloud: gateway: routes: - id: add_request_header_route uri: https://example.org filters: - AddRequestHeader=X-Request-red, blue
粘贴到 yuapi-gateway 项目中,修改一下,然后启动项目。
这段配置是使用 Spring Cloud Gateway 创建了一个路由,将所有以 /api/ 开头的请求转发到 http://localhost:8123 上的目标服务。
访问 http://localhost:8123/api/name,自动跳转回模拟接口项目,现在拿不到请求头,因为它不会默认添加叫 "yupi"
的请求头,点击
按钮继续执行。
网页就会输出如下图所示内容。
然后访问 http://localhost:8090/api/name,自动跳转回模拟接口项目,说明已经访问到我们的接口项目,它访问 8090,给你转发到
8123,点击
按钮继续执行。
网页返回的内容是一样的响应。
把 yuapi-gateway 项目中过滤器的注释取消掉,修改一下,然后重启项目。
这个 filters 部分指定了一个过滤器,它的作用是添加一个名为 yupi 的请求头,并且请求头的值为 swag。
访问 http://localhost:8090/api/name,自动跳转回模拟接口项目。
查看控制台的输出结果,说明我们的请求头添加成功了。
之前给大家讲了一个叫请求染色的东西。请求染色其实就是给请求打上标识,以证明这个请求是从我这儿发起的,这样下游服务就能识别它。通过在请求头中添加特定的标识,比如yupi字段,我们可以实现对接口的保护,确保只有带有这个请求头的请求才能被下游服务认可并允许调用。这是染色的方式之一,还可以增加额外的参数等等。
9.3 官方文档阅读 2(时间点 02:18:27-02:42:40)
再往下 AddRequestParameter,这个就是增加请求参数,上一个是增加请求头。
试一下,复制配置。
粘贴到 yuapi-gateway 项目中,修改一下,传一个 name 的参数,之前我们的 name 都是空的,然后重启项目。
把断点去掉。
访问 http://localhost:8090/api/name,name 的参数就显示出来了,直接请求添加参数,可以看作是一种控制请求信息的方法。
在后端开发中,我们经常会遇到一层一层的服务器调用的情况。在这种场景下,每一层的服务器都可能会给请求添加自己的请求头,逐层补充信息。这个过程有点类似于计算机网络中数据包的封装,其中包含了网络头和其他一些信息。
继续阅读官方文档 AddResponseHeader,添加响应头,和之前的一样。
再往下 CircuitBreaker(断路器),断路器的作用是实现服务降级。当你访问某个接口地址时,如果这个接口出现错误,断路器会将你的请求降级,从而转而请求另外一个接口。
要使用这个断路器的话,就要引入这个包。
在 yuapi-gateway 项目中引入一下,它是由 Spring Cloud Starter 整合的。resilience4j 是一个面向 Java 的降级库,它能够帮助我们定义各种降级逻辑,根据不同的情况触发不同的降级策略。例如,当访问本地接口出现问题时,我们可以触发一种降级策略;当访问其他服务出现异常时,我们也可以采取不同的降级逻辑。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId> </dependency>
然后复制配置。
还有一个外部降级的配置,也复制了,结合一起用。
粘贴到 yuapi-gateway 项目中,修改一下,然后重启项目。
- CircuitBreaker: 定义了一个断路器过滤器,用于实现降级功能。该断路器的名字是 "myCircuitBreaker",当路由目标地址出现故障时,会触发降级策略,将请求转发到 "/fallback" 路径。
- 路由 "yupi-fallback",定义了一组断言,这里使用 "Path=/fallback" 断言,表示请求的路径为 "/fallback" 时,会匹配到该路由。
以下配置中,"add_request_header_route" 路由会对请求添加请求头和请求参数,并通过断路器实现降级功能。当该路由的目标地址出现故障时,请求会被转发到 "/fallback" 路由,即 "yupi-fallback" 路由,从而实现了降级处理。
访问 http://localhost:8090/api/name,没问题。
降级是一个不太容易测试的功能,但它会在特定条件下触发。如果你希望在触发降级时能够进行全局的异常处理或者执行特殊的处理逻辑,你可以自己定义不同的降级策略,根据不同的触发条件来实现这些逻辑。
继续阅读官方文档 CacheRequestBody,这个功能在企业项目中使用较少,可能很多人并不了解它的作用。实际上,它的作用是让原本的请求信息中的 body 参数可以被多次读取。默认情况下,请求的 body 参数只能被读取一次,但是使用了这个配置后,就可以多次读取请求的 body 参数,并将其作为一个持久化的缓存。
再往下 DedupeResponseHeader,去重复的。有时候在开发中,可能会遇到这样的场景:
请求经过了多个服务器,每个服务器都添加了一层跨域头。但是由于重复添加跨域头,可能导致最终跨域失败并出现错误。为了解决这个问题,我们可以使用 DedupeResponseHeader 来去除重复的响应头。
DedupeResponseHeader 的作用就是检查响应头中是否包含重复的头信息,并进行去重处理。它还提供了一些去重策略,例如保留最后一个重复头信息或随便保留一个。
再往下 JsonToGrpc,转换序列化格式。
再往下 MapRequestHeader,假设你的原始请求中有一个请求头 A,这个过滤器会将请求头 A 的值映射给另一个请求头 B,实际上就是为你添加了一个新的请求头,并将原始请求头 A 的值填充到这个新请求头 B 中。
再往下 ModifyRequestBody,修改请求的参数。比如原本的请求参数是小写的 "aa",然后你可以通过 ModifyRequestBody 将它改成大写的 "AA"。这里没有使用声明式的配置,而是采用了编程式的定义方式。因为像修改请求参数这种情况,通常都需要比较灵活的处理方式,所以使用编程式的定义会更加合适。
再往下 ModifyResponseBody,修改响应参数。比如说原本返回的是 "A",但可以强行将它改成 "B"。或者可以给响应值再封装一层,封装一个 "code",封装一个 "message",封装一个状态码。
再往下 PrefixPath(前缀处理器),就是说当你访问原本的地址 https://example.org 时,如果你本来要访问的是 /hello,这个过滤器会给你添加一个前缀 /mypath,最终实际访问的地址会变成 /mypath/hello。
再往下 PreserveHostHeader(持久化host),这个大家先不用过于纠结。就是说有时候我们的请求在服务期间转发时,会导致请求头中的 host 值发生改变。而通过使用持久化 host 的方式,我们可以让请求转发后的 host 值保持不变,不发生改变。
再往下 RedirectTo(重定向),就是访问 https://example.org,它会自动给你重定向到 https://acme.org。
再往下 RemoveRequestHeader,这个很有意思,我们刚才不是讲了可以添加请求头,那同理我们可以移除请求头。
再往下 RemoveRequestParameter,删除请求参数,比如原本的请求参数有 name,可以把它删掉。
再往下 RemoveResponseHeader,删除响应头。
再往下 RequestHeaderSize,限制请求的大小,如果请求的大小超过 1000B 就直接报错,这个就是一些请求保护方面的东西。
再往下 RequestRateLimiter(限流),这个非常重要的特性。在 Spring Cloud Gateway 中实现限流非常简单,官方推荐使用 Redis 来进行限流,因为网关通常是重要的组件,不建议在单机上实现限流。大多数情况下,网站是分布式的,使用多个机器提供服务。因此,建议将限流逻辑集中存储到 Redis 中,让 Redis 作为集中式存储帮助我们统计是否达到限流阈值。
官方文档中提到,如果要使用 Redis 的限流功能,首先需要引入spring-boot-starter-data-redis-reactive这个库。然后文档介绍了限流所使用的是令牌桶算法。对于令牌桶算法,有一些参数需要配置,例如生成令牌的速率、桶的容量以及初始的令牌数等等。理解这些参数可能有助于更好地理解限流的实现方式,当然,这种限流方法可能稍显复杂。
如果你觉得官方文档理解起来有困难,不用担心,你可以通过百度搜索查找一些博客或其他资源来学习更简单的限流方法。
再往下 RewriteLocationResponseHeader,改写特殊的请求头。

再往下 RewritePath(改写路径),比如说原本你访问的是 /red,然后经过网关,它会把 /red 这个路径去掉,最终转发给后端服务。这种操作在 Nginx 里也经常配置过,如果大家有学过的话应该很熟悉。

再往下 RewriteResponseHeader,改写响应头。

再往下 SaveSession,session 的持久化,先不用关注这个。

再往下 SecureHeaders(安全),这部分可能与 Spring Security 框架有关,它涉及一些安全方面的请求头,不需要过于关注这部分内容。

再往下 SetPath,它的作用是设置请求的路径。假如你原本访问的是 /red/blue,现在通过使用 SetPath,你可以直接将请求路径修改为 /blue。
再往下 SetRequestHeader,改写请求头。

再往下 SetResponseHeader,改写响应头。
所以你会发现,这个处理器的主要作用是对请求头和请求参数进行增删改查,我们可以用一个常用的术语来描述它。它实际上在操作请求头和响应头。此外,它还有一些流量保护的功能。总的来说,这个处理器的功能非常简单,没有什么难的。

再往下 StripPrefix,它的作用是移除请求的前缀。比如当你访问网关的时候,请求的地址是 /name/blue/red,但是你的服务实际上只需要处理 /red 这个地址,这时候你就可以使用 StripPrefix,它会帮助你将前缀去掉,直接将两层前缀 /name/blue 去掉,甚至可以去掉三层或更多。
再往下 Retry(重试处理器),它能够帮助你自动重试接口。比如你的接口访问一次失败了,Retry 会自动帮你再次尝试访问。而且它还可以控制重试的时间间隔,采用指数级递增的方式。比如第一次访问失败后,间隔一秒再重试;第二次访问失败后,间隔两秒再重试;第三次访问失败后,间隔四秒再重试,以此类推,这叫做降频重试。这个 Retry 过滤器是非常值得学习的。
这个就不带大家演示了,因为重试的情况通常在实际的项目中才会发生,你需要根据你自己的业务异常来定义在什么情况下进行重试。它本质上不算是一个接口保护,但在某种程度上它也是一个业务的保护,保证你的业务一定能成功完成。

再往下 RequestSize,刚刚我们讲的是请求头的大小限制,这个是请求大小。如果你请求大小超过数量,它就会给你报错。

再往下 SetRequestHostHeader,设置请求的 host 头。

再往下 Default Filters(默认过滤器),刚刚我们讲的 filters 是写在单个路由下,对某一个路径生效,或者只对某一个断言生效。但是现在,你可以直接给整个网关定一些默认的过滤器,比如说我们刚刚讲的染色功能。
现在可以这样写,直接给所有的经过网关的请求都加一个,这就是全局的染色了,我们这个项目有可能会用到这个特性。
这一块我们已经把所有的处理器讲完了~ 再往下就更简单了,这个文档重点就是第五、六章。
10. 官方文档阅读 4 - 过滤器(时间点 02:42:40-02:48:22)
下面是全局过滤器,它教你怎么自定义过滤器,就是一个编程式的过滤器。我们下次写代码的时候会详细演示。在下次的直播中,我们会结合具体业务场景应用网关,带大家使用编程式和配置式结合的方式来实现过滤器。
这部分内容看起来很多,但实际上非常简单。文档对这部分也没有写得很清楚,因此我们可以跳过这一段,继续看下一章节。

再往下 HttpHeadersFilters,它会向下游发送请求之前应用到每个请求。和之前的有点像,这个也不用关注,继续看下一章节。
再往下就是安全方面的配置,配置 SSL 证书、加密之类。这个也不用关注,继续看下一章节。
这部分配置就是设置了一个 HTTP 超时时间。当你的网关接收到请求并转发到下游服务时,如果超过了预设的时间限制,例如连接超时或响应超时,它会抛出异常并报错。这相当于是对全局接口超时进行了判断和设置。
接着往下,有一个跨域配置,这是非常重要的功能。通过这个跨域配置,可以直接在网关这里定义你想要的跨域设置。可以指定允许跨域的请求头,哪些请求需要跨域支持,以及允许的跨域方法。实际上,这和我们自己编写跨域代码是相似的。但是通过网关配置,可以更简单和集中地管理跨域设置。

再往下 Troubleshooting,其中有一些建议,就是在调试过程中将日志级别降低为 trace 或 debug 级别,这样可以更详细地查看日志信息,帮助你排查问题。

这部分介绍了自定义路由工厂、断言工厂和处理器工厂,但一般情况下我们不需要关注和修改这些工厂的逻辑。我们通常只会关注自定义处理器的逻辑和自定义路由的逻辑,而不会去改变这些更底层的工厂相关的内容,因为在大多数情况下,我们用不到这些工厂的自定义功能。

基本上这个官方文档就看完了,结束咯(~ ̄▽ ̄)~
八、小作业(时间点 02:48:22-02:50:33)
通过阅读源码:Spring Cloud Gateway Sample 来了解 gateway 编程式开发,这个是公网给的入门级的例子。
项目分区导航:⬅️ 13-项目笔记(六) | 14-项目笔记(四) | ➡️ 15-API 开放平台简历写法
💬 评论