跳至主要內容

【Da-API】⑥分布式改造&RPC

holic-x...大约 22 分钟项目Da-API平台

【Da-API】⑥分布式改造&RPC

1.RPC框架引入

网关业务逻辑梳理

问题:网关项目比较纯净,没有操作数据库的包、并且还要调用之前项目写过的代码,复制粘贴维护麻烦。

理想:直接请求到其他项目的方法(调用)

​ 目前网关项目遇到一个问题,就是网关项目比较纯净,没有涉及数据库操作的包,但同时需要调用之前编写过的代码。尽管复制粘贴一开始并不麻烦,但是随着次数增多以及未来的修改维护,就变得相当繁琐了。(考虑系统迭代的可维护性和可扩展性)

​ 因此,理想情况就是希望能够直接请求 api-platform-backend 项目中的 invokeCount 方法,此处引入一个概念:远程过程调用(RPC)

RPC概念引入

​ 场景分析:如果你在项目 A 中编写了一个非常有用的函数,现在你在项目B中也想要使用这个函数。但问题是,项目A和项目B是独立运行的,它们不共享同一片内存,也不在同一个进程中。那么,如何能调用项目 A 中的那个函数呢?即如何调用其他项目的方法?

​ 解决方案:

【1】最笨拙的方式就是copy:复制代码、依赖、环境等,在自己的项目中引用一份一模一样的版本,但是这种方式会导致后期维护更新繁琐(可能引发环境依赖和代码问题,因为项目之间各自有独特的设置和条件)

【2】抽离公共代码块,打包成jar供其他项目引用(例如客户端SDK,参考本项目中的api-platform-sdk)

【3】通过HTTP请求调用:即让可复用代码的项目方提供一个接口供其他项目进行调用(在api-platform-backend项目的invokeCount统计次数调用方法,可以将其写到 controller 中并为其创建一个 API 接口,以便其他项目可以调用,这个方法是可行的,但同样需要注意它的限制和适用性)

【4】RPC(需区别于HTTP 请求)

HTTP请求调用 VS RPC

HTTP请求调用的构建思路:

【1】提供方开发一个接口(提供地址、请求方法、参数、返回值)

【2】调用方使用HTTP Client 工具操作发送HTTP请求,处理响应数据

RPC构建思路:像调用本地方法一样去调用远程方法,以项目中接口调用次数统计方法invokeCount为例

image-20240315212918489

HTTP请求调用 VS RPC:

【1】RPC对开发者更为透明,减少了开发过程中的沟通成本

【2】RPC向远程服务器发送请求的时候未必要使用HTTP协议,还可以使用TCP/IP等,性能更高(内部服务更为适用)

扩展说明:RPC 和 HTTP 请求在本质上有一些区别。RPC 相对于 HTTP 请求更加透明,这意味着它在开发者间更具透明度。这种透明性是如何理解的呢?一个广泛的定义是,RPC 的目标是使远程方法的调用就像调用本地方法一样,从而实现更为便捷的远程通信。

​ 以 HTTP 请求为例,调用方需要完成诸多步骤。首先,它需要发送 HTTP 请求,使用一个 HTTP 客户端的代码库。以api-platform-client-sdk 项目为例,可以看到客户端对象是如何使用 HTTPUtil 这个 hutool 工具类的包来发送请求的,这意味着调用方需要编写特定的代码段来发送请求,然后处理返回值,可能还需要解析返回值并封装参数的映射关系等。

​ 以api-platform-gateway为例,在不引入数据库的基础上希望通过调用api-platform-backend提供的接口完成交互,因此需要封装backend指定的请求参数、请求方式、请求头等进行交互,交互完成还需进一步处理响应数据(例如将code、data、message等响应数据解析出来),这些都是需要编写额外的代码进行处理

​ 但是如果通过RPC方式,就能实现与调用本地方法类似的体验。例如在api-platform-backend项目中的UserInterfaceInfoService提供了invokeCount方法,则只需一行代码:userInterfaceInfoService.invokeCount,就可以调用该方法。RPC可以直接指定要传递的参数,并且也能够直接获得返回值,无论是布尔类型还是字符串。RPC 方式不需要像 HTTP 请求那样进行额外的封装,但如果需要封装,当然也可以根据需求自行处理。因此,可以说 RPC 的主要目标之一是模仿调用本地方法的方式,从而实现远程调用。

