Spring Cloud Gateway 高级玩法:动态路由、请求日志、限流、灰度发布全方案

Spring Cloud Gateway 高级玩法:动态路由、请求日志、限流、灰度发布全方案

微服务架构越来越复杂,API网关成了整个系统的咽喉要道,传统的静态路由配置已经无法满足日益增长的业务需求...

今天就来聊聊Spring Cloud Gateway的高级玩法,让你掌握动态路由、请求日志、限流、灰度发布等全套技能!

一、为什么需要Spring Cloud Gateway?

在开始深入探讨之前,我们先来理解为什么Spring Cloud Gateway如此重要。

1.1 微服务架构的挑战

// 传统单体应用 vs 微服务架构
// 单体应用:所有功能都在一个应用里
// 微服务架构:功能分散在多个服务中

随着业务的发展,微服务数量急剧增加,带来了新的挑战:

  1. 服务发现:如何知道每个服务的位置?
  2. 负载均衡:如何合理分配请求?
  3. 安全控制:如何统一进行身份验证?
  4. 流量控制:如何防止系统被突发流量冲垮?
  5. 监控追踪:如何跟踪一个请求的完整链路?

1.2 Spring Cloud Gateway的优势

Spring Cloud Gateway作为Spring官方推出的API网关,相比Zuul有以下优势:

  1. 基于Reactor模型:异步非阻塞,性能更优
  2. 支持WebSocket:更好的实时通信支持
  3. 精细化路由:支持谓词和过滤器组合
  4. 易于扩展:良好的插件化设计

二、动态路由配置

静态路由配置虽然简单,但在生产环境中显得不够灵活。让我们看看如何实现动态路由。

2.1 动态路由的核心思想

动态路由允许我们在不重启网关的情况下修改路由规则,这对于频繁变更的业务场景非常有用。

# application.yml - 静态路由配置示例
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix=1

2.2 基于数据库的动态路由实现

@Component
public class DynamicRouteService {
    
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;
    
    /**
     * 添加路由
     */
    public String addRoute(RouteDefinition definition) {
        try {
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            // 发布事件通知网关刷新路由
            publisher.publishEvent(new RefreshRoutesEvent(this));
            return "路由添加成功";
        } catch (Exception e) {
            return "路由添加失败:" + e.getMessage();
        }
    }
    
    /**
     * 更新路由
     */
    public String updateRoute(RouteDefinition definition) {
        try {
            routeDefinitionWriter.delete(Mono.just(definition.getId()));
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            publisher.publishEvent(new RefreshRoutesEvent(this));
            return "路由更新成功";
        } catch (Exception e) {
            return "路由更新失败:" + e.getMessage();
        }
    }
    
    /**
     * 删除路由
     */
    public String deleteRoute(String id) {
        try {
            routeDefinitionWriter.delete(Mono.just(id)).subscribe();
            publisher.publishEvent(new RefreshRoutesEvent(this));
            return "路由删除成功";
        } catch (Exception e) {
            return "路由删除失败:" + e.getMessage();
        }
    }
}

2.3 路由配置实体类

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GatewayRouteDefinition {
    
    /**
     * 路由ID
     */
    private String id;
    
    /**
     * 路由URI
     */
    private String uri;
    
    /**
     * 谓词定义
     */
    private List<GatewayPredicateDefinition> predicates;
    
    /**
     * 过滤器定义
     */
    private List<GatewayFilterDefinition> filters;
    
    /**
     * 路由排序
     */
    private Integer order = 0;
    
    /**
     * 是否启用
     */
    private Boolean enabled = true;
}

2.4 数据库表设计

