07

1. 项目概述

本项目是一个面向开发者的 API 平台,提供 API 接口供开发者调用。用户通过注册登录,可以开通接口调用权限,并可以浏览和调用接口。每次调用都会进行统计,用户可以根据统计数据进行分析和优化。管理员可以发布接口、下线接口、接入接口,并可视化接口的调用情况和数据。本项目侧重于后端,涉及多种编程技巧和架构设计层面的知识。

2. 本期时间点

81731d5318484a478069f67f5646ac31

3. 本期计划

  1. 完成网关业务逻辑
  2. 开发管理员分析的功能
  3. 上线

4. 后端网关业务

1. 梳理网关业务逻辑

以下操作可以复用:

  1. 实际情况应该是去数据库中查是否已分配给用户秘钥(ak、sk是否合法)
    1. 先根据 accessKey 判断用户是否存在,查到 secretKey
    2. 对比 secretKey 和用户传的加密后的 secretKey 是否一致
  2. 从数据库中查询模拟接口是否存在,以及请求方法是否匹配(还可以校验请求参数)
  3. 调用成功,接口调用次数 + 1 invokeCount

进一步说明:

找到 yuapi-gateway 项目的 CustomGlobalFilter,看下有哪些业务逻辑是没有做的。

目前的实现中,我们在鉴权时是直接写死 "yupi",检查 accessKey 是否与用户匹配。

然而,更好的实践是从数据库中查询用户分配的 accessKey 并验证其有效性。

bf46954f3dbb48fa9acdb579de731fbf

其次,就是我们要检查模拟接口是否存在,并验证请求方法是否匹配。

甚至还可以对用户的请求参数进行合法性校验。

da82adf702c44e12899a311a33d6c07a

再次,如果用户调用模拟接口成功,需要统计接口的调用。

68e2171d2d064585995fa3a0f1981b38

接下来我们来依次开发。

2. 项目启动及问题思考

启动 nacos,进入 nacos 中的 bin 目录;

选中上面的路径栏,输入 cmd,按[Enter]回车进入。

8d3bfba520c8419096707ead611ae1c5

把命令粘贴进去,先以单机模式运行,在 8848 端口启动起来了。

▼bash

复制代码# Linux/Unix/Mac
sh startup.sh -m standalone

# ubuntu
bash startup.sh -m standalone

# Windows
startup.cmd -m standalone
840c8d83982f42c7a6e4a60b8e8b0125

启动 redis 之后,以 debug 模式启动 yuapi-backend 项目。

04f6cbe522b349a4b66ef39432271a9f2a411ee9131e4de9be5efe1ce0b6a9ebcbc44148b1324b7db237dd96364f9b4e

把之前整的取消掉。

de34cb45f71a43f2b84f44552768f132

再以 debug 模式启动 yuapi-gateway 项目,有结果就行,报错没关系。

c5530ca9890343f28e16a3f15df2d60ada3231fcec05419e9c759512e3eb63e3

把之前打的断点取消掉。

64884d1677944743a244a54029d6e3df

首先,从数据库中查是否已分配给用户秘钥,这代码逻辑应该在哪写呢?

之前我们的鉴权写在了模拟接口项目,用 idea 单独打开 yuapi-interface 项目。

e1fbdfd96da547b497a33e3fcc2b1851

找到之前校验用户 API 签名的那段代码,这里我们是直接写在单个具体的方法里。

53f20aeec8b04ca59fd0ea4f91b1639e

目前我们面临一个问题,就是项目尚未接入数据库,也并未引入 MyBatis。

我们不能指望开发人员自行引入 MyBatis 并调用我们的公共数据库,这显然是不切实际的。

因此,应该为开发人员提供一个远程调用的服务,以便帮助他们进行验证,或者我们可以提供一个 SDK 供他们引入,以便进行验证操作。在这种情况下,我们需要依赖一个公共的服务来实现这个功能。

3. 抽象公共服务

**项目名:**yuapi-common

**目的:**让方法、实体类在多个项目间复用,减少重复编写。

服务抽取:

  1. 数据库中查是否已分配给用户秘钥(根据 accessKey 拿到用户信息,返回用户信息,为空表示不存在)
  2. 从数据库中查询模拟接口是否存在(请求路径、请求方法、请求参数,返回接口信息,为空表示不存在)
  3. 接口调用次数 + 1 invokeCount(accessKey、secretKey(标识用户),请求接口路径)

步骤:

  1. 新建干净的 maven 项目,只保留必要的公共依赖
  2. 抽取 service 和实体类
  3. install 本地 maven 包
  4. 让服务提供者引入 common 包,测试是否正常运行
  5. 让服务消费者引入 common 包

进一步说明:

接下来,我们计划创建一个公共服务模块,以便在多个项目之间实现方法和实体类的复用,从而避免重复编写相同的代码。

