通过SpringCloud Gateway实现API接口镜像请求(陪跑)网关功能

前言

在某些业务场景中,为实现新老接口的顺利切换,需要设置新接口陪同老接口同时运行(即接口陪跑)、比较新老接口的执行结果,待比较验证通过后,再正式切换到新接口。

我们可将这种在调用老接口的同时调用新接口进行陪跑的功能,称为"接口镜像"。并且可以通过 Spring Cloud Gateway实现接口镜像。

Spring Cloud Gateway概述

目前Spring Cloud的 API网关,主要基于Spring 6、Spring Boot 3,可提供API路由及安全、监控统计和可扩展性等网关基本功能。其中有两个核心概念:Server 和Proxy exchange代理交换机,可兼容WebFlux 和 MVC两种模式。

网关提供了很多内置的Gateway Filter工厂类,提供了多种路由过滤器,用于针对特定路由进行修改HTTP请求或响应。同时,我们可以自定义GatewayFilterFactory,从而实现更强大的定制化功能。

详细介绍,参见:Spring Cloud Gateway 官网

自定义FilterFactory实现接口镜像

Gateway在处理http请求时,因为其遵循了Reactive Streams(响应式流规范),数据流只能被消费一次,即在通过 request.getBody() 获取的请求体,只能读取一次,无法对其进行直接编辑或复制。这有点类似于 Rust 语言风格,可以大幅提升内存效率。

那如果想多次处理请求体,则需要通过缓存请求体,并包装新的请求对象的方式来解决。

java 复制代码
import com.mydemo.project.services.ApiLogger;
import com.mydemo.project.services.ServiceInvoker;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;


/**
 * 镜像请求过滤器工厂类
 */
@Slf4j
@Component
public class MirrorRequestFilterFactory extends AbstractGatewayFilterFactory<MirrorRequestFilterFactory.Config> {
    
    @Autowired
    private ApiLogger apiLogger;

    @Autowired
    private ServiceInvoker serviceInvoker;

    // 注入 Spring 自动配置的编解码器配置类,用于获取 HttpMessageReader
    private final ServerCodecConfigurer codecConfigurer;

    // 构造器注入 ServerCodecConfigurer
    public MirrorRequestFilterFactory(ServerCodecConfigurer codecConfigurer) {
        super(Config.class);
        this.codecConfigurer = codecConfigurer;
        log.info("MirrorRequestFilter init");
    }

    @Override
    public GatewayFilter apply(Config config) {
        log.info("request filtered, in apply");

        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            // 仅处理 POST 请求
            if (request.getMethod() != HttpMethod.POST) {
                return chain.filter(exchange);
            }

            // 读取并缓存原始请求体
            return DataBufferUtils.join(request.getBody())
                    .flatMap(dataBuffer -> {
                        // 复制请求体数据
                        byte[] bytes = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(bytes);
                        DataBufferUtils.release(dataBuffer); // 释放原始缓冲区

                        // 创建新的DataBuffer用于原始请求继续使用
                        Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                            DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                            DataBufferUtils.retain(buffer);
                            return Mono.just(buffer);
                        });

                        // 创建装饰器请求,确保原始请求能继续使用请求体
                        ServerHttpRequest decoratedRequest = new ServerHttpRequestDecorator(request) {
                            @Override
                            public Flux<DataBuffer> getBody() {
                                return cachedFlux;
                            }
                        };

                        if (config.isMirrorEnabled()) {
                            log.info("starting mirror request");
                            // 使用复制的请求体发起新请求
                            String copiedBody = new String(bytes, StandardCharsets.UTF_8);

                            // 异步发起镜像请求(不阻塞主流程)
                            serviceInvoker.invokeAsync(config.getMirrorUrl(), request, copiedBody);
                        }

                        // 将新的请求传入下一个 Filter 链,继续处理原始请求
                        ServerWebExchange nextExchange = exchange.mutate().request(decoratedRequest).build();
                        return chain.filter(nextExchange).doOnSuccess(response -> {
                            apiLogger.log("primary", "success", request, response);
                        }).doOnError(e -> {
                            apiLogger.log("primary", "failure", request, e.getMessage());
                        });
                    });
        };
    }

    /**
     * 配置类
     */
    @Data
    public static class Config {
        private String mirrorUrl; // 镜像 URL
        private boolean mirrorEnabled = true; // 是否启用镜像
    }
}

镜像功能的配置和启用

修改 application.yml,添加如下配置:

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: route1
          uri: http://localhost:8088  # 模拟主API地址
          predicates:
            - Path=/api/req
          filters:
            - RewritePath=/api/req, /primary/req  # 网关代理主API地址
            - name: MirrorRequestFilterFactory  # 接口镜像功能过滤器
              args:
                mirrorUrl: http://127.0.0.1:8081/mirror/req  # 模拟镜像API地址
                mirrorEnabled: true  # 启用镜像功能

此处使用了两个过滤器。RewritePath过滤器,实现接收外部请求,并转换为对模拟主API地址的请求;

MirrorRequestFilterFactory,接口镜像功能过滤器,实现对模拟主API地址访问的同时,启用镜像API功能,实现对模拟镜像API地址的访问。

工程依赖版本

Spring Cloud Gateway 不同版本变化较大,当前官网最新版本为4.3.0,但考虑到要兼容目前最新版的Nacos等组件,工程依赖版本情况如下:

groupId artifactId version
org.springframework.cloud spring-cloud-dependencies 2023.0.3.3
org.springframework.boot spring-boot-dependencies 3.3.13
org.springframework.cloud spring-cloud-starter-gateway 4.1.9
org.springframework.cloud spring-cloud-starter-loadbalancer 4.1.6
org.springframework.boot spring-boot-configuration-processor 3.3.13
org.projectlombok lombok 1.18.20

采用的Java版本:JDK17

最后,为实现接口陪跑,还需要实现异步发起镜像接口调用、主接口与镜像接口的交互日志记录、接口返回数据比对等功能,因与本次讨论主题无关,故略过。

相关推荐
卓怡学长8 小时前
w269基于spring boot + vue 候鸟监测数据管理系统
java·数据库·spring boot·spring·intellij-idea
Listen·Rain13 小时前
向量化详解
java·人工智能·spring·机器学习·ai
添砖java_85715 小时前
网页聊天室测试报告
java·spring boot·spring·maven
xixingzhe216 小时前
Spring 依赖注入
java·后端·spring
闲猫1 天前
Spring AI 对接Deepseek ChatModel 聊天对话
java·前端·spring
醇氧1 天前
Spring 容器 Map 注入机制详解
java·后端·spring
ylscode1 天前
Cloudflare Workers Cache 深度解析:边缘缓存如何重塑无服务器性能边界
java·spring·mybatis
沉静的小伙1 天前
Spring事务
java·数据库·spring
微三云 - 廖会灵 (私域系统开发)1 天前
电商系统微服务拆分实战:从单体到Spring Cloud的分布式事务踩坑与补偿方案
分布式·spring cloud·微服务
Wang's Blog1 天前
Java框架快速入门: Spring Bean核心配置与实例化详解
java·开发语言·spring