​ RPC 的最大作用在于模拟本地方法调用的体验。看上去是请求本地代码,实际上,它可能会请求到其他项目、其他服务器等等。这就是 RPC 的最大价值所在。它最大的优势在于它的透明性,你不需要了解它是如何在 HTTP Client 中怎么封装参数,只需直接调用现成的方法即可,这样可以大大减少沟通成本

扩展问题:feign 不也是动态生成的 httpclient 吗?

​ Feign 本质上也是动态生成的 HTTP 客户端,但它在调用远程方法时更加精简了 HTTP 请求的步骤。尽管 Feign 使远程调用看起来像是调用本地方法,但实际上与 RPC 仍然有一些微小的区别。虽然两者都可以实现类似的功能,但它们在底层协议上存在差异。

​ RPC(Remote Procedure Call 远程过程调用) 的一个关键优势在于,它可以使用多种底层协议进行远程调用,而不限于 HTTP 协议。虽然 HTTP 协议可以实现类似的功能,但考虑到性能,RPC 可以选择更原生的协议,如 TCP/IP。而且,网络上还存在其他性能更高的协议,可以根据需要进行选择。

​ 在微服务项目中,对于内部接口,使用 RPC 可能会获得更好的性能。然而,选择使用 Feign 还是 RPC 取决于具体的技术需求,没有绝对的优劣之分。需要注意的是,RPC 和 HTTP 请求可以结合使用,因为 RPC 的底层协议可以是 HTTP,也可以是其他协议,如 TCP/IP 或自定义协议。

综上所述,RPC 和 HTTP 请求是可以互相结合的,但 RPC 在协议的选择上更加灵活。当比较这两个之间的作用,首先强调 RPC 的主要作用,然后阐述 RPC 和 HTTP 之间的关系。

RPC工作流程

​ 参考上述图示,可以对RPC的一些核心概念进行理解分析

提供者(Producer/Provider): 需要一个项目来提供方法,这个项目被称为提供者。它的主要任务是为其他人提供已经写好的代码,让其他人可以使用

调用方(Invoker/Consumer): 一旦服务提供者提供了服务,调用方需要能够找到这个服务的位置。

存储: 公共存储用于存储提供者提供的方法信息。调用方可以从存储中获取提供者的地址和方法信息(存储有时也会被称为注册中心,它管理着服务信息,包括提供者的 IP 地址等等)

​ 以项目中的invokeCount为例,提供者api-platform-backend提供invokeCount方法并将其放在一个存储器中,这个存储器中存储了提供者的地址、提供方法等信息,而调用方可以通过这个存储器去跟踪发布在上面的公共方法并获取到相关的信息进行调用

​ 但需要注意的是在整个流程中,最终的调用并不是由注册中心来完成的。虽然注册中心会提供信息,但实际上调用方需要自己进行最后的调用动作。注册中心的作用是告诉调用方提供者的地址等信息,然后调用方会根据这些信息来完成最后的调用。 ​ 一般情况下,调用方会直接寻找提供者进行调用,而不是依赖注册中心来完成实际的调用过程。注册中心主要的功能是提供地址信息,而并不会承担将调用方所需的内容传送到提供者的角色,整个过程遵循这样的流程。

2.RPC业务引入

Dubbo框架

​ 基于对RPC框架的理解,此处项目选择Dubbo框架作为学习参考

​ Dubbo-阿里系是目前国内非常主流的 RPC 实现框架。当然,还有其他类似的框架(GRPC-Google系 和 TRPC-腾讯系)

​ 理解Dubbo框架基本思想,阅读 Dubbo 框架的官方文档open in new window,在入门应用的基础上再去深入学习

启动流程:先启动注册中心、后启动服务提供者、最后启动服务消费者

以项目应用为例:理清注册中心、服务提供方、服务消费者的主要职责

​ api-platform-backend作为服务提供者,提供3个方法供远程调用

【1】ak鉴权:去数据库中查ak是否已分配给用户

【2】校验模拟接口:从数据库中查询模拟接口是否存在,以及请求方法是否匹配(还可以校验请求参数)

【3】接口调用次数统计:调用成功,接口调用次数 + 1 invokeCount

​ api-platform-gateway作为服务消费者(服务调用者),通过注册中心提供的信息调用方法

Nacos

​ PS:在使用各种框架和库时,尽量避免将项目存放在包含中文字符的路径下。一定要注意,路径名称不能包含中文字符,因为如果不小心将项目放在中文路径下,可能会导致各种莫名其妙的错误出现

Nacos官方文档参考open in new window

【1】Nacos安装配置