首先,我们需要明确哪些通用服务是必要的。先从数据库中查询是否已为用户分配了密钥,我们可以对这个方法进行进一步抽象,考虑需要传递哪些参数以及期望的返回值,具体来说,我们要查找数据库以确认是否已经将 accessKey 分配给了某个用户。

我们**需要关注哪些参数需要传递?**只需要 accessKey 参数,如果还有其他参数,可以在之后进行补充。

其次,我们需要处理的另一个问题是从数据库查询模拟接口是否存在。这个任务是网关模块的一部分,我们需要确保网关能够实现这个功能,具体来说,我们需要一个方法来检查数据库中是否存在特定的模拟接口。

**这个方法需要接收哪些参数呢?**找到网关项目 yuapi-gateway。

5a948bc8c66c49e29f66071288b6c030

这里的第四步,我们既然是要查模拟接口是否存在,是不是要传递这个接口的信息。

7867fe583fb74dfba3f1f7ef3c42a9da

这**接口信息是什么?**是从请求路径和方法中获取到的,甚至你还可以传请求参数;

所以这里的话我们可以封装一个对象。

373b7b18434e4c979ed0efe0c75b69b1

第三个就是对接口调用次数进行加一操作。这个过程需要传递什么参数呢?

考虑到要将调用次数加一,必须要知道是哪个用户进行了接口调用,以及具体是哪个接口的调用次数要增加。在这里,可以借助 accessKey 和 secretKey 这两个密钥来标识用户并获取其信息。

因此,我们需要传递的参数包括 accessKey 和 secretKey。通过 secretKey,我们可以识别用户并获取相关信息。此外,还需要传递请求的接口路径。暂定这些,后续需要进行调整,也可以进行修改。

4. 创建公共服务

1. 创建公共服务项目

建一个 Maven 项目,提供公共的接口、实体类;

点击 File → New。

b9e23ef361644f7d8aef0e2222784d19

创建 Maven 项目,选择 1.8 版本,点击 Next。

d34bffda761743509ff08e76731befc2

项目名yuapi-common

81a415045f7e4cb1b7504620365cb26a

选择在新窗口打开。

a09dd6c746d043c2b9a4f4e9b2bcc997

重新设置新项目的 Maven 仓库。

832da93b665444f5b6f0c2ffa3a85d26

在 java 目录下新建包。

我们要将服务之间进行调用的几个方法写到公共类中,以及相应的 model,就看需要什么就传什么。

471b1b5097d9475b904b15d668a6e18a

回到 yuapi-backend 项目,查看 invokeCount 方法,它这里接收接口的 id 和用户 id,我们只要拿到这两个即可。

fceb7b1c9cd94aeb9bbcb0b1891b84f3

我们就用它这个方法,复制 UserInterfaceInfoService.java,粘贴到 yuapi-common 项目中的 service 包下,还爆红了,因为实体类不存在,还要复制实体类。

aa8f4763d3394560bcdb5cf4cecfa8c8

复制 yuapi-backend 项目中的 model 包,粘贴到 yuapi-common 项目中。

11cbd7471a294ce5875094cf725398fc51c58b9fa27f42acb1e1ea739a2ff6e1

把 dto 包删掉,因为它就是类与类之间的转换,不需要。

00315b2b887a48d3b6193b5d55b7a93a

在公共服务项目引入 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 中。

640962d674374855841ee18cced9a523

把 SpringBoot 的声明也粘贴进来,点击加载。

ps.这个声明相当于定义了一些依赖的版本、信息。

28aee0d6e16247dc8c3d488ecbb8d81da8ae93b8b451443da67045e08044832e

model 包下就不爆红了。

3eaca4b407724ef5bf5dc9638b2e0369

来到 PostVO,删掉上面的引入,让它重新加载。

10723261ff7d4f47abe6236a4e2488201af28f6c17d240b884974a9ae7f11e19

回到 UserInterfaceInfoService 中,删掉上面的引入,让它重新加载。

d2f7394fd2dd40dfb27012ee5e8541d4

重新加载后就不爆红了。

d7c4ac8f22e04914bf428d185c254981

2. 编写方法

在 UserInterfaceInfoService 定义两个方法。

8c878a44a31c4be281afeb16f2a6898b

我们不应该将所有的服务都集中在 UserInterfaceInfoService 中,应该将它们分成多个独立的服务进行编写;

复制 yuapi-backend 项目的 InterfaceInfoService.java、UserService.java。

98b456bfa7dd4791b81955381e6e9d1a

粘贴到 yuapi-common 项目中。

065b7ca3a1dd496d81096ffb93ee3193

这两个文件还有爆红,删除引入,让它重新加载。

95279bfaa59e4d72bf5c6eb71e7cf104085655fe064f4775be8cc16d5126c583055a9187e0094c91b2881013eac4480820df4c09a0d94d9788eded67e1638846

把 UserService 里面的用户注册、登录... 删掉,不需要。

b26890bae2044258b48d286536041b82