CREATE TABLE `gateway_route` (
  `id` varchar(64) NOT NULL COMMENT '路由ID',
  `route_name` varchar(128) NOT NULL COMMENT '路由名称',
  `uri` varchar(256) NOT NULL COMMENT '路由URI',
  `predicates` text NOT NULL COMMENT '谓词定义(JSON)',
  `filters` text COMMENT '过滤器定义(JSON)',
  `order_num` int DEFAULT '0' COMMENT '排序',
  `enabled` tinyint(1) DEFAULT '1' COMMENT '是否启用(0-否,1-是)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='网关路由表';

三、请求日志记录

为了更好地监控和调试API,我们需要详细记录每个请求的信息。

3.1 自定义全局过滤器记录请求日志

@Component
@Slf4j
public class RequestLogGlobalFilter implements GlobalFilter, Ordered {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 记录请求开始时间
        long startTime = System.currentTimeMillis();
        
        // 获取请求信息
        String requestId = UUID.randomUUID().toString();
        String method = request.getMethodValue();
        String url = request.getURI().toString();
        String clientIp = getClientIp(request);
        
        log.info("[{}] {} {} from {}", requestId, method, url, clientIp);
        
        return chain.filter(exchange).then(
            Mono.fromRunnable(() -> {
                long endTime = System.currentTimeMillis();
                long duration = endTime - startTime;
                
                ServerHttpResponse response = exchange.getResponse();
                HttpStatus statusCode = response.getStatusCode();
                
                log.info("[{}] {} {} completed with status {} in {} ms", 
                        requestId, method, url, statusCode, duration);
            })
        );
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
    
    /**
     * 获取客户端真实IP
     */
    private String getClientIp(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ip = headers.getFirst("X-Forwarded-For");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            if (ip.contains(",")) {
                ip = ip.split(",")[0];
            }
        } else {
            ip = headers.getFirst("X-Real-IP");
        }
        
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddress().getAddress().getHostAddress();
        }
        return ip;
    }
}

3.2 记录请求体和响应体

对于POST、PUT等请求,我们还需要记录请求体和响应体内容:

@Component
@Slf4j
public class RequestBodyLogFilter implements GlobalFilter, Ordered {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 只记录特定类型的请求
        String contentType = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
        if (contentType != null && 
            (contentType.contains("application/json") || 
             contentType.contains("application/x-www-form-urlencoded"))) {
            
            // 包装请求以缓存body
            return DataBufferUtils.join(request.getBody())
                .flatMap(dataBuffer -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);
                    
                    String requestBody = new String(bytes, StandardCharsets.UTF_8);
                    log.debug("Request Body: {}", requestBody);
                    
                    // 重新构造请求
                    Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                        DataBuffer buffer = exchange.getResponse().bufferFactory()
                            .wrap(bytes);
                        return Mono.just(buffer);
                    });
                    
                    ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(request) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return cachedFlux;
                        }
                    };
                    
                    return chain.filter(exchange.mutate().request(mutatedRequest).build());
                });
        }
        
        return chain.filter(exchange);
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}

四、限流策略

为了保护后端服务不被突发流量冲垮,我们需要实现有效的限流策略。

4.1 基于Redis的令牌桶算法限流