Nacos下载open in new window,使用官方推荐的稳定版本2.1.1open in new window,将nacos.zip包下载完成解压

启动nacos

# Windows(进入bin目录执行指令)
startup.cmd -m standalone

​ 单机模式运行,启动nacos

image-20240315220928487

【2】整合Nacos注册中心

​ 参考Nacos配置文档open in new window

查看nacos注册中心

​ nacos正常启动后可访问注册中心启动地址:http://[IP]:8848/nacos/index.html#/login,默认用户名密码nacos,因为服务还没注册上所以里面是空的

image-20240315221723035

image-20240315221840708

设置api-platform-backend、api-platform-gateway 项目的依赖设置

<!-- RPC构建引入,引入Dubbo、Nacos注册中心 -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>3.0.9</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
            <version>2.1.0</version>
        </dependency>

application.yml配置修改:分别在api-platform-backend、api-platform-gateway引入相关配置

修改api-platform-backend、api-platform-gateway的application.yml配置
# Nacos & Dubbo 配置(以下配置指定了应用的名称、使用的协议(Dubbo)、注册中心的类型(Nacos)和地址)
dubbo:
  application:
    # 设置应用的名称
    name: dubbo-springboot-demo-provider
  # 指定使用 Dubbo 协议,且端口设置为 -1,表示随机分配可用端口
  protocol:
    name: dubbo
    port: -1
  registry:
    # 配置注册中心为 Nacos,使用的地址是 nacos://localhost:8848
    id: nacos-registry
    address: nacos://localhost:8848

​ 随后分别启动,查看项目是否正常运行(注意启动流程:注册中心、服务提供方backend、服务消费者gateway)

整合实例配置

​ 先关闭两个项目的启动,随后将核心代码引入项目中

​ api-platform-backend创建provider文件夹编写DemoService和DemoServiceServiceImpl,随后在项目启动类中添加@EnableDubbo注解

DemoService.java:
/**
 * 示例服务
 *
 */
public interface DemoService {

    String sayHello(String name);

    String sayHello2(String name);

    default CompletableFuture<String> sayHelloAsync(String name) {
        return CompletableFuture.completedFuture(sayHello(name));
    }

}

DemoServiceImpl/**
 * 示例服务实现类
 */
@DubboService
public class DemoServiceImpl implements DemoService {

    @Override
    public String sayHello(String name) {
        System.out.println("Hello " + name + ", request from consumer: " + RpcContext.getContext().getRemoteAddress());
        return "Hello " + name;
    }

    @Override
    public String sayHello2(String name) {
        return "hi noob";
    }

}
MainApplication 添加@EnableDubbo注解
@SpringBootApplication(exclude = {RedisAutoConfiguration.class})
@MapperScan("com.noob.springbootinit.mapper")
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@EnableDubbo
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }

}

api-platform-gateway创建provider文件夹编写DemoService(此处需注意DemoService与backend提供的包名要一致),随后修改项目启动类

package com.noob.springbootinit.provider;

import java.util.concurrent.CompletableFuture;

/**
 * 示例服务
 */
public interface DemoService {

    String sayHello(String name);

    String sayHello2(String name);

    default CompletableFuture<String> sayHelloAsync(String name) {
        return CompletableFuture.completedFuture(sayHello(name));
    }

}

​ 先启动api-platform-backend,随后启动api-platform-gateway,启动后确认提示信息,发现backend服务被正常注册,但是gateway却没有正确找到该服务(检查nacos主页看服务是否正常配置)

image-20240315224249431

image-20240315224229751

image-20240315224513533

​ 从提示信息可以看到,backend中的DemoService服务是放在com.noob.springinit.provider目录下的,而gateway的DemoService却一开始被放在com.gateway下,导致DemoSerivce没有正确映射(调整DemoService位置再次启动查看,如下图所示 缓存问题可以忽略,继续往下跟踪查看service是否正常启动)

image-20240315224729472

PS:可能会出现的其他问题,例如端口号被占用等问题,dubbo 端口配置是-1(随机端口),系统有时候可能会把把两个项目都随机成相同的端口,所以我们只需把区分不同项目端口配置的端口号即可

实例整合要点

【1】启动流程:先注册中心、后服务提供方、最后服务消费方

【2】注解设置:例如启动类的EnableDubbo、接口实现类以及Bean引用注解等

【3】application.yml配置(注意启动端口号不要冲突)

【4】服务提供方和服务消费方都尽量引用相同的依赖和配置

3.网关模块优化

网关业务逻辑梳理