写两个方法:

  1. 数据库中查是否已分配给用户秘钥(accessKey)
  2. 从数据库中查询模拟接口是否存在(请求路径、请求方法、请求参数)

首先,在 UserService 中写数据库中查是否已分配给用户秘钥的方法。

2f1d288ac5d244f49b5aa71f5e847bb0

其次,在 InterfaceInfoService 写从数据库中查询模拟接口是否存在的方法。

9dbc119794e345ed851361ab3de53fcb

给以下两个文件统一加上Inner的文件名前缀。

89b995ddfbfa4de68bf94c2cb83386a63bdf597c261e44e9ba14d941927a7868

删除多余的内容,没有继承它,修改后的三个文件:

206fdfb5f5bc491abb2d078d71302bc0d7c0fc14f65648fd9df220e504b51cdd802b4b8373f14ae1bfed7f7d612627d0

把 yuapi-common 项目打包,先设置它的版本为0.0.1

352e189036ae4619bf1d919e4745dbaa

点击右侧 Maven,双击 install 进行打包。

c6bfa6d4b9ad4b149668ec59324eef41

打包成功。

cff648d5d3814d49a9dfdf43b1044cfa

3. 引入公共服务依赖

复制公共服务版本信息。

00fba64eaf0847cab111e3bf9e264667
<groupId>com.yupi</groupId>
<artifactId>yuapi-common</artifactId>
<version>0.0.1</version>

粘贴到 yuapi-backend 项目中。

5728c9b0bb64406294580f66648c8458

💭 题外话:

在学习 Dubbo 的过程中,建议大家在学完 Spring Boot 和 Redis 之后开始学习。如果时间比较紧张,可以选择暂时不深入学习 Dubbo,但如果有半年或者更长时间来学习,建议在进入 Spring Cloud 之前先学习 Dubbo。这是因为 Spring Cloud 微服务架构的底层可能会使用 HTTP 协议或其他协议进行远程调用,微服务涉及到更多的知识领域,例如网关、分布式事务以及服务总线等多个概念。

而 Dubbo 作为一个 RPC 调用框架,更加专注于远程调用的领域,相对更为纯粹。因此,在掌握 Dubbo 的基础之后,再深入学习 Spring Cloud 将会更有帮助。


验证一下,看看 yuapi-backend 项目能不能用这个公共的类了;

把 UserInterfaceInfoService.java、UserInterfaceInfo.java 删掉。

57f6572505e6453a99d38d54cb5abafc

更换引入。

e59e9458dffb45f4bd824994767895c5332059827a0f4dcc832e704c694027e26b49d2d1bb7640f197b4b2c2fd4d0736b2b4d5f88d2c473e968c8ba584d096d2309ff620b557417580c8e3b6469da0e23d6c6ef6e1ab41f5893d56089708b348aaf67ef9f6ec4936995667494a5eb18433afc71bc351471cbe03a77fa8bf3f5673acf83d2b114429b1d5ef67408f76cde925ad8de6954187818192f51a746d2f0a78843782534ddbaac123e36b2fbf14f71c521cc1544a72b8264a7652e126af1b8435b88e9146ea92e5c41c2f8d7550366c4bc7338e4ed998838781ddad1e95950188617bbf4e35a35ba9d5c03f0ca376a2b91bf3ea48bb890f7c47e79e5c8c

把这个 User、InterfaceInfo 删掉,直接调用公共服务里的。

5f3d6517b3a342fd8d4abaddd71aedea

执行测试类,看看能不能运行成功。

75551b2fbd714abfa5dfb619f847bcf9

调用成功,也输出的 SQL 语句。

c5a0e0fc8f0c42a19384742aeb4baa35

现在公共服务算是抽取完成了。

4. 方法实现

接下来就去实现刚刚的那几个方法,点这里,找一下 yuapi-common 依赖。

dce984d424304bff950caec82c09f22c

找到 InnerUserService,来实现接口;

光标放到 InnerUserService,按[Alt+Enter],选择Implenment interface(实现接口)。

9f31fee0dd1a4858bd2cdb40039e0085

选择目标目录。

71f6d81d25fc4382bece3d53705963ec

实现方法就选 getInvokeUser。

5394ef5327da4c23a11c73f9d2663ca2

它就生成在我们指定的目录下了。

0aafef8706aa4be0948114fe5d5ebe3a

再实现 InnerInterfaceInfoService 的接口;

光标放到 InnerInterfaceInfoService,按[Alt+Enter],选择 Implenment interface。

0c19bdcff0b443a1aaeea179d23b9515

选择目标目录。

d83435ceeea2475b9245da5f4d0f494c

实现方法选 getInterfaceInfo。

ea8e9ff7798b4adf9c1d07babe29c357

生成好了。

df0d46a8dbf14333adadb0e20a13697c

再生成另一个,光标放到 InnerUserInterfaceInfoService;

按[Alt+Enter],选择 Implenment interface。