@Component
@Slf4j
public class RedisRateLimiterFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 限流配置
     * key: 路由ID
     * value: [限流数量, 时间窗口(秒)]
     */
    private Map<String, int[]> rateLimitConfig = new HashMap<>();
    
    public RedisRateLimiterFilter() {
        // 初始化限流配置
        rateLimitConfig.put("user-service", new int[]{100, 60}); // 每分钟100次
        rateLimitConfig.put("order-service", new int[]{50, 60}); // 每分钟50次
    }
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();
        
        // 根据路径确定路由ID
        String routeId = getRouteIdByPath(path);
        if (routeId == null) {
            return chain.filter(exchange);
        }
        
        // 获取限流配置
        int[] config = rateLimitConfig.get(routeId);
        if (config == null) {
            return chain.filter(exchange);
        }
        
        int limit = config[0]; // 限流数量
        int window = config[1]; // 时间窗口
        
        // 获取客户端IP作为限流标识
        String clientIp = getClientIp(request);
        String key = "rate_limit:" + routeId + ":" + clientIp;
        
        // 使用Lua脚本实现原子性限流
        String script = 
            "local current = redis.call('GET', KEYS[1])\n" +
            "if current == false then\n" +
            "  redis.call('SET', KEYS[1], 1)\n" +
            "  redis.call('EXPIRE', KEYS[1], ARGV[2])\n" +
            "  return 1\n" +
            "else\n" +
            "  current = tonumber(current)\n" +
            "  if current < tonumber(ARGV[1]) then\n" +
            "    redis.call('INCR', KEYS[1])\n" +
            "    return current + 1\n" +
            "  else\n" +
            "    return -1\n" +
            "  end\n" +
            "end";
        
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        redisScript.setResultType(Long.class);
        
        return redisTemplate.execute(redisScript, Collections.singletonList(key), 
                                   String.valueOf(limit), String.valueOf(window))
            .flatMap(result -> {
                if (result > 0) {
                    // 未超过限流阈值,继续处理请求
                    return chain.filter(exchange);
                } else {
                    // 超过限流阈值,返回429状态码
                    ServerHttpResponse response = exchange.getResponse();
                    response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
                    String data = "{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\"}";
                    DataBuffer buffer = response.bufferFactory().wrap(data.getBytes(StandardCharsets.UTF_8));
                    response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
                    return response.writeWith(Mono.just(buffer));
                }
            });
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 2;
    }
    
    /**
     * 根据路径获取路由ID
     */
    private String getRouteIdByPath(String path) {
        if (path.startsWith("/user")) {
            return "user-service";
        } else if (path.startsWith("/order")) {
            return "order-service";
        }
        return null;
    }
    
    /**
     * 获取客户端IP
     */
    private String getClientIp(ServerHttpRequest request) {
        // 实现同上...
        return "127.0.0.1";
    }
}

4.2 基于Sentinel的限流

除了自定义实现,我们还可以集成阿里巴巴的Sentinel来实现更强大的限流功能:

<!-- pom.xml 添加依赖 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
# application.yml 配置
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
      filter:
        enabled: true
      # 关闭URL路径聚合,确保每个API独立限流
      http-method-specify: true
@Component
public class SentinelGatewayConfiguration {
    
    @PostConstruct
    public void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        
        // 用户服务限流规则
        rules.add(new GatewayFlowRule("user-service")
            .setCount(100) // 限流阈值
            .setIntervalSec(60) // 时间窗口
            .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)
            .setMaxQueueingTimeMs(1000)); // 最大排队等待时间
            
        // 订单服务限流规则
        rules.add(new GatewayFlowRule("order-service")
            .setCount(50)
            .setIntervalSec(60)
            .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)
            .setMaxQueueingTimeMs(1000));
            
        GatewayRuleManager.loadRules(rules);
    }
}

五、灰度发布策略

灰度发布是一种重要的发布策略,可以降低新版本上线的风险。

5.1 基于请求头的灰度发布

@Component
@Slf4j
public class GrayReleaseFilter implements GlobalFilter, Ordered {
    
    /**
     * 灰度发布配置
     * key: 服务名
     * value: 灰度版本权重 (0-100)
     */
    private Map<String, Integer> grayConfig = new HashMap<>();
    
    public GrayReleaseFilter() {
        // 初始化灰度配置
        grayConfig.put("user-service", 20); // 20%流量进入灰度版本
        grayConfig.put("order-service", 10); // 10%流量进入灰度版本
    }
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();
        
        // 根据路径确定服务名
        String serviceName = getServiceNameByPath(path);
        if (serviceName == null) {
            return chain.filter(exchange);
        }
        
        // 获取灰度配置
        Integer grayWeight = grayConfig.get(serviceName);
        if (grayWeight == null || grayWeight <= 0) {
            return chain.filter(exchange);
        }
        
        // 检查是否有强制灰度的请求头
        String grayHeader = request.getHeaders().getFirst("X-Gray-Version");
        if ("true".equals(grayHeader)) {
            // 强制路由到灰度版本
            ServerHttpRequest.Builder builder = request.mutate();
            builder.header("X-Gray-Version", "true");
            return chain.filter(exchange.mutate().request(builder.build()).build());
        }
        