网关需要调用backend的什么方法

可以确认下api-platform-gateway下CustomGlobalFilter目前还没有完善的功能点,基于这个调调做完善处理,依次开发完善

【1】ak鉴权:去数据库中查ak是否已分配给用户

【2】校验模拟接口:从数据库中查询模拟接口是否存在,以及请求方法是否匹配(还可以校验请求参数)

【3】接口调用次数统计:调用成功,接口调用次数 + 1 invokeCount

​ 一般情况下,可能会直接考虑在api-platform-backend中直接定义这几个方法,随后再在gateway中通过RPC框架进行调用。但是考虑到一些通用的场景分析以及后续代码可维护性,此处选择构建一个抽象公共服务,让其中的方法、实体类在多个项目间进行复用,进而避免重复编写代码。而这个服务的抽取最优先考虑的就是上面的三个功能,必须先明确那些通用服务是必要的

服务抽取:

【1】数据库中查是否已分配给用户秘钥(根据 accessKey 拿到用户信息,返回用户信息,为空表示不存在)

【2】从数据库中查询模拟接口是否存在(请求路径、请求方法、请求参数,返回接口信息,为空表示不存在)

【3】接口调用次数 + 1 invokeCount(accessKey、secretKey(标识用户),请求接口路径)

步骤:

【1】新建干净的 maven 项目,只保留必要的公共依赖

【2】抽取 service 和实体类

【3】install 本地 maven 包

【4】让服务提供者引入 common 包,测试是否正常运行

【5】让服务消费者引入 common 包

​ 从每个服务接口功能分析,关注这个方法的输入和输出,例如某个功能要提供一个公共服务接口,需确认它需要接收、传递什么参数

​ 以根据ak校验其是否已经分配给了某个用户为例,xxx

构建公共服务项目

【1】创建api-platform-common

​ 创建api-platform-common,并配置maven依赖

image-20240315231334189

image-20240315231545464

【2】抽离公共模块

​ 本质上就是将一些公共的实体、方法定义抽离到api-platform-common中,其作用类似于api-platform-client-sdk,但此处需要区分的是,它抽离的是一些公共的实体、接口定义类,而不提供接口实现,具体接口实现还是要通过具体的项目中进行定义。

​ 因为如果要通过RPC构建不同项目的方法调用,则不免会有一些公共的代码的维护,在一些普通的应用场景开发中可能copy一份,但是对于系统长期迭代和项目维护来说是不友好的,因此通过构建公共模块将一些通用的东西进行抽离。例如api-platform-backend的model定义以及注册接口定义

pom.xml:依赖应用尽量与api-platform-backend的保持一致,删除多余的依赖,这个公共服务只负责提供公共的实体定义和接口,而不负责具体的实现


​ 迁移api-platform-backend的model/entity、enums、vo定义(这些都是项目中一些公共的,多处需要引用到的内容),可以先将其copy到common项目下,随后再在backend项目中删除已经迁移的内容(遇到飘红则相应进行修改,将原有的项目引用调整为依托于api-platform-common这个公共jar的内容)

(1)迁移api-platform-backend的model/entity、enums、vo定义(可以适当清理掉一些与当前项目无关的内容)

(2)定义service接口:只做接口定义不作实现,其中定义三个要对外提供的接口(InnerUserService、InnerInterfaceInfoService、InnerUserInterfaceInfoService)

(3)构建完成借助maven导出common依赖(参考api-platform-client-sdk项目)

image-20240315234917485

【3】api-paltform-backend引入公共模块&Nacos注册

​ api-platform-backend在pom.xml中引入api-platform-common的依赖,随后清理掉已经迁移出去的公共类,将其引用一一进行调整(ctrl+B编译,调整飘红报错信息即可,其主要在于调整原有引入类的包路径),相应的mapper.xml文件中引入的配置也要做更改,调整完成重启项目查看项目功能模块流程是否正常

​ 随后在api-platform-backend项目中实现接口方法(对应修改文件InnerInterfaceInfoService、InnerUserInterfaceInfoService、InnerUserService)

​ 可以借助快捷键快速生成Service实现类。找到指定的要实现的Service点击选中按Alt+Enter,随后选择要生成的实现类和方法

image-20240316080046451

​ 依次构建完成可以看到对应项目service/inner分别生成InnerInterfaceInfoServiceImpl、InnerUserInterfaceInfoServiceImpl、InnerUserServiceImpl三个实现类,依次实现方法即可