c3e9253cf22d4ee2a2bd21ebf1a44df8

选择目标目录。

af4b639a716349a39deb6ad351756320

实现方法选 invokeCount。

d6f51db97cc649199ca5aa199ebc8404

生成好了。

2defc143683044f5845c1c04189f3435

在实现类里,我们就可以编写对应的方法了。

先去写统计接口次数的这个类,直接引入 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);
    }

}
9a036a9c83554cd3ab571dc7fb10b48c

再去写下获取调用用户信息的类,直接去数据库中根据 ak、sk 判断获取。

55ed565442f44901977b07d700a06293
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);
    }

}
4dc4b9a504874e968521b96c17871381

最后写下根据请求路径、方法名获取接口信息的类。

5ed159c0452943da92194dfccc95a54c
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);
    }

}
6cd4c17be3214e3590f9a90763e5bc29

这三个接口就写完了~

大家还记得我们在使用 Dubbo 时需要做哪些事情吗?

参考下之前编写的DemoService,来看一下,它只需要在我们的实现类上添加@DubboService注解即可。

ffb9a9c3863c4f7fafab3b2a47b5dbbf

现在我们将刚刚的三个接口都加上这个注解,使它们也能够正常使用 Dubbo。

deeccd42da9c4bb4944d1edc52b48bbf8234162ea38544519e731216a7e3b0e7769e7c80c21d47f6b99b8f2efa994c59

在 impl 包下新建inner包,把内部和外部都隔离开,将三个接口都放进去。

4a2a362e4dc74cc3a7f09247fdc856c6

这些服务基本上写好了。

5. 调用公共服务

1. 在网关编写调用公共服务

以 debug 模式重启 yuapi-backend、yuapi-gateway 项目、启动 yuapi-interface 项目。

访问 nacos 注册中心 http://自己服务的open in new window IP 地址:8848/nacos/index.html,都注册上了。

c14d8ba3da6a4329ac4af5f95541ca19

如果发现 yuapi-backend 项目启动失败:

  1. 网速问题,网速太卡了,导致注册超时,等一会,重新启动
  2. 端口冲突,换个端口

ps.网速问题延伸,因为注册默认 3 秒,超过 3 秒即注册失败,可以去设置增加注册超时时间。

接下来就去网关项目中使用这些公共的服务,复制公共服务依赖。

25a86b5792574a83a207ca63a0ebf70f
<dependency>
  <groupId>com.yupi</groupId>
  <artifactId>yuapi-common</artifactId>
  <version>0.0.1</version>
</dependency>

粘贴到 yuapi-gateway 项目中。

253edcee709544b9b95d37eb1bf267df

进入到网关要操作的主业务流程里,先引入三个类:

获取用户是否存在、获取接口信息是否存在、统计接口调用次数。

749ac0d777dd4af9ac0770c55a91fac5

完成这步。

c324ec7f39ff4834a83a5f86804462e0

修改后:

85c587f3961c456eb0c2e4cb2d7c1863

往下滑,完善这步。

580202e3592040fbb0b0b8bd6a507ce6

修改后:

167dd2c6e2cc4700acba967637eeffc2

往下滑,来整这步。

5eb129c8de4a48319206f9ca347ac8c9

先提取下请求路径、方法。

c43d09ed569b456fbfa4c22b25ae9853

然后修改:

58fa88e4a9c44f01a35e2f0ddce88a19

💡 有同学提到了一种防止绕过网关的方法,即在网关上添加一个请求头。

实际上,是否添加这个请求头并不是特别重要,之前已经和大家讨论过,我们的目标是为了实现流量染色。

如果要添加这个请求头,可以在请求中再添加一个固定的标识,例如"yuapi",并给它一个任意的值,然后在客户端进行判断。

但是目前考虑到实际影响不大,暂时不添加这个请求头了。因为如果添加的话,客户端还需要进行额外的判断,在每个模拟接口处添加逻辑或者编写一个公共方法,这会变得比较繁琐。


往下滑,来整这步。

af44a12cd41340be890429f583e8d3d1

先添加两个参数:接口 id、用户 id。

a72f669734834d8d817ac129c8922d29

然后去修改:

bbe6f04249334b578739e48299ccfbea

ps.在生产环境中,通常公司都会接一个报警系统,以确保一旦出现问题,能够立即触发报警机制。

2. 调用测试

重启 yuapi-gateway 项目,有小报错没关系,能显示之前的示例结果即可。

dcdc61cd312a4192b001830deb4ca519

打个断点。

ffa268ec13544d54a930e80d5ec4a47b

访问 http://localhost:8090/api/name/get?name=yupi,自动跳回 yuapi-gataway 项目。

018071e1359b4acabd4716c7f4cf6128

按[F8]执行到这里,你会发现它的请求路径是 /api/name/get。

413beb1f56764703bc9959be0d459ca3

按[F9]结束执行,这个有点问题,我们后端数据库存的是 http://localhost:7529/name/user。