        // 根据权重随机决定是否进入灰度版本
        Random random = new Random();
        int randomValue = random.nextInt(100);
        if (randomValue < grayWeight) {
            // 路由到灰度版本
            ServerHttpRequest.Builder builder = request.mutate();
            builder.header("X-Gray-Version", "true");
            log.info("Routing to gray version for service: {}, weight: {}", serviceName, grayWeight);
            return chain.filter(exchange.mutate().request(builder.build()).build());
        }
        
        return chain.filter(exchange);
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 3;
    }
    
    /**
     * 根据路径获取服务名
     */
    private String getServiceNameByPath(String path) {
        if (path.startsWith("/user")) {
            return "user-service";
        } else if (path.startsWith("/order")) {
            return "order-service";
        }
        return null;
    }
}

5.2 基于用户标签的灰度发布

@Component
@Slf4j
public class UserTagGrayFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private UserService userService;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 获取用户ID
        String userId = getUserIdFromRequest(request);
        if (userId == null) {
            return chain.filter(exchange);
        }
        
        // 获取用户标签
        Set<String> userTags = userService.getUserTags(userId);
        if (userTags == null || userTags.isEmpty()) {
            return chain.filter(exchange);
        }
        
        // 检查用户是否属于灰度用户
        if (userTags.contains("gray")) {
            ServerHttpRequest.Builder builder = request.mutate();
            builder.header("X-User-Tag", "gray");
            log.info("Routing gray user: {}", userId);
            return chain.filter(exchange.mutate().request(builder.build()).build());
        }
        
        return chain.filter(exchange);
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 4;
    }
    
    /**
     * 从请求中获取用户ID
     */
    private String getUserIdFromRequest(ServerHttpRequest request) {
        // 从token或cookie中解析用户ID
        String token = request.getHeaders().getFirst("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            // 解析JWT token获取用户ID
            return parseUserIdFromToken(token.substring(7));
        }
        return null;
    }
    
    private String parseUserIdFromToken(String token) {
        // JWT解析逻辑
        try {
            // 简化实现,实际应该使用JWT库
            return "user_" + token.hashCode();
        } catch (Exception e) {
            return null;
        }
    }
}

六、完整配置示例

让我们来看一个完整的配置示例,整合以上所有功能:

server:
  port: 8080