​ Dubbo配置:基于上面配置的基础上,如果需要对外注册这三个接口,则在api-platform-backend中需要在相应的Service实现上加上@DubboService注解,并在项目启动类中也加上@EnableDubbo。

​ 构建完成,随后启动注册中心、启动api-platform-backend检查方法是否正常发布(网速问题延伸,因为注册默认 3 秒,超过 3 秒即注册失败,可以去设置增加注册超时时间),如果注册失败相应检查上述步骤(注解配置等),注册完成则在nacos可以看到其提供的内容

image-20240316082033794

【4】api-paltform-gateway引入公共模块&Nacos注册

在pom.xml中引入依赖

        <!-- 引入自定义api-platform-common公共服务 -->
        <dependency>
            <groupId>com.noob</groupId>
            <artifactId>api-platform-common</artifactId>
            <version>0.0.1</version>
        </dependency>

完善CustomGlobalFilter功能

@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {

    @DubboReference
    private InnerUserService innerUserService;

    @DubboReference
    private InnerInterfaceInfoService innerInterfaceInfoService;

    @DubboReference
    private InnerUserInterfaceInfoService innerUserInterfaceInfoService;


    private static final String INTERFACE_HOST = "http://localhost:8080";


    private static final List<String> IP_WHITE_LIST = Arrays.asList("127.0.0.1");

    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //(1)用户发送请求到 API 网关(application.yml配置实现)

        //(2)请求日志
        ServerHttpRequest request = exchange.getRequest();
        String path = INTERFACE_HOST + request.getPath().value();
        String method = request.getMethod().toString();
        log.info("请求唯一标识:" + request.getId());
        log.info("请求路径:" + path);
        log.info("请求方法:" + method);
        log.info("请求参数:" + request.getQueryParams());
        String sourceAddress = request.getLocalAddress().getHostString();
        log.info("请求来源地址:" + sourceAddress);
        log.info("请求来源地址:" + request.getRemoteAddress());
        ServerHttpResponse response = exchange.getResponse();

        //(3)访问控制(黑白名单)
        if (!IP_WHITE_LIST.contains(sourceAddress)) {
            response.setStatusCode(HttpStatus.FORBIDDEN);
            return response.setComplete();
        }

        //(4)用户鉴权(判断 ak、sk 是否合法)
        HttpHeaders headers = request.getHeaders();
        String accessKey = headers.getFirst("accessKey");
        String nonce = headers.getFirst("nonce");
        String timestamp = headers.getFirst("timestamp");
        String sign = headers.getFirst("sign");
//        String body = headers.getFirst("body");

        // 指定utf-8编码格式处理中文乱码问题 String body = request.getHeader("body");
        String body = URLDecoder.decode(headers.getFirst("body"), "utf-8");

        // 借助RPC框架调用方法,去数据库中查是否已分配给用户
        User invokeUser = null;
        try {
            invokeUser = innerUserService.getInvokeUser(accessKey);
        } catch (Exception e) {
            log.error("getInvokeUser error", e);
        }
        if (invokeUser == null) {
            return handleNoAuth(response);
        }

        // 直接校验如果随机数大于1万,则抛出异常,并提示"无权限"
        if (Long.parseLong(nonce) > 10000) {
            return handleNoAuth(response);
        }

        // 校验时间和当前时间不能超过5分钟
        Long currentTime = System.currentTimeMillis() / 1000;
        // 定义一个常量FIVE_MINUTES,表示五分钟的时间间隔(乘以60,将分钟转换为秒,得到五分钟的时间间隔)。
        final Long FIVE_MINUTES = 60 * 5L;
        // 判断当前时间与传入的时间戳是否相差五分钟或以上,Long.parseLong(timestamp)将传入的时间戳转换成长整型
        // 然后计算当前时间与传入时间戳之间的差值(以秒为单位),如果差值大于等于五分钟,则返回true,否则返回false
        if ((currentTime - Long.parseLong(timestamp)) >= FIVE_MINUTES) {
            // 如果时间戳与当前时间相差五分钟或以上,调用handleNoAuth(response)方法进行处理
            return handleNoAuth(response);
        }

        // 校验生成签名
        String secretKey = invokeUser.getSecretKey();
        String serverSign = SignUtil.genSignByBody(body, secretKey); // 需对body进行解码,处理中文乱码问题
//        String serverSign = SignUtil.genSignByBody(URLDecoder.decode(body,"utf-8"),secretKey);
        if (!sign.equals(serverSign)) {
            return handleNoAuth(response);
        }

        //(5)请求的模拟接口是否存在?todo 调用后台数据库访问校验请求模拟接口是否存在以及请求方法是否匹配,或校验请求参数
        InterfaceInfo interfaceInfo = null;
        try {
            interfaceInfo = innerInterfaceInfoService.getInterfaceInfo(path, method);
        } catch (Exception e) {
            log.error("getInterfaceInfo error", e);
        }
        if (interfaceInfo == null) {
            return handleNoAuth(response);
        }

        // (6)请求转发,调用模拟接口,调用成功打印响应日志,并统计调用次数
        return handleResponse(exchange, chain, interfaceInfo.getId(), invokeUser.getId());

    }

    @Override
    public int getOrder() {
        return -1;
    }

    /**
     * 处理无权限访问,禁止访问
     *
     * @param response
     * @return
     */
    public Mono<Void> handleNoAuth(ServerHttpResponse response) {
        response.setStatusCode(HttpStatus.FORBIDDEN);
        return response.setComplete();
    }


    /**
     * 处理调用失败(返回一个规范的错误码,此处返回500调用失败)
     *
     * @param response
     * @return
     */
    public Mono<Void> handleInvokeError(ServerHttpResponse response) {
        response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
        return response.setComplete();
    }

    /**
     * 处理响应
     *
     * @param exchange
     * @param chain
     * @return
     */
    public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain, long interfaceInfoId, long userId) {
        try {
            ServerHttpResponse originalResponse = exchange.getResponse();
            // 缓存数据的工厂
            DataBufferFactory bufferFactory = originalResponse.bufferFactory();
            // 拿到响应码
            HttpStatus statusCode = originalResponse.getStatusCode();
            if (statusCode == HttpStatus.OK) {
                // 装饰,增强能力
                ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
                    // 等调用完转发的接口后才会执行
                    @Override
                    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                        log.info("body instanceof Flux: {}", (body instanceof Flux));
                        if (body instanceof Flux) {
                            Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                            // 往返回值里写数据
                            // 拼接字符串
                            return super.writeWith(
                                    fluxBody.map(dataBuffer -> {
                                        // 7. 调用成功,接口调用次数 + 1 invokeCount
                                        try {
                                            innerUserInterfaceInfoService.invokeCount(interfaceInfoId, userId);
                                        } catch (Exception e) {
                                            log.error("invokeCount error", e);
                                        }
                                        byte[] content = new byte[dataBuffer.readableByteCount()];
                                        dataBuffer.read(content);
                                        DataBufferUtils.release(dataBuffer);//释放掉内存
                                        // 构建日志
                                        StringBuilder sb2 = new StringBuilder(200);
                                        List<Object> rspArgs = new ArrayList<>();
                                        rspArgs.add(originalResponse.getStatusCode());
                                        String data = new String(content, StandardCharsets.UTF_8); //data
                                        sb2.append(data);
                                        // 打印日志
                                        log.info("响应结果:" + data);
                                        return bufferFactory.wrap(content);
                                    }));
                        } else {
                            // 8. 调用失败,返回一个规范的错误码
                            log.error("<--- {} 响应code异常", getStatusCode());
                        }
                        return super.writeWith(body);
                    }
                };
                // 设置 response 对象为装饰过的
                return chain.filter(exchange.mutate().response(decoratedResponse).build());
            }
            return chain.filter(exchange); // 降级处理返回数据
        } catch (Exception e) {
            log.error("网关处理响应异常" + e);
            return chain.filter(exchange);
        }
    }
}

​ 可能存在的问题,Springboot项目启动失败,maven依赖引入冲突(api-platform-common中引入依赖和自身项目引入依赖冲突,需清理),可以直接清理api-platform-common的依赖,或者在项目中引入common做去除

​ 没有配置数据库信息,可以排除此类的autoconfig,启动以后就可以正常运行

调试

​ 接口信息调用是根据url和请求方式查找的,因此数据库中要有符合的数据方能进行测试

​ 如果出现dubbo服务调用失败,则检查相应异常,参考文章open in new window,清理掉原启动类的DemoService调用,随后再次调试检查api-platform-backend是否正常执行

​ 如果后端正常执行,则会打印相应的SQL语句,检查SQL执行情况跟踪问题(此处根据URL、METHOD查找接口信息,因此要检查数据库中是否有唯一匹配的内容,与此同时在数据表设计上也要构建相应的校验)

测试结果

4.扩展问题

如何让其他用户上传自己编写的接口?

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

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

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

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3