1edf7b715e07448ea90d9ff32e352fd7

临时问题:如何获取接口转发服务器的地址

**思路提供:**网关启动时,获取所有的接口信息,维护到内存的 hashmap 中;有请求时,根据请求的 url 路径或者其他参数(比如 host 请求头)来判断应该转发到哪台服务器、以及用于校验接口是否存在。

这里的参数比如说,在客户端调用接口时使用。你需要获取特定接口的信息,例如其中的主机(host)信息。在客户端发起请求时,你可以将这个主机信息添加到请求头中。这样,网关就能够获取到这个请求头。

进一步说明:

就是根据用户请求的地址,我们要找出需要转发的目标服务器地址。为了获取类似于 localhost:7529 这样的地址,我们需要进行一些步骤,**如何实现这一步骤呢?**建议从数据库中获取这些信息。这里提供一个参考方法,但值得注意的是,这一步不是必需的,可以根据个人情况决定是否采取这一步。

**具体方法如下:**当我们启动网关项目时,首先从数据库中获取所有接口信息。对于每个 URL,我们需要获取与之关联的路径以及接口名称信息。之后,根据接口的其他相关信息,我们可以找到应该转发到的目标主机(host)。可以为数据库添加一个额外的字段,用于存储转发的目标服务器地址,通过这个地址,我们可以将请求转发到对应的路径上,这就是大致的流程。


偷个懒,因为我们所有的模拟接口项目都位于同一个地址(8123),直接将这个地址进行拼接。

通常情况下,如果你自己在开发类似的模拟接口平台,也可以像这里一样,事先写入几个固定的地址,避免每次都需要从数据库中读取的繁琐过程。

写一个地址的常量,然后将其拼接。

18e7ed5545e94177913caf84f5e66205

重启 yuapi-gateway 项目,打开 yuapi-client-sdk 项目;

这个项目也要去引入公共服务。

076563884edb4d1281f5b75879df540a

💡 有同学问:如何让其他用户上传自己编写的接口?

需要提供一个注册机制。在这个机制下,其他用户可以上传他们自己编写的接口信息。为了简化流程,可以设计一个用户友好的界面。在这个界面上,用户可以输入他们的接口信息,包括服务器地址(host)、请求路径等内容。也可以规定,在接入我们的平台时,用户必须使用我们提供的 SDK 或遵循一定的要求。

**如何进行接入和要求的遵循?**在用户上传接口的时候,我们需要对接口信息进行测试调用,以确保接口的正常运行,这可以通过我们的平台来完成。同时,我们也可以要求用户标明该接口是否是由我们的网关调用,这可能需要用户在代码中加入判断代码,或者引入我们提供的 SDK 来实现。

**接口信息的组织和存储:**当用户上传接口信息时,这些信息将被存储在 InterfaceInfo 接口中。除了 URL 外,还应该添加一个 host 字段,用于明确区分不同服务器的地址。这样,可以更清晰地区分请求路径和服务器地址,提高接口信息的可读性和可维护性。


我们回到前端去调用,不在浏览器调用了,这样的话密码也绕不过去。

启动前端项目,访问 http://localhost:8000,登录,就调用第一个接口,点击查看open in new window

3a3dc55cf85d408991815cbf6f047506

输入请求参数后,点击调用。

▼json

复制代码{"username":"yupi"}
16c2807698334da1bc3dd814f92b4256

自动跳回 yuapi-gateway 项目。

97087049c6f2499d859e96d41701f27c

按[F8]向下执行到这里,请求来源是 127.0.0.1。

31041498dae748a08fa55d9b167bcb10

按[F8]向下执行到这里,参数都拿到了。

3e9562dd2b7e4289b75e121c7a743848

再往下,就是远程调用了,在 yuapi-backend 项目打个断点。

541cc8da87484da8aeeb7f1a797be629

回到 yuapi-gateway 项目按[F8],就会跳到我们刚刚在 yuapi-backend 项目打断点的地方。

6debbe2761714f9e8ee6039900d406ec

按[F8]跳到下一行,再按[F9]结束执行,然后把 yuapi-backend 项目断点取消。

fd84e31df24546ad8c08969403108b0a

回到 yuapi-backend 继续按[F8]向下执行。

1b5859d7cd814112ba5b7905dd2f4a19

执行到这里,发现它显示没有查到,按[F9]结束执行。

38c123f542214d588f2cc4bbdd4ed567

把这里的断点取消。

521429ce580e438fb658102f51fb8d8f

往下滑,在这里打个断点。

b9b9b3857078457f8f1f8d86234c5de2

去数据库人工修改下 url 的数据为 http://localhost:8123/api/name/user,先把整个流程跑通。

7824105b45fa41e0910c40f17760c652

不过啊,因为我们登录的是 yupi 账号,如果登录其他账号就会报无权限错误,为什么呢?

