07
1. 项目概述
本项目是一个面向开发者的 API 平台,提供 API 接口供开发者调用。用户通过注册登录,可以开通接口调用权限,并可以浏览和调用接口。每次调用都会进行统计,用户可以根据统计数据进行分析和优化。管理员可以发布接口、下线接口、接入接口,并可视化接口的调用情况和数据。本项目侧重于后端,涉及多种编程技巧和架构设计层面的知识。
2. 本期时间点
3. 本期计划
- 完成网关业务逻辑
- 开发管理员分析的功能
- 上线
4. 后端网关业务
1. 梳理网关业务逻辑
以下操作可以复用:
- 实际情况应该是去数据库中查是否已分配给用户秘钥(ak、sk是否合法)
- 先根据 accessKey 判断用户是否存在,查到 secretKey
- 对比 secretKey 和用户传的加密后的 secretKey 是否一致
- 从数据库中查询模拟接口是否存在,以及请求方法是否匹配(还可以校验请求参数)
- 调用成功,接口调用次数 + 1 invokeCount
进一步说明:
找到 yuapi-gateway 项目的 CustomGlobalFilter,看下有哪些业务逻辑是没有做的。
目前的实现中,我们在鉴权时是直接写死 "yupi",检查 accessKey 是否与用户匹配。
然而,更好的实践是从数据库中查询用户分配的 accessKey 并验证其有效性。
其次,就是我们要检查模拟接口是否存在,并验证请求方法是否匹配。
甚至还可以对用户的请求参数进行合法性校验。
再次,如果用户调用模拟接口成功,需要统计接口的调用。
接下来我们来依次开发。
2. 项目启动及问题思考
启动 nacos,进入 nacos 中的 bin 目录;
选中上面的路径栏,输入 cmd,按[Enter]回车进入。
把命令粘贴进去,先以单机模式运行,在 8848 端口启动起来了。
▼bash
复制代码# Linux/Unix/Mac
sh startup.sh -m standalone
# ubuntu
bash startup.sh -m standalone
# Windows
startup.cmd -m standalone
启动 redis 之后,以 debug 模式启动 yuapi-backend 项目。
把之前整的取消掉。
再以 debug 模式启动 yuapi-gateway 项目,有结果就行,报错没关系。
把之前打的断点取消掉。
首先,从数据库中查是否已分配给用户秘钥,这代码逻辑应该在哪写呢?
之前我们的鉴权写在了模拟接口项目,用 idea 单独打开 yuapi-interface 项目。
找到之前校验用户 API 签名的那段代码,这里我们是直接写在单个具体的方法里。
目前我们面临一个问题,就是项目尚未接入数据库,也并未引入 MyBatis。
我们不能指望开发人员自行引入 MyBatis 并调用我们的公共数据库,这显然是不切实际的。
因此,应该为开发人员提供一个远程调用的服务,以便帮助他们进行验证,或者我们可以提供一个 SDK 供他们引入,以便进行验证操作。在这种情况下,我们需要依赖一个公共的服务来实现这个功能。
3. 抽象公共服务
**项目名:**yuapi-common
**目的:**让方法、实体类在多个项目间复用,减少重复编写。
服务抽取:
- 数据库中查是否已分配给用户秘钥(根据 accessKey 拿到用户信息,返回用户信息,为空表示不存在)
- 从数据库中查询模拟接口是否存在(请求路径、请求方法、请求参数,返回接口信息,为空表示不存在)
- 接口调用次数 + 1 invokeCount(accessKey、secretKey(标识用户),请求接口路径)
步骤:
- 新建干净的 maven 项目,只保留必要的公共依赖
- 抽取 service 和实体类
- install 本地 maven 包
- 让服务提供者引入 common 包,测试是否正常运行
- 让服务消费者引入 common 包
进一步说明:
接下来,我们计划创建一个公共服务模块,以便在多个项目之间实现方法和实体类的复用,从而避免重复编写相同的代码。
首先,我们需要明确哪些通用服务是必要的。先从数据库中查询是否已为用户分配了密钥,我们可以对这个方法进行进一步抽象,考虑需要传递哪些参数以及期望的返回值,具体来说,我们要查找数据库以确认是否已经将 accessKey 分配给了某个用户。
我们**需要关注哪些参数需要传递?**只需要 accessKey 参数,如果还有其他参数,可以在之后进行补充。
其次,我们需要处理的另一个问题是从数据库查询模拟接口是否存在。这个任务是网关模块的一部分,我们需要确保网关能够实现这个功能,具体来说,我们需要一个方法来检查数据库中是否存在特定的模拟接口。
**这个方法需要接收哪些参数呢?**找到网关项目 yuapi-gateway。
这里的第四步,我们既然是要查模拟接口是否存在,是不是要传递这个接口的信息。
这**接口信息是什么?**是从请求路径和方法中获取到的,甚至你还可以传请求参数;
所以这里的话我们可以封装一个对象。
第三个就是对接口调用次数进行加一操作。这个过程需要传递什么参数呢?
考虑到要将调用次数加一,必须要知道是哪个用户进行了接口调用,以及具体是哪个接口的调用次数要增加。在这里,可以借助 accessKey 和 secretKey 这两个密钥来标识用户并获取其信息。
因此,我们需要传递的参数包括 accessKey 和 secretKey。通过 secretKey,我们可以识别用户并获取相关信息。此外,还需要传递请求的接口路径。暂定这些,后续需要进行调整,也可以进行修改。
4. 创建公共服务
1. 创建公共服务项目
建一个 Maven 项目,提供公共的接口、实体类;
点击 File → New。
创建 Maven 项目,选择 1.8 版本,点击 Next。
项目名yuapi-common
。
选择在新窗口打开。
重新设置新项目的 Maven 仓库。
在 java 目录下新建包。
我们要将服务之间进行调用的几个方法写到公共类中,以及相应的 model,就看需要什么就传什么。
回到 yuapi-backend 项目,查看 invokeCount 方法,它这里接收接口的 id 和用户 id,我们只要拿到这两个即可。
我们就用它这个方法,复制 UserInterfaceInfoService.java,粘贴到 yuapi-common 项目中的 service 包下,还爆红了,因为实体类不存在,还要复制实体类。
复制 yuapi-backend 项目中的 model 包,粘贴到 yuapi-common 项目中。
把 dto 包删掉,因为它就是类与类之间的转换,不需要。
在公共服务项目引入 yuapi-backend 项目的依赖,删除多余的依赖,这个公共服务只负责提供接口,不负责提供实现。
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
粘贴到 pom.xml 中。
把 SpringBoot 的声明也粘贴进来,点击加载。
ps.这个声明相当于定义了一些依赖的版本、信息。
model 包下就不爆红了。
来到 PostVO,删掉上面的引入,让它重新加载。
回到 UserInterfaceInfoService 中,删掉上面的引入,让它重新加载。
重新加载后就不爆红了。
2. 编写方法
在 UserInterfaceInfoService 定义两个方法。
我们不应该将所有的服务都集中在 UserInterfaceInfoService 中,应该将它们分成多个独立的服务进行编写;
复制 yuapi-backend 项目的 InterfaceInfoService.java、UserService.java。
粘贴到 yuapi-common 项目中。
这两个文件还有爆红,删除引入,让它重新加载。
把 UserService 里面的用户注册、登录... 删掉,不需要。
来写两个方法:
- 数据库中查是否已分配给用户秘钥(accessKey)
- 从数据库中查询模拟接口是否存在(请求路径、请求方法、请求参数)
首先,在 UserService 中写数据库中查是否已分配给用户秘钥的方法。
其次,在 InterfaceInfoService 写从数据库中查询模拟接口是否存在的方法。
给以下两个文件统一加上Inner
的文件名前缀。
删除多余的内容,没有继承它,修改后的三个文件:
把 yuapi-common 项目打包,先设置它的版本为0.0.1
。
点击右侧 Maven,双击 install 进行打包。
打包成功。
3. 引入公共服务依赖
复制公共服务版本信息。
<groupId>com.yupi</groupId>
<artifactId>yuapi-common</artifactId>
<version>0.0.1</version>
粘贴到 yuapi-backend 项目中。
💭 题外话:
在学习 Dubbo 的过程中,建议大家在学完 Spring Boot 和 Redis 之后开始学习。如果时间比较紧张,可以选择暂时不深入学习 Dubbo,但如果有半年或者更长时间来学习,建议在进入 Spring Cloud 之前先学习 Dubbo。这是因为 Spring Cloud 微服务架构的底层可能会使用 HTTP 协议或其他协议进行远程调用,微服务涉及到更多的知识领域,例如网关、分布式事务以及服务总线等多个概念。
而 Dubbo 作为一个 RPC 调用框架,更加专注于远程调用的领域,相对更为纯粹。因此,在掌握 Dubbo 的基础之后,再深入学习 Spring Cloud 将会更有帮助。
验证一下,看看 yuapi-backend 项目能不能用这个公共的类了;
把 UserInterfaceInfoService.java、UserInterfaceInfo.java 删掉。
更换引入。
把这个 User、InterfaceInfo 删掉,直接调用公共服务里的。
执行测试类,看看能不能运行成功。
调用成功,也输出的 SQL 语句。
现在公共服务算是抽取完成了。
4. 方法实现
接下来就去实现刚刚的那几个方法,点这里,找一下 yuapi-common 依赖。
找到 InnerUserService,来实现接口;
光标放到 InnerUserService,按[Alt+Enter],选择Implenment interface
(实现接口)。
选择目标目录。
实现方法就选 getInvokeUser。
它就生成在我们指定的目录下了。
再实现 InnerInterfaceInfoService 的接口;
光标放到 InnerInterfaceInfoService,按[Alt+Enter],选择 Implenment interface。
选择目标目录。
实现方法选 getInterfaceInfo。
生成好了。
再生成另一个,光标放到 InnerUserInterfaceInfoService;
按[Alt+Enter],选择 Implenment interface。
选择目标目录。
实现方法选 invokeCount。
生成好了。
在实现类里,我们就可以编写对应的方法了。
先去写统计接口次数的这个类,直接引入 UserInterfaceInfoService,调用它的方法即可。
package com.yupi.project.service.impl;
import com.yupi.project.service.UserInterfaceInfoService;
import com.yupi.yuapicommon.service.InnerUserInterfaceInfoService;
import javax.annotation.Resource;
public class InnerUserInterfaceInfoServiceImpl implements InnerUserInterfaceInfoService {
@Resource
private UserInterfaceInfoService userInterfaceInfoService;
@Override
public boolean invokeCount(long interfaceInfoId, long userId) {
// 调用注入的 UserInterfaceInfoService 的 invokeCount 方法
return userInterfaceInfoService.invokeCount(interfaceInfoId, userId);
}
}
再去写下获取调用用户信息的类,直接去数据库中根据 ak、sk 判断获取。
package com.yupi.project.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.mapper.UserMapper;
import com.yupi.yuapicommon.model.entity.User;
import com.yupi.yuapicommon.service.InnerUserService;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.Resource;
public class InnerUserServiceImpl implements InnerUserService {
@Resource
private UserMapper userMapper;
/**
* 实现接口中的 getInvokeUser 方法,用于根据密钥获取内部用户信息。
*
* @param accessKey 密钥
* @return 内部用户信息,如果找不到匹配的用户则返回 null
* @throws BusinessException 参数错误时抛出业务异常
*/
@Override
public User getInvokeUser(String accessKey) {
// 参数校验
if (StringUtils.isAnyBlank(accessKey)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 创建查询条件包装器
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("accessKey", accessKey);
// 使用 UserMapper 的 selectOne 方法查询用户信息
return userMapper.selectOne(queryWrapper);
}
}
最后写下根据请求路径、方法名获取接口信息的类。
package com.yupi.project.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.mapper.InterfaceInfoMapper;
import com.yupi.yuapicommon.model.entity.InterfaceInfo;
import com.yupi.yuapicommon.service.InnerInterfaceInfoService;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.Resource;
public class InnerInterfaceInfoServiceImpl implements InnerInterfaceInfoService {
@Resource
private InterfaceInfoMapper interfaceInfoMapper;
/**
* 实现接口中的 getInterfaceInfo 方法,用于根据URL和请求方法获取内部接口信息。
*
* @param url 请求URL
* @param method 请求方法
* @return 内部接口信息,如果找不到匹配的接口则返回 null
* @throws BusinessException 参数错误时抛出业务异常
*/
@Override
public InterfaceInfo getInterfaceInfo(String url, String method) {
// 参数校验
if (StringUtils.isAnyBlank(url, method)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 创建查询条件包装器
QueryWrapper<InterfaceInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("url", url);
queryWrapper.eq("method", method);
// 使用 InterfaceInfoMapper 的 selectOne 方法查询接口信息
return interfaceInfoMapper.selectOne(queryWrapper);
}
}
这三个接口就写完了~
大家还记得我们在使用 Dubbo 时需要做哪些事情吗?
参考下之前编写的DemoService
,来看一下,它只需要在我们的实现类上添加@DubboService
注解即可。
现在我们将刚刚的三个接口都加上这个注解,使它们也能够正常使用 Dubbo。
在 impl 包下新建inner
包,把内部和外部都隔离开,将三个接口都放进去。
这些服务基本上写好了。
5. 调用公共服务
1. 在网关编写调用公共服务
以 debug 模式重启 yuapi-backend、yuapi-gateway 项目、启动 yuapi-interface 项目。
访问 nacos 注册中心 http://自己服务的 IP 地址:8848/nacos/index.html,都注册上了。
如果发现 yuapi-backend 项目启动失败:
- 网速问题,网速太卡了,导致注册超时,等一会,重新启动
- 端口冲突,换个端口
ps.网速问题延伸,因为注册默认 3 秒,超过 3 秒即注册失败,可以去设置增加注册超时时间。
接下来就去网关项目中使用这些公共的服务,复制公共服务依赖。
<dependency>
<groupId>com.yupi</groupId>
<artifactId>yuapi-common</artifactId>
<version>0.0.1</version>
</dependency>
粘贴到 yuapi-gateway 项目中。
进入到网关要操作的主业务流程里,先引入三个类:
获取用户是否存在、获取接口信息是否存在、统计接口调用次数。
完成这步。
修改后:
往下滑,完善这步。
修改后:
往下滑,来整这步。
先提取下请求路径、方法。
然后修改:
💡 有同学提到了一种防止绕过网关的方法,即在网关上添加一个请求头。
实际上,是否添加这个请求头并不是特别重要,之前已经和大家讨论过,我们的目标是为了实现流量染色。
如果要添加这个请求头,可以在请求中再添加一个固定的标识,例如"yuapi",并给它一个任意的值,然后在客户端进行判断。
但是目前考虑到实际影响不大,暂时不添加这个请求头了。因为如果添加的话,客户端还需要进行额外的判断,在每个模拟接口处添加逻辑或者编写一个公共方法,这会变得比较繁琐。
往下滑,来整这步。
先添加两个参数:接口 id、用户 id。
然后去修改:
ps.在生产环境中,通常公司都会接一个报警系统,以确保一旦出现问题,能够立即触发报警机制。
2. 调用测试
重启 yuapi-gateway 项目,有小报错没关系,能显示之前的示例结果即可。
打个断点。
访问 http://localhost:8090/api/name/get?name=yupi,自动跳回 yuapi-gataway 项目。
按[F8]执行到这里,你会发现它的请求路径是 /api/name/get。
按[F9]结束执行,这个有点问题,我们后端数据库存的是 http://localhost:7529/name/user。
临时问题:如何获取接口转发服务器的地址
**思路提供:**网关启动时,获取所有的接口信息,维护到内存的 hashmap 中;有请求时,根据请求的 url 路径或者其他参数(比如 host 请求头)来判断应该转发到哪台服务器、以及用于校验接口是否存在。
这里的参数比如说,在客户端调用接口时使用。你需要获取特定接口的信息,例如其中的主机(host)信息。在客户端发起请求时,你可以将这个主机信息添加到请求头中。这样,网关就能够获取到这个请求头。
进一步说明:
就是根据用户请求的地址,我们要找出需要转发的目标服务器地址。为了获取类似于 localhost:7529 这样的地址,我们需要进行一些步骤,**如何实现这一步骤呢?**建议从数据库中获取这些信息。这里提供一个参考方法,但值得注意的是,这一步不是必需的,可以根据个人情况决定是否采取这一步。
**具体方法如下:**当我们启动网关项目时,首先从数据库中获取所有接口信息。对于每个 URL,我们需要获取与之关联的路径以及接口名称信息。之后,根据接口的其他相关信息,我们可以找到应该转发到的目标主机(host)。可以为数据库添加一个额外的字段,用于存储转发的目标服务器地址,通过这个地址,我们可以将请求转发到对应的路径上,这就是大致的流程。
偷个懒,因为我们所有的模拟接口项目都位于同一个地址(8123),直接将这个地址进行拼接。
通常情况下,如果你自己在开发类似的模拟接口平台,也可以像这里一样,事先写入几个固定的地址,避免每次都需要从数据库中读取的繁琐过程。
写一个地址的常量,然后将其拼接。
重启 yuapi-gateway 项目,打开 yuapi-client-sdk 项目;
这个项目也要去引入公共服务。
💡 有同学问:如何让其他用户上传自己编写的接口?
需要提供一个注册机制。在这个机制下,其他用户可以上传他们自己编写的接口信息。为了简化流程,可以设计一个用户友好的界面。在这个界面上,用户可以输入他们的接口信息,包括服务器地址(host)、请求路径等内容。也可以规定,在接入我们的平台时,用户必须使用我们提供的 SDK 或遵循一定的要求。
**如何进行接入和要求的遵循?**在用户上传接口的时候,我们需要对接口信息进行测试调用,以确保接口的正常运行,这可以通过我们的平台来完成。同时,我们也可以要求用户标明该接口是否是由我们的网关调用,这可能需要用户在代码中加入判断代码,或者引入我们提供的 SDK 来实现。
**接口信息的组织和存储:**当用户上传接口信息时,这些信息将被存储在 InterfaceInfo 接口中。除了 URL 外,还应该添加一个 host 字段,用于明确区分不同服务器的地址。这样,可以更清晰地区分请求路径和服务器地址,提高接口信息的可读性和可维护性。
我们回到前端去调用,不在浏览器调用了,这样的话密码也绕不过去。
启动前端项目,访问 http://localhost:8000,登录,就调用第一个接口,点击查看
。
输入请求参数后,点击调用。
▼json
复制代码{"username":"yupi"}
自动跳回 yuapi-gateway 项目。
按[F8]向下执行到这里,请求来源是 127.0.0.1。
按[F8]向下执行到这里,参数都拿到了。
再往下,就是远程调用了,在 yuapi-backend 项目打个断点。
回到 yuapi-gateway 项目按[F8],就会跳到我们刚刚在 yuapi-backend 项目打断点的地方。
按[F8]跳到下一行,再按[F9]结束执行,然后把 yuapi-backend 项目断点取消。
回到 yuapi-backend 继续按[F8]向下执行。
执行到这里,发现它显示没有查到,按[F9]结束执行。
把这里的断点取消。
往下滑,在这里打个断点。
去数据库人工修改下 url 的数据为 http://localhost:8123/api/name/user,先把整个流程跑通。
不过啊,因为我们登录的是 yupi 账号,如果登录其他账号就会报无权限错误,为什么呢?
因为我们之前的接口逻辑是写在模拟接口项目里的,我们现在的校验逻辑不应该写在这里;
注释掉,重启模拟接口项目。
在前端重新点击调用
,自动跳转回 yuapi-gateway 项目。
这次拿到了,按[F9]结束执行。
返回前端页面查看,成功输出。
去数据库中查,也能看见次数的变化,现在整个业务流程跑通。
ps.还有个小问题,我们的网关没有校验用户是否还有调用次数,这个一定要放在发送请求前,交给大家实现🐶。
我们回顾一下之前的业务流程,看一下用户是如何获取到他们的 AKSK 用于模拟调用接口的。
找到 InterfaceInfoController 中的 invokeInterfaceInfo 方法。
那么在这个方法中,我们是如何获取 AKSK 的呢?
首先,用户需要进行登录,我们就能从登录用户的信息中提取出他们的 AKSK。因此,可以说我们之前已经实现了动态的操作,能够在运行时获取用户信息并为他们分配密钥了。
所以 application.yml 这里写什么都无所谓了。
5. 统计分析功能
接下来再给大家讲一个小的知识点,我们要去开发专门供管理员使用的统计分析功能,在实际开发过程中,并不需要去开发这个功能,根据实际需求而定。
1. 需求分析
各接口的总调用次数占比(饼图)取调用最多的前 3 个接口,从而分析出哪些接口没有人用(降低资源、或者下线),高频接口(增加资源、提高收费)。
进一步说明:
**现在我们可以进行哪些分析呢?**基于我们当前的业务和数据,我们可以分析系统中的用户注册情况,即每日新增用户数量或总用户数,或者是,哪些接口被频繁调用、对于同一用户,他们使用的接口情况,例如某个用户今天调用了多少次接口。
**我们要做什么呢?**我们基于现有的数据,假设一些需求。例如,设想统计某个特定用户对接口的调用次数占比,可以采用饼图来呈现这个数据,通过图表,能直观地看到接口被调用的次数。
然而,在着手处理这个需求之前,我们必须要明确分析的目的是什么。我们一般进行分析都是为了达到某个特定的目标,就拿🐟的情况举例,之前主要从事运营分析和大数据分析,对诉求就是要求很高,所以我们必须要明确清楚分析诉求。
回到我们刚才提到的需求,以此为例,我们可以将其分解一下。这个需求的目的明确地是让某个用户,或者说用户本人,能够了解在某段时间内使用哪些接口的次数较多。这样的分析有何意义呢?
可以据此判断某个接口在特定时间内是否被频繁调用,如果某个接口在某段时间内几乎没有调用记录,那么**我们是否需要继续保持资源的分配呢?**另一方面,对于高频次调用的接口,我们可以考虑是否需要增加相应资源,或者在某些情况下进行收费等等。因此,这个需求背后是有很多的价值和意义的。
2. 后端开发
1. SQL 查询调用数据
接下来,我们需要考虑如何实际实现这个需求,实现起来并不复杂。
这个实现涉及前后端的协同沟通,后端的任务相对简单,主要是涉及到数据库增删改查的操作,我们需要编写一个接口来获取示例数据,比如:“接口 A 被调用了两次,接口 B 被调用了三次”。
那么,**这个接口应该如何编写呢?我们需要获取哪些接口信息呢?**比如接口的名称、描述、地址等等。但是考虑到接口数量可能会很多,不可能全部展示出来,所以可以设置一个条件,只取前三个接口作为示例。首先,我们需要获取到每个接口的 interfaceInfoId,因为我们要按接口进行分组统计,这个统计操作涉及到了 user_interface_info 表。
让我们着手编写相应的 SQL 查询语句。基本上,我们需要按接口进行分组,然后统计每组接口的调用总数,这可以通过以下 SQL 查询实现:
-- 获取接口调用次数的统计信息,并按照调用总次数降序排列,最后取前三个接口作为结果
select interfaceInfoId, sum(totalNum) as totalNum
from user_interface_info
group by interfaceInfoId
order by totalNum desc
limit 3;
接下来,进行测试,先往数据库中添加一些数据,模拟不同接口被不同用户调用的情况,然后运行这个查询语句,查看结果是否符合预期。
💡 有同学说:有必要增加热点推荐功能吗?
个人认为没有必要,我们不必过于复杂化这个系统,如果我们的接口数量很少,甚至可以在一个屏幕上展示出来,那么就没有必要考虑热点推荐功能。
打开 SQL 控制台。
将 SQL 语句粘贴进去,按[Ctrl+A]全选,点击绿色按钮执行。
下方显示查询结果。
2. 业务层去关联查询接口信息
在获取了这个 SQL 查询结果之后,还有其他需要处理的事情吗?
现在我们只拿到了接口的 id,但如果除了 id,还需要展示接口的名称,应该如何处理呢?
需要进行关联查询来获取接口信息。由于我们只取了前三,所以在这种情况下进行关联查询不会对性能产生太大影响,如果大家要展示所有接口,关联查询可能就会变得复杂。
回到 yuapi-backend 项目,新建一个 controller:AnalysisController。
先写一点内容。
再新建一个包装类,复制 PostVO.java,粘贴到 vo 包下,并重命名为InterfaceInfoVO
。
然后修改一下 InterfaceInfoVO.java。
package com.yupi.project.model.vo;
import com.yupi.yuapicommon.model.entity.InterfaceInfo;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 接口信息封装视图
*
* @author yupi
* @TableName product
*/
@EqualsAndHashCode(callSuper = true)
@Data
// 这里就继承InterfaceInfo,再补充一个调用次数的字段
public class InterfaceInfoVO extends InterfaceInfo {
/**
* 调用次数
*/
private Integer totalNum;
private static final long serialVersionUID = 1L;
}
由于我们需要查询,涉及到两个表,user_interface_info 表和接口信息表。因此,在这种情况下,我们可以在映射层(Map层)编写一个自定义的 SQL 查询语句,然后在业务层实现下面的关联查询。
找到 UserInterfaceInfoMapper 整个自定义 SQL,把刚刚编写的 SQL 语句粘贴进去。
编写代码。
把光标放到 listTopInvokeInterfaceInfo 中,按[Alt+Enter] 生成 statement。
自动生成了对应的 map。
把 SQL 语句粘贴过来。
我们是**如何处理这个业务的呢?**我们并不是直接在代码中编写 SQL,而是先将 SQL 写好,然后在数据库中执行它,确保 SQL 的正确性。然后再开始编写代码,这样可以节省很多时间。
如果你遇到业务逻辑问题,怀疑是数据查询的问题,可以先打印相关日志,并将 SQL 语句复制到数据库中执行一次。这样可以确定是业务代码问题还是 SQL 语句问题。
然后修改一下,把 limit 改成动态参数。
继续编写 AnalysisController。
package com.yupi.project.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yupi.project.annotation.AuthCheck;
import com.yupi.project.common.BaseResponse;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.common.ResultUtils;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.mapper.UserInterfaceInfoMapper;
import com.yupi.project.model.vo.InterfaceInfoVO;
import com.yupi.project.service.InterfaceInfoService;
import com.yupi.yuapicommon.model.entity.InterfaceInfo;
import com.yupi.yuapicommon.model.entity.UserInterfaceInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 分析控制器
*/
@RestController
@RequestMapping("/analysis")
@Slf4j
public class AnalysisController {
@Resource
private UserInterfaceInfoMapper userInterfaceInfoMapper;
@Resource
private InterfaceInfoService interfaceInfoService;
/**
* 获取调用次数最多的接口信息列表。
* 通过用户接口信息表查询调用次数最多的接口ID,再关联查询接口详细信息。
*
* @return 接口信息列表,包含调用次数最多的接口信息
*/
@GetMapping("/top/interface/invoke")
@AuthCheck(mustRole = "admin")
public BaseResponse<List<InterfaceInfoVO>> listTopInvokeInterfaceInfo() {
// 查询调用次数最多的接口信息列表
List<UserInterfaceInfo> userInterfaceInfoList = userInterfaceInfoMapper.listTopInvokeInterfaceInfo(3);
// 将接口信息按照接口ID分组,便于关联查询
Map<Long, List<UserInterfaceInfo>> interfaceInfoIdObjMap = userInterfaceInfoList.stream()
.collect(Collectors.groupingBy(UserInterfaceInfo::getInterfaceInfoId));
// 创建查询接口信息的条件包装器
QueryWrapper<InterfaceInfo> queryWrapper = new QueryWrapper<>();
// 设置查询条件,使用接口信息ID在接口信息映射中的键集合进行条件匹配
queryWrapper.in("id", interfaceInfoIdObjMap.keySet());
// 调用接口信息服务的list方法,传入条件包装器,获取符合条件的接口信息列表
List<InterfaceInfo> list = interfaceInfoService.list(queryWrapper);
// 判断查询结果是否为空
if (CollectionUtils.isEmpty(list)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
// 构建接口信息VO列表,使用流式处理将接口信息映射为接口信息VO对象,并加入列表中
List<InterfaceInfoVO> interfaceInfoVOList = list.stream().map(interfaceInfo -> {
// 创建一个新的接口信息VO对象
InterfaceInfoVO interfaceInfoVO = new InterfaceInfoVO();
// 将接口信息复制到接口信息VO对象中
BeanUtils.copyProperties(interfaceInfo, interfaceInfoVO);
// 从接口信息ID对应的映射中获取调用次数
int totalNum = interfaceInfoIdObjMap.get(interfaceInfo.getId()).get(0).getTotalNum();
// 将调用次数设置到接口信息VO对象中
interfaceInfoVO.setTotalNum(totalNum);
// 返回构建好的接口信息VO对象
return interfaceInfoVO;
}).collect(Collectors.toList());
// 返回处理结果
return ResultUtils.success(interfaceInfoVOList);
}
}
重启 yuapi-backend 项目,后端整完了~
3. 前端开发
1. 图表库
接下来我们要着手处理前端部分,我们的目标是在前端页面上展示一个饼图。
那么,**如何展示饼图呢?**一般来说,我们并不需要自己手动绘制饼图,因为已经有现成的图表库可供使用。在大多数情况下,我们会使用现成的图表库,除非你的公司拥有充足的资源来开发自己的图表组件。比如:
ECharts 唯一的缺点是它的外观相对较传统,不够现代化,缺乏一些科技感和精致度,但它的功能却非常强大。
BizCharts 则是一个商业性质的图表库,提供的图表更加华丽。另外,AntV 也是一个值得推荐的选择,相较于 BizCharts,AntV 更被广泛推崇。
如何使用图表库:
- 看官网
- 找到快速入门、按文档去引入库
- 进入示例页面
- 找到你要的图
- 在线调试
- 复制代码
- 改为真实数据
如果是 React 项目,用这个库:https://github.com/hustcc/echarts-for-react。
2. AntV 官网浏览
访问 AntV 官网 —— AntV。
AntV 是阿里蚂蚁金服开发的一套数据可视化库,用于数据呈现与展示。大家以后需要进行数据可视化,强烈建议使用 AntV。这个库拥有丰富的功能和一系列的产品,其界面设计也相当出色。
AntV 不仅仅是一款数据可视化工具,它还包括了多个子库。
其中,G2 专注于 PC 端图表的展示与绘制;
S2 则提供了多维表格的功能,适用于专注于表格数据的场景;
G6 则用于处理关系型数据的可视化,尤其适用于图论和模型分析等领域;
而 X6 则是一套流程编辑引擎,可用于创建流程图和 ER 图等,非常适合构建流程图系统。
L7 是更高级的数据可视化库,它专注于地理位置城市化、空间城市化以及地理数据的可视化呈现,这使得它在展示地理信息方面具有非常出色的表现。
F2 则专注于移动端图表的展示与绘制。与之对应的,G2则是适用于 PC 端的图表库,两者在不同终端上都能够提供高效的数据可视化。
AVA 并不是针对普通开发人员的工具。它更适用于对框架进行深度二次开发,或者研发自己的技术框架,需要进行更加复杂的数据分析、定制化数据分析等等。一般来说,大多数开发者并不需要使用 AVA。
接下来,对于我们的 PC 端,可能会使用 G2 这个库。如何使用呢?后面可能还会给大家更详细地介绍,但本期先给大家快速地看一下,其实所有这些可视化库都有相似的用法:
进入官方网站,然后浏览网站上的图表示例。你可以只专注于图表示例,无需浏览其他内容,直接找到你需要的图表样式。
找到饼图,点击进入。 —— 饼图
右侧有现成的代码供你使用,无需过多考虑,只需要将代码复制粘贴到的项目中。
接着,将后端实际获取到的数据替换掉原始代码中获取数据的部分,这里就不介绍这个库的使用方法,因为它的用法非常简单。所有这些可视化图表库的用法都类似,它们没有太大的学习成本,除非你要深入研究这些内容。
3. ECharts 使用
访问 ECharts 官网,点击示例
。 —— ECharts
ps.百度已将 ECharts 捐赠给了 Apache 基金会。
找到 饼图,就用第一个。
可以在这里调试。
比如这里改成 '谷牛'。
然后复制代码。
option = {
title: {
text: 'Referer of a Website',
subtext: 'Fake Data',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: 'Access From',
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: '谷牛' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
代码先留着,接下来要引入 Echarts。
因为我们前端项目是 react,去 github 找 react 版的,搜索:react-echarts。
往下滑,复制安装命令。
▼bash
复制代码npm install --save echarts-for-react
npm install --save echarts
粘贴到前端的终端中执行。
安装之后,回到 github 往下看,看它怎么用。
只需要将其引入,引入后,需要使用 ReactECharts 组件,这里有一个 option 选项。
import ReactECharts from 'echarts-for-react';
// render echarts option.
<ReactECharts option={this.getOption()} />
option 选项对应我们示例代码中的这里。
4. 新建分析页
去 routes.ts 增加新的路由。
在 Admin 目录下新建InterfaceAnalysis
目录,复制 index.tsx,粘贴到新建的目录下。
删除 index.tsx 多余的内容,并修改一下。
引入 ECharts。
然后把示例代码粘贴进来,修改一下。
import { PageContainer } from '@ant-design/pro-components';
import '@umijs/max';
import React, { useEffect, useState } from 'react';
import ReactECharts from 'echarts-for-react';
/**
* 接口分析
* @constructor
*/
const InterfaceAnalysis: React.FC = () => {
// 存储数据的状态
const [data, setData] = useState([]);
// 控制加载状态的状态,默认加载中(true)
const [loading, setLoading] = useState(true);
useEffect(() => {
// todo 从远程获取数据
},[])
// ECharts图表的配置选项
const option = {
title: {
text: 'Referer of a Website',
subtext: 'Fake Data',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: 'Access From',
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: '谷牛' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
return (
<PageContainer>
{/* 使用 ReactECharts 组件,传入图表配置 */}
<ReactECharts loadingOption={{
// 控制加载状态
showLoading: loading
}}
option={option} />
</PageContainer>
);
};
export default InterfaceAnalysis;
回到前端页面查看效果,成功展示出来了。
我们刚刚在后端新写了一个接口,在前端用 openapi 生成。
继续编写分析页的代码。
import { PageContainer } from '@ant-design/pro-components';
import '@umijs/max';
import React, {useEffect, useState} from 'react';
import ReactECharts from 'echarts-for-react';
import {listTopInvokeInterfaceInfoUsingGET} from "@/services/yuapi-backend/analysisController";
/**
* 接口分析
* @constructor
*/
const InterfaceAnalysis: React.FC = () => {
const [data, setData] = useState<API.InterfaceInfoVO[]>([]);
useEffect(() => {
try {
listTopInvokeInterfaceInfoUsingGET().then(res => {
if (res.data) {
setData(res.data);
}
})
} catch (e: any) {
}
// todo 从远程获取数据
}, [])
// 映射:{ value: 1048, name: 'Search Engine' },
const chartData = data.map(item => {
return {
value: item.totalNum,
name: item.name,
}
})
const option = {
title: {
text: '调用次数最多的接口TOP3',
left: 'center',
},
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: 'Access From',
type: 'pie',
radius: '50%',
data: chartData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
};
return (
<PageContainer>
<ReactECharts option={option} />
</PageContainer>
);
};
export default InterfaceAnalysis;
回到前端页面查看效果。
如果要改样式,先在 ECharts 调整好,再粘贴到项目中即可。
开发完咯~ 下期不再见<( ̄ˇ ̄)/
6. 上线计划
**前端:**参考之前用户中心或伙伴匹配系统的上线方式。
后端:
- backend 项目:web 项目,部署 spring boot 的 jar 包(对外的)
- gateway 网关项目:web 项目,部署 spring boot 的 jar 包(对外的)
- interface 模拟接口项目:web 项目,部署 spring boot 的 jar 包(不建议对外暴露的)
关键:网络必须要连通
**自己学习用:**单个服务器部署这三个项目就足够。
如果你是搞大事,多个服务器建议在 同一内网 ,内网交互会更快、且更安全。
7. 扩展思路
- 用户可以申请更换签名
- 怎么让其他用户也上传接口?
- 需要提供一个机制(界面),让用户输入自己的接口 host(服务器地址)、接口信息,将接口信息写入数据库。
- 可以在 interfaceInfo 表里加个 host 字段,区分服务器地址,让接口提供者更灵活地接入系统。
- 将接口信息写入数据库之前,要对接口进行校验(比如检查他的地址是否遵循规则,测试调用),保证他是正常的。
- 将接口信息写入数据库之前遵循咱们的要求(并且使用咱们的 sdk),
- 在接入时,平台需要测试调用这个接口,保证他是正常的。
- 网关校验是否还有调用次数
- 需要考虑并发问题,防止瞬间调用超额。
- 网关优化
- 比如增加限流 / 降级保护,提高性能等。还可以考虑搭配 Nginx 网关使用。
- 功能增强
- 可以针对不同的请求头或者接口类型来设计前端界面和表单,便于用户调用,获得更好的体验。
- 可以参考 swagger、postman、knife4j 的页面。
上面提到的要检查用户提供的地址是否符合规则。我们的项目中,所有的模拟接口都是以 /api 开头,或者说,我们规定所有模拟接口的地址都必须以 http://localhost:8123 开头。你可以根据需要自行设置规则,然而,在制定这些规则时,最好不要过于严格,最好像之前提到的,可以在数据库中存储一个 host 的信息,只需满足一些特定的后缀条件,比如以 /api 开头等。
这个项目实际上有很多可以发展的方向,有许多要考虑的要点。如果你希望项目取得成功,就需要考虑许多方面。你需要制定规范,关注安全性、性能等方面。此外,还需要考虑如何防止被滥用。
有同学问:Dubbo 不是需要暴露服务才可以吗?
我们这里不是那个模拟接口用 Dubbo 去暴露的,**我们暴露的是什么?**暴露是系统内部用的接口,什么查询数据库中是否给用户分配密钥、接调用接口次数统计。暴露的不是开发者提供的接口,开发者提供的接我们是通过网关去转发的。然后这里你如果想让网端直接通过同一套地址去调用,那你就让用户遵循这个地址规则。
有同学问:现在的 SDK 不是固定了方法吗?
对于这个问题,需要明确一个**核心点:**一旦接口投入使用后,肯定要针对这个接口进行 SDK 的开发。你需要不断地完善 SDK,使其适应不断变化的需求。当然,也可以让 SDK 从接口信息表中读取信息,然后动态生成方法等等,这种做法也是可行的,然而,并不建议这样做。实际上,接口的发布可能不太像我们在应用商店发布应用那样灵活,在这种接口的发布过程中,建议还是介入一点人工。
以腾讯云的 SDK 手册为例,它有许多不同的 SDK,尽管代码可能是自动生成的,但是它的 SDK 是根据接口动态变化的。这意味着它并不是为 100 个接口提供同一个代码供调用,相反,它会为每个方法、每个接口生成相应的方法,同样,它也会根据不同的地址生成相应的方法。这一点非常重要,请大家多去使用一下第三方的 API 接口,这样就会更了解对方的 SDK 是如何设计的。
你可以去腾讯云下载他们的 SDK,会发现其中包含大量方法,基本上每个接口都有相应的方法。这样做的目的是让使用者更加便利,他们只需要输入方法名就能实现操作,无需关心具体的接口地址。
有同学问:就是每次发布一个接口,都需要更新一次 SDK?
是的,实际上每次发布一个新的接口,都需要对 SDK 进行更新。建议的做法是这样的:由于用户并不关心具体的接口地址,因此你可以让他们直接调用方法名,然后根据这些方法名去动态生成对应的方法。每次发布新接口时,更新 SDK 的操作可以做得非常简单,可以采用脚本的方式,从数据库中读取接口地址,与之前已有的地址进行对比,然后补充相应的方法,这个过程是可行的,事实上,很多公司都在这样做。