spring:
  application:
    name: api-gateway
    
  redis:
    host: localhost
    port: 6379
    database: 0
    
  cloud:
    gateway:
      # 全局cors配置
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods: "*"
            allowedHeaders: "*"
      
      # 默认过滤器
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
        - name: Retry
          args:
            retries: 3
            statuses: BAD_GATEWAY
            
      # 路由配置
      routes:
        # 用户服务路由
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix=1
            - name: Hystrix
              args:
                name: user-service
                fallbackUri: forward:/fallback/user
                
        # 订单服务路由
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            - StripPrefix=1
            - name: Hystrix
              args:
                name: order-service
                fallbackUri: forward:/fallback/order
                
        # 商品服务路由
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/product/**
          filters:
            - StripPrefix=1
            
    # Sentinel配置
    sentinel:
      transport:
        dashboard: localhost:8080
      eager: true
      
# 日志配置
logging:
  level:
    org.springframework.cloud.gateway: DEBUG
    com.example.gateway.filter: DEBUG

七、监控与告警

有了完善的网关功能,我们还需要建立监控体系来确保系统稳定运行。

7.1 Prometheus指标收集

@Component
public class GatewayMetricsCollector {
    
    private final Counter requestCounter;
    private final Timer requestTimer;
    private final Gauge activeRequestsGauge;
    
    public GatewayMetricsCollector(MeterRegistry meterRegistry) {
        requestCounter = Counter.builder("gateway_requests_total")
            .description("Total requests through gateway")
            .register(meterRegistry);
            
        requestTimer = Timer.builder("gateway_request_duration_seconds")
            .description("Request duration through gateway")
            .register(meterRegistry);
            
        activeRequestsGauge = Gauge.builder("gateway_active_requests")
            .description("Active requests through gateway")
            .register(meterRegistry, new AtomicInteger(0));
    }
    
    public void recordRequest(String routeId, String method, int status, long duration) {
        requestCounter.increment(
            Tags.of(
                "route", routeId,
                "method", method,
                "status", String.valueOf(status)
            )
        );
        
        requestTimer.record(duration, TimeUnit.MILLISECONDS);
    }
}

7.2 告警规则配置

# prometheus告警规则
groups:
- name: gateway.rules
  rules:
  - alert: HighErrorRate
    expr: rate(gateway_requests_total{status=~"5.."}[5m]) / rate(gateway_requests_total[5m]) > 0.05
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: "High error rate detected"
      description: "Gateway error rate is above 5%"
      
  - alert: HighLatency
    expr: histogram_quantile(0.95, rate(gateway_request_duration_seconds_bucket[5m])) > 2
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: "High latency detected"
      description: "Gateway 95th percentile latency is above 2 seconds"

八、最佳实践总结

8.1 性能优化建议

  1. 合理设置线程池大小
server:
  undertow:
    io-threads: 4
    worker-threads: 20
  1. 启用HTTP/2
server:
  http2:
    enabled: true
  1. 配置连接池
spring:
  cloud:
    gateway:
      httpclient:
        pool:
          max-idle-time: 30s
          max-life-time: 60s

8.2 安全加固

  1. 请求大小限制
spring:
  codec:
    max-in-memory-size: 10MB
  1. 防止重放攻击
@Component
public class ReplayAttackFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 获取时间戳和签名
        String timestamp = request.getHeaders().getFirst("X-Timestamp");
        String signature = request.getHeaders().getFirst("X-Signature");
        
        if (timestamp == null || signature == null) {
            return unauthorizedResponse(exchange);
        }
        
        // 检查时间戳有效性(5分钟内有效)
        long now = System.currentTimeMillis();
        long reqTime = Long.parseLong(timestamp);
        if (Math.abs(now - reqTime) > 5 * 60 * 1000) {
            return unauthorizedResponse(exchange);
        }
        
        // 检查签名是否已使用
        String signKey = "signature:" + signature;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(signKey))) {
            return unauthorizedResponse(exchange);
        }
        
        // 记录签名,5分钟内不可重复使用
        redisTemplate.opsForValue().set(signKey, "1", Duration.ofMinutes(5));
        
        return chain.filter(exchange);
    }
    
    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        String data = "{\"code\":401,\"message\":\"请求无效\"}";
        DataBuffer buffer = response.bufferFactory().wrap(data.getBytes(StandardCharsets.UTF_8));
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 10;
    }
}

九、常见问题与解决方案

9.1 网关启动慢

问题原因:默认情况下会加载所有路由配置,当路由较多时会影响启动速度。

解决方案:

spring:
  cloud:
    gateway:
      eager-load:
        enabled: false

9.2 跨域问题

问题现象:前端调用API时报跨域错误。

解决方案:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods: "*"
            allowedHeaders: "*"
            allowCredentials: true

9.3 文件上传失败

问题现象:上传大文件时出现连接断开或超时。

解决方案:

spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB
      
  codec:
    max-in-memory-size: 100MB

结语

Spring Cloud Gateway作为新一代API网关,在微服务架构中发挥着重要作用。通过动态路由、请求日志、限流、灰度发布等功能的合理运用,我们可以构建出高性能、高可用、易维护的网关系统。

掌握了这些高级玩法,相信你在面对复杂的微服务架构时会更加从容。记住,技术的价值在于解决实际问题,只有结合具体业务场景,才能发挥出最大的威力!

如果你觉得这篇文章对你有帮助,欢迎分享给更多的朋友。在微服务的路上,我们一起成长!


关注「服务端技术精选」,获取更多干货技术文章!


标题:Spring Cloud Gateway 高级玩法:动态路由、请求日志、限流、灰度发布全方案
作者:jiangyi
地址:http://jiangyi.space/articles/2025/12/21/1766304284804.html

    0 评论
avatar