因为我们之前的接口逻辑是写在模拟接口项目里的,我们现在的校验逻辑不应该写在这里;

注释掉,重启模拟接口项目。

14f6adadf7354b50bc98b5fd3176f88e

在前端重新点击调用,自动跳转回 yuapi-gateway 项目。

52cad00c09144048aad3b6443464f234

这次拿到了,按[F9]结束执行。

ed1456110eb64e4096a204a553646911

返回前端页面查看,成功输出。

9edf84008dcf4d6b9262e15eb7611a3f

去数据库中查,也能看见次数的变化,现在整个业务流程跑通。

2409a5ef40674883908502be57f5ab71

ps.还有个小问题,我们的网关没有校验用户是否还有调用次数,这个一定要放在发送请求前,交给大家实现🐶。

9910ec67c7b84dd29dbcb068b95a2d1e

我们回顾一下之前的业务流程,看一下用户是如何获取到他们的 AKSK 用于模拟调用接口的。

找到 InterfaceInfoController 中的 invokeInterfaceInfo 方法。

那么在这个方法中,我们是如何获取 AKSK 的呢?

首先,用户需要进行登录,我们就能从登录用户的信息中提取出他们的 AKSK。因此,可以说我们之前已经实现了动态的操作,能够在运行时获取用户信息并为他们分配密钥了。

69f31268887c4885aa81f148fa78cca2

所以 application.yml 这里写什么都无所谓了。

0ab5d46e119c475a82a9eedcdde6978b

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;

接下来,进行测试,先往数据库中添加一些数据,模拟不同接口被不同用户调用的情况,然后运行这个查询语句,查看结果是否符合预期。

edd7a5e833434f86bf7c4498e43c7532

💡 有同学说:有必要增加热点推荐功能吗?

个人认为没有必要,我们不必过于复杂化这个系统,如果我们的接口数量很少,甚至可以在一个屏幕上展示出来,那么就没有必要考虑热点推荐功能。


打开 SQL 控制台。

ceed599e11da410fb470f0eb1991356d8a4172ef03914ec19079d896aa6beaff

将 SQL 语句粘贴进去,按[Ctrl+A]全选,点击绿色按钮执行。

4d43e022ca874b6ca113e66a19e40240

下方显示查询结果。

7a3b81423f444561bb8830c4950d96e2

2. 业务层去关联查询接口信息

在获取了这个 SQL 查询结果之后,还有其他需要处理的事情吗?

现在我们只拿到了接口的 id,但如果除了 id,还需要展示接口的名称,应该如何处理呢?

需要进行关联查询来获取接口信息。由于我们只取了前三,所以在这种情况下进行关联查询不会对性能产生太大影响,如果大家要展示所有接口,关联查询可能就会变得复杂。

回到 yuapi-backend 项目,新建一个 controller:AnalysisController。

bbdef92bdc4c4a9aa64f6f45d8c02f9b

先写一点内容。

03e01dca739747da996c5bd2866b24a7

再新建一个包装类,复制 PostVO.java,粘贴到 vo 包下,并重命名为InterfaceInfoVO

55d42a6c4138451f9ec33913c3b909c5

然后修改一下 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;
}
6827e0981b524e72976af279210e45e6

由于我们需要查询,涉及到两个表,user_interface_info 表和接口信息表。因此,在这种情况下,我们可以在映射层(Map层)编写一个自定义的 SQL 查询语句,然后在业务层实现下面的关联查询。

找到 UserInterfaceInfoMapper 整个自定义 SQL,把刚刚编写的 SQL 语句粘贴进去。

49d5511acf53483a872a428b2355f553

编写代码。

6bd207a0688647b3a9f55a04873db827

把光标放到 listTopInvokeInterfaceInfo 中,按[Alt+Enter] 生成 statement。

5ca712c0cd694f8383bf41fba471eb5b

自动生成了对应的 map。

0c35810a3a4346e3962257637387c11a

把 SQL 语句粘贴过来。

28511f62156146d5ac72a94ee753eb41374a87e950eb4c27ae1d24d6035fd143

我们是**如何处理这个业务的呢?**我们并不是直接在代码中编写 SQL,而是先将 SQL 写好,然后在数据库中执行它,确保 SQL 的正确性。然后再开始编写代码,这样可以节省很多时间。

如果你遇到业务逻辑问题,怀疑是数据查询的问题,可以先打印相关日志,并将 SQL 语句复制到数据库中执行一次。这样可以确定是业务代码问题还是 SQL 语句问题。

然后修改一下,把 limit 改成动态参数。

3a38eeb7c3f640c3898366bc4222401a

继续编写 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 更被广泛推崇。

如何使用图表库:

  1. 看官网
  2. 找到快速入门、按文档去引入库
  3. 进入示例页面
  4. 找到你要的图
  5. 在线调试
  6. 复制代码
  7. 改为真实数据

如果是 React 项目,用这个库:https://github.com/hustcc/echarts-for-react。

2. AntV 官网浏览

访问 AntV 官网 —— AntVopen in new window

cee1094302694c1db8c02ece52ed3d03

AntV 是阿里蚂蚁金服开发的一套数据可视化库,用于数据呈现与展示。大家以后需要进行数据可视化,强烈建议使用 AntV。这个库拥有丰富的功能和一系列的产品,其界面设计也相当出色。

AntV 不仅仅是一款数据可视化工具,它还包括了多个子库。

其中,G2 专注于 PC 端图表的展示与绘制;

b42f7abbbf7a4384a2b44f94397ba76f

S2 则提供了多维表格的功能,适用于专注于表格数据的场景;

242014958568402f8ca1cab431736a9b

G6 则用于处理关系型数据的可视化,尤其适用于图论和模型分析等领域;

c08e2ccbcd95481ba16b6af1343fe6c7

而 X6 则是一套流程编辑引擎,可用于创建流程图和 ER 图等,非常适合构建流程图系统。

8b9c3f63387c4f4dabbf447b64b72080

L7 是更高级的数据可视化库,它专注于地理位置城市化、空间城市化以及地理数据的可视化呈现,这使得它在展示地理信息方面具有非常出色的表现。

c7fa9dc48662489c85f50e1443058a69

F2 则专注于移动端图表的展示与绘制。与之对应的,G2则是适用于 PC 端的图表库,两者在不同终端上都能够提供高效的数据可视化。

25440c9870474518941f80fc342fc9ef

AVA 并不是针对普通开发人员的工具。它更适用于对框架进行深度二次开发,或者研发自己的技术框架,需要进行更加复杂的数据分析、定制化数据分析等等。一般来说,大多数开发者并不需要使用 AVA。

bebf695e4bf0478097491a43174ea973

接下来,对于我们的 PC 端,可能会使用 G2 这个库。如何使用呢?后面可能还会给大家更详细地介绍,但本期先给大家快速地看一下,其实所有这些可视化库都有相似的用法:

进入官方网站,然后浏览网站上的图表示例。你可以只专注于图表示例,无需浏览其他内容,直接找到你需要的图表样式。

e1303d0775234294b5c0f64fe86fb7ce

找到饼图,点击进入。 —— 饼图open in new window

842fb3e26c104ac8b10caeeda951170c

右侧有现成的代码供你使用,无需过多考虑,只需要将代码复制粘贴到的项目中。

接着,将后端实际获取到的数据替换掉原始代码中获取数据的部分,这里就不介绍这个库的使用方法,因为它的用法非常简单。所有这些可视化图表库的用法都类似,它们没有太大的学习成本,除非你要深入研究这些内容。

db9b0f35d49f479284d871438f1151b6

3. ECharts 使用

访问 ECharts 官网,点击示例。 —— EChartsopen in new window

ps.百度已将 ECharts 捐赠给了 Apache 基金会。

f83a6a3f33924785bfaf6bb47efbafa0

找到 饼图open in new window,就用第一个。

74557aeda22b4333956baa0b9e688f1d

可以在这里调试。

42ae6a40349148189fc35d0b44b16c17

比如这里改成 '谷牛'。

49195591f0c944b5a6a0395e706cd059

然后复制代码。

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,去 githubopen in new window 找 react 版的,搜索:react-echartsopen in new window

a28fca2937e4450195eaa0f48788efbb5b821456a8b84410b55bd7afbdd2c331

往下滑,复制安装命令。

41a018f4296c4bf1997b2c6b5849351f
▼bash

复制代码npm install --save echarts-for-react

npm install --save echarts

粘贴到前端的终端中执行。

b62394c005ad4541a79c98eeae9019d8ddbc800244ec42aa8e8332ae9aaf613c

安装之后,回到 github 往下看,看它怎么用。

只需要将其引入,引入后,需要使用 ReactECharts 组件,这里有一个 option 选项。

14eda4d8a1c1447e907e730e4f668f6e
import ReactECharts from 'echarts-for-react';

// render echarts option.
<ReactECharts option={this.getOption()} />

option 选项对应我们示例代码中的这里。

9e455fee6a23484b95d0b716bc325b53

4. 新建分析页

去 routes.ts 增加新的路由。

e03ef1b07a754ba3b7ee1b475ddd498d

在 Admin 目录下新建InterfaceAnalysis目录,复制 index.tsx,粘贴到新建的目录下。

e7cc3eb28a424d3f94bc241fa49b8d3c

删除 index.tsx 多余的内容,并修改一下。

17e8204989e14c8f954f482b71c2675b

引入 ECharts。

2e38000966d54b04ab68695731867e6e

然后把示例代码粘贴进来,修改一下。

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;

回到前端页面查看效果,成功展示出来了。

a0e3e8281f1d43dc9844bc14747a9850

我们刚刚在后端新写了一个接口,在前端用 openapi 生成。

153a5a0072d74805a659749bf5c81386

继续编写分析页的代码。

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;

回到前端页面查看效果。

0b47a72755d4467d87472e69ff826689

如果要改样式,先在 ECharts 调整好,再粘贴到项目中即可。

84c8ba623c5b4698a7a7a46b78f886ca

开发完咯~ 下期不再见<( ̄ˇ ̄)/

6. 上线计划

**前端:**参考之前用户中心或伙伴匹配系统的上线方式。

后端:

  • backend 项目:web 项目,部署 spring boot 的 jar 包(对外的)
  • gateway 网关项目:web 项目,部署 spring boot 的 jar 包(对外的)
  • interface 模拟接口项目:web 项目,部署 spring boot 的 jar 包(不建议对外暴露的)

关键:网络必须要连通

**自己学习用:**单个服务器部署这三个项目就足够。

如果你是搞大事,多个服务器建议在 同一内网 ,内网交互会更快、且更安全。

7. 扩展思路

  1. 用户可以申请更换签名
  2. 怎么让其他用户也上传接口?
  • 需要提供一个机制(界面),让用户输入自己的接口 host(服务器地址)、接口信息,将接口信息写入数据库。
  • 可以在 interfaceInfo 表里加个 host 字段,区分服务器地址,让接口提供者更灵活地接入系统。
  • 将接口信息写入数据库之前,要对接口进行校验(比如检查他的地址是否遵循规则,测试调用),保证他是正常的。
  • 将接口信息写入数据库之前遵循咱们的要求(并且使用咱们的 sdk),
  • 在接入时,平台需要测试调用这个接口,保证他是正常的。
  1. 网关校验是否还有调用次数
  • 需要考虑并发问题,防止瞬间调用超额。
  1. 网关优化
  • 比如增加限流 / 降级保护,提高性能等。还可以考虑搭配 Nginx 网关使用。
  1. 功能增强
  • 可以针对不同的请求头或者接口类型来设计前端界面和表单,便于用户调用,获得更好的体验。
  • 可以参考 swagger、postman、knife4j 的页面。

上面提到的要检查用户提供的地址是否符合规则。我们的项目中,所有的模拟接口都是以 /api 开头,或者说,我们规定所有模拟接口的地址都必须以 http://localhost:8123open in new window 开头。你可以根据需要自行设置规则,然而,在制定这些规则时,最好不要过于严格,最好像之前提到的,可以在数据库中存储一个 host 的信息,只需满足一些特定的后缀条件,比如以 /api 开头等。

这个项目实际上有很多可以发展的方向,有许多要考虑的要点。如果你希望项目取得成功,就需要考虑许多方面。你需要制定规范,关注安全性、性能等方面。此外,还需要考虑如何防止被滥用。


有同学问:Dubbo 不是需要暴露服务才可以吗?

我们这里不是那个模拟接口用 Dubbo 去暴露的,**我们暴露的是什么?**暴露是系统内部用的接口,什么查询数据库中是否给用户分配密钥、接调用接口次数统计。暴露的不是开发者提供的接口,开发者提供的接我们是通过网关去转发的。然后这里你如果想让网端直接通过同一套地址去调用,那你就让用户遵循这个地址规则。

有同学问:现在的 SDK 不是固定了方法吗?

对于这个问题,需要明确一个**核心点:**一旦接口投入使用后,肯定要针对这个接口进行 SDK 的开发。你需要不断地完善 SDK,使其适应不断变化的需求。当然,也可以让 SDK 从接口信息表中读取信息,然后动态生成方法等等,这种做法也是可行的,然而,并不建议这样做。实际上,接口的发布可能不太像我们在应用商店发布应用那样灵活,在这种接口的发布过程中,建议还是介入一点人工。

以腾讯云的 SDK 手册为例,它有许多不同的 SDK,尽管代码可能是自动生成的,但是它的 SDK 是根据接口动态变化的。这意味着它并不是为 100 个接口提供同一个代码供调用,相反,它会为每个方法、每个接口生成相应的方法,同样,它也会根据不同的地址生成相应的方法。这一点非常重要,请大家多去使用一下第三方的 API 接口,这样就会更了解对方的 SDK 是如何设计的。

你可以去腾讯云下载他们的 SDK,会发现其中包含大量方法,基本上每个接口都有相应的方法。这样做的目的是让使用者更加便利,他们只需要输入方法名就能实现操作,无需关心具体的接口地址。

有同学问:就是每次发布一个接口,都需要更新一次 SDK?

是的,实际上每次发布一个新的接口,都需要对 SDK 进行更新。建议的做法是这样的:由于用户并不关心具体的接口地址,因此你可以让他们直接调用方法名,然后根据这些方法名去动态生成对应的方法。每次发布新接口时,更新 SDK 的操作可以做得非常简单,可以采用脚本的方式,从数据库中读取接口地址,与之前已有的地址进行对比,然后补充相应的方法,这个过程是可行的,事实上,很多公司都在这样做。