SpringCloud(六)Gateway 路由网关

文章目录

  • 一、Gateway是什么?
      • 1.1、网关位置
      • 1.2 Gateway特性
  • 二、使用步骤
      • 1.1 创建项目工程模块 `sgg-gateway-api9527`
      • 1.2 配置 yml 文件
      • 1.3 配置启动类
        • 1.4 Gateway 默认提供的断言
      • 1.5 如何自定义断言工厂
      • 1.6 Route Filter
      • 1.7 自定义 Route Filter
      • 1.8 配置 yml 文件
      • 1.9 简单测试
      • 2.0 请求限流
      • 2.1 简单测试
      • 2.2 基于Hystrix 实现服务容错
      • 2.3 接口测试
      • 2.4 GlobalFilter
      • 2.4 Gateway 监控端点
      • 2.6 日志级别
      • 27 Wiretap
  • 总结


一、Gateway是什么?

GateWay 是 Spring 生态系统之上构建的API网关服务, 基于 Spring5,SpringBoot 2 和Project Reactor 等技术。

Gateway 旨在提供一种简单而有效的方式来对 API 进行路由, 以及提供一些强大的过滤功能, 例如 熔断, 限流, 重试等。

SpringCloud Gateway 是SpringCloud 的一个全新项目,基于Spring 5 和 Spring Boot 2.0 Project Reactor 等技术开发的网关, 他是为了微服务提供一种简单有效的API路由管理方式

SpriingCloud Gateway 作为 Spring Cloud 生态系统中的网关, 目标是替代 Zuul,在Spring Cloud 2.0以上版本, 没有新版本的 Zuul2.0 以上最新版本的进行集成,仍然还是使用Zuul 1.x 非Reactor 模式的老版本, 而为了提升网关的性能, Spring Cloud Gateway 是基于 WebFlux 框架实现的, 而WebFlux 框架底层则使用了高性能的Reactor 模式通信的框架 Netty

SpringCloud Gateway 的目标是提供统一的路由方式且基于 Filter 链的方式提供了网关的基本功能, 例如: 安全, 监控、 限流,

1.1、网关位置

SpringCloud(六)Gateway 路由网关_第1张图片

1.2 Gateway特性

  • 基于异步非阻塞模型
  • 基于Spring 5, Project Reactor 和 SpringBoot 2.0
  • 动态路由,能够匹配任何请求属性
  • 可以对路由指定 Predicate(断言) 和 Filter (过滤器)
  • 集成Hystrix 的断路器功能
  • 集成 Spring Cloud 的服务发现功能
  • 易于编写的 Predicate 和 Filter
  • 请求限流功能
  • 支持路径重写

传统的Web 框架,比如说 struts2, SpringMVC 等都是基于 Servlet API和servlet 容器基础之上,但是,在Servlet 3.1 之后有了异步非阻塞的支持,而WebFlux 是一个典型的非阻塞异步的框架,他的核心是基于 Reactor 的相关API实现的,相对于传统的 web 框架来说, 他可以在Netty, Undertow 及 支持Servlet 3.1 的容器上, 非阻塞式 + 函数式编程

Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于SpringMVC, 他不需要依赖 Servlet API, 他是完全异步非阻塞的。 并且基于 Reactor来实现响应式规范。

  • Route(路由): 路由是构建网关的基本模块, 他由ID,目标URI,一系列的断言和过滤器组成,如果断言为 true 则匹配该路由
  • Predicate(断言):参考java8 中的 java.util.function.Predicate 开发人员可以匹配http请求中的所有内容,比如请求头或者请求参数, 如果请求参数与断言相匹配则进行路由。
  • Filter(过滤):指的是Spring框架中Gateway Filter 的实例, 使用过滤器,可以在请求路由前或者之后进行修改。
    SpringCloud(六)Gateway 路由网关_第2张图片
    web,请求,通过一些匹配条件(断言),定位到真正的服务节点,并且在这个转发过程的前后进行一些精细化控制, Predicate 就是我们的匹配条件而Filter,就可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标的URI, 就可以实现一个具体的路由了

官方示意图
SpringCloud(六)Gateway 路由网关_第3张图片
客户端向Gateway 发出请求, 然后在Gateway Handler Mapping 中找到请求相匹配的路由, 将其发送到Gateway Web Handler
Handler 在通过制定的过滤器链来讲请求发送到我们实际的服务执行业务逻辑,然后返回,过滤器链之间用虚线分开是因为过滤器可能会在发送代理之前 (“pre”) 或之后 (“post”)

二、使用步骤

1.1 创建项目工程模块 sgg-gateway-api9527

引入依赖

  <dependencies>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-gatewayartifactId>
        dependency>
        <dependency>
            <groupId>cn.flldaygroupId>
            <artifactId>sgg-api-commonartifactId>
            <version>${project.version}version>
        dependency>
    dependencies>

1.2 配置 yml 文件

server:
  port: 9527
spring:
  application:
    name: cloud-gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启动态路由
      routes:
        - id: payment-service # 路由唯一id
          uri: lb://SGG-PAYMENT-SERVICE # lb 代表 负载均衡loadBalances , 后面跟着的是微服务名称 在 eureka 中注册的名字
          predicates: # 断言
            - Path=/** # 判断是否匹配
eureka:
  instance:
    hostname: cloud-gateway-service
    instance-id: cloud-gateway-service-${server.port}
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka

1.3 配置启动类

@SpringBootApplication
@EnableEurekaClient
public class Gateway9527App {
    public static void main(String[] args) {
        SpringApplication.run(Gateway9527App.class, args);
    }
}

顺序启动 7001, 7002,8001, 8002, 9527 。

通过我们的网关访问我们的payment 提供的接口
http://localhost:9527/SGG-PAYMENT-SERVICE/payment/111SGG-PAYMENT-SERVICE 就是我们配置的 lb 负载均衡 的服务名 后面就匹配我们的 -Path 断言
SpringCloud(六)Gateway 路由网关_第4张图片
可以看到我们通过网关访问, 网关顺利转发到真正的服务接口上。 同时还具有负载均衡的效果。
SpringCloud(六)Gateway 路由网关_第5张图片

1.4 Gateway 默认提供的断言

SpringCloud(六)Gateway 路由网关_第6张图片
都可以试试。
有兴趣的可以自己尝试一下。 官网介绍

1.5 如何自定义断言工厂

我们可以看一下其他的断言类。发现都是继承自 AbstractRoutePredicateFactory 我们也来尝试自己定义一个 断言工厂
注意项:

  • 自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑和shortcutFieldOrder方法。
  • 在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
  • 命名需要以 RoutePredicateFactory 结尾,比如 CheckAuthRoutePredicateFactory,那么在使用的时候 CheckAuth 就是这个路由断言工厂的名称。代码如下所示。

创建 PortPredicateFactory.java

@Component
@Slf4j
public class PortPredicateFactory extends AbstractRoutePredicateFactory<PortPredicateFactory.Config> {
     public PortRoutePredicateFactory() {
        super(Config.class);
    }
    
    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return serverWebExchange -> {
         	ServerHttpRequest request = exchange.getRequest();
            ApplicationContext applicationContext = exchange.getApplicationContext();
            if (config.getPort().equals(new Integer("8001"))) {
                return true;
            }
            return false;
        };
    }
    @Data
    public class Config {
        private Integer port;
    }
}

上面的意思就是如果 端口号为 8001 就可以执行, 不等于 8001 就不执行。 可以看到 通过 exchange 参数我们可以获取到很多东西。 这个时候就可以通过这些东西做很多事情。 具体的我就不演示了。 现在配置yml 文件试试吧。
修改 yml 文件

routes:
  - id: payment-service
    uri: lb://SGG-PAYMENT-SERVICE
    predicates:
      - Path=/**
      - Port=8001

重启服务访问 接口。
SpringCloud(六)Gateway 路由网关_第7张图片
使用 8001 的时候,访问没有问题。 我们修改配置类将 -Port=8002 试试

1.6 Route Filter

一下都是转自 芋道源码
Gateway 内置了许多种 Route Filter 实现。 对请求进行拦截 实现自定义的功能, 比如说,限流。熔断等功能,并且, 多个 Route Filter 可以组合实现, 满足我们大多数的处理逻辑

SpringCloud(六)Gateway 路由网关_第8张图片

1.7 自定义 Route Filter

创建AuthGatewayFilterFactory 认证filter工厂


@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> {

    public AuthGatewayFilterFactory() {
        super(AuthGatewayFilterFactory.Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        Map<String, Integer> tokenMap = new HashMap<>();
        tokenMap.put("gss", 1);
        // 创建 gatewayfilter 对象
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ServerHttpRequest request = exchange.getRequest();
                HttpHeaders headers = request.getHeaders();
                String token = headers.getFirst(config.getTokenHeaderName());
                // 如果没有 token 不进行认证
                 if (!StringUtils.hasText(token)) {
                     return chain.filter(exchange);
                 }
                 // 认证开始
                ServerHttpResponse response = exchange.getResponse();
                Integer userId = tokenMap.get(token);
                // 通过 token 获取不到 userId 说明认证不通过
                if (userId == null) {
                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                    DataBuffer wrap = exchange.getResponse().bufferFactory().wrap("认证未通过".getBytes());
                    return response.writeWith(Flux.just(wrap));
                }
                request = request.mutate().header(config.getUserIdHeaderName(), String.valueOf(userId)).build();
                return chain.filter(exchange.mutate().request(request).build());
            }
        };
    }

    public static class Config {

        private static final String DEFAULT_TOKEN_HEADER_NAME = "token";

        private static final String DEFAULT_HEADER_NAME = "user-id";

        private String tokenHeaderName = DEFAULT_TOKEN_HEADER_NAME;
        private String userIdHeaderName = DEFAULT_HEADER_NAME;

        public static String getDefaultTokenHeaderName() {
            return DEFAULT_TOKEN_HEADER_NAME;
        }

        public static String getDefaultHeaderName() {
            return DEFAULT_HEADER_NAME;
        }

        public String getTokenHeaderName() {
            return tokenHeaderName;
        }

        public void setTokenHeaderName(String tokenHeaderName) {
            this.tokenHeaderName = tokenHeaderName;
        }

        public String getUserIdHeaderName() {
            return userIdHeaderName;
        }

        public void setUserIdHeaderName(String userIdHeaderName) {
            this.userIdHeaderName = userIdHeaderName;
        }
    }
}

通过@Compoent 注解 保证 Gateway 在加载所有的 GatewayFilterFactory Bean 的时候,能够加载到我们的自定义AuthGatewayFilterFactory , 有没有发现和自定义断言的时候 一毛一样 哈哈哈
继承 AbstractGatewayFilterFactory 抽象类。 并将泛型参数 设置为我们自己定义的 AuthGatewayFilterFactory.Config 配置类, 这样,Gateway 在解析的时候,会转换成 Config 对象
注意:在AuthGatewayFilterFactory 构造方法中,需要传递 Config 类给父级构造方法,保证能够正确创建出Config 对象。在Config 类中我们定义了两个属性。
- tokenHeaderName : 认证token 的 header 的名字, 默认值为 token
- userIdHeaderName : 认证后的UserId 的 header 名字, 默认为 user-id
在方法 apply(Config config) 中, 我们通过内部类定义了需要创建的GatewayFilter。

1.8 配置 yml 文件

spring:
  cloud:
    gateway:
      default-filters: 
        - name: Auth
          args: 
            token-header-name: access-token

spring.cloud.gateway.default-filters 配置项, Gateway 默认过滤器,对所有的路由都生效。对应 FilterDefinition 数组, 在这里我们配置之了一个自定义的 Filter 配置
- name: 过滤器名称,这里我们设置为Auth, 因为 Gateway 默认使用的 XXXGatewayFilterFactory 的前缀 XXX 名字。 因为 AuthGatewayFilterFactory 就是 Auth
- args: 过滤器的参数配置, 对应 Config 类,这里我们设置 token-header-name 配置项 access-token , 表示从请求头 access-token 中获取认证 token

1.9 简单测试

使用 PostMan 请求接口
SpringCloud(六)Gateway 路由网关_第9张图片
使用 access-token 为 gss 时候
SpringCloud(六)Gateway 路由网关_第10张图片

2.0 请求限流

Gateway 内置 RequestRateLimiterGatewayFilterFactory 提供请求限流功能。 该 Filter 是基于 Token Bucket Algorithm 令牌桶算法。 同时搭配 redis 实现分布式限流。
SpringCloud(六)Gateway 路由网关_第11张图片
限流需要使用到 redis 我们先启动一下 redis, 然后引入redis 的依赖

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

配置 yml 文件

spring:
  redis: 
	host: 127.0.0.1
	port: 6379
  cloud:
    gateway:
      default-filters:
        - name: Auth
          args:
            token-header-name: access-token
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 1 # 令牌桶的每秒放的数量
            redis-rate-limiter.burstCapacity: 2 # 令牌桶的最大令牌数
            key-resolver: "#{@ipKeyResolver}" # 获取限流key 的bean 的名字

说一下。 yml 文件配置是增量的。 在上面的基础上添加。 不是删除掉。在添加这个

再次添加一个 default-filters 配置项 。 添加了限流过滤器 RequestRateLimiter 配置参数:
- redis-rate-limiter.replenishRate: 1 # 令牌桶的每秒放的数量
- redis-rate-limiter.burstCapacity: 2 # 令牌桶的最大令牌数
- key-resolver: 获取限流key 的Bean 的名字

burstCapacity 参数: 可以理解为每秒最大的请求数,因此每请求一次,都会从桶里获取一块令牌
ReplenishRate 参数: 可以近似理解为每秒平均的请求数,如果令牌桶为空的情况下,一秒最多放这么多令牌书, 所以最大请求数当然也是这么多
实际上,在令牌桶满的情况下, 每秒最大请求书是 burstCapacity + ReplenishRate

在 GatewayConfig 配置类下创建获取限流Key 的Bean

    @Bean
    public KeyResolver ipKeyResolver(){
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
                return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
            }
        };
    }

创建的 ipKeyResolver Bean 是通过解析请求的来源 IP 作为限流 KEY,这样我们就能实现基于 IP 的请求限流。我们想要实现基于用户的请求限流,那么我们可以创建从请求中解析用户身份的 KeyResolver Bean。也就是说,通过自定义的 KeyResolver 来实现不同粒度的请求限流

2.1 简单测试

使用浏览器,疯狂访问 http://localhost:9527/SGG-PAYMENT-SERVICE/payment/1 就会被限流。 出现 429 页面。
SpringCloud(六)Gateway 路由网关_第12张图片

2.2 基于Hystrix 实现服务容错

引入依赖

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
        dependency>

创建 FallbackController ,提供 /fallback 接口。 用于Hystrix fallback 的重定向。

@RestController
@Slf4j
public class FallbackController {

    @GetMapping(value = "fallback")
    public String fallback(ServerWebExchange exchange) {
        Object requestURL = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        Object error = exchange.getAttribute(ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR);
        log.error("fallback 发生异常: [ {} ], [ {} ]", error, requestURL);
        return "服务降级。。。" + error;
    }
}

通过 exchange.getAttribute(ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR) 可以获取具体的fallback 异常。
配置 yml 文件

spring:
  redis: 
	host: 127.0.0.1
	port: 6379
  cloud:
    gateway:
      default-filters:
        - name: Hystrix
          args:
            name: fallbackcmd # 对应 Hystrix Command 名字
            fallbackUri: forwad:/fallback # 处理 Hystrix fallback 的情况, 重定向到指定地址

在 filters 中配置项,添加了 Hystrix 过滤器。 其配置参数如下。:

  • name: 对应的 Hystrix Command 名字,后续可以通过 hystrix.command.{name} 配置项, 来设置 name 对应的 Hystrix Command 的配置, 比如说超时时间,隔离策略等。
  • fallbackUri: 处理 Hystrix fallback 的情况。 重定向到指定地址,主要 要么为空 要么必须以forward: 开头

2.3 接口测试

SpringCloud(六)Gateway 路由网关_第13张图片
访问 timeout 超时接口。 可以看到出现异常了就会转发到我们的 fallback

2.4 GlobalFilter

在Gateway 中, 有两类过滤器
- Route Filter 路由过滤器 对应GatewayFilter 接口
- Global Filter 全局过滤器 对应 GlobalFilter 接口
两者基本是等价的,不同的是 Route Filter不是全局,可以配置在 指定路由上。 绝大多数情况, RouteFilter 能满足我们的拓展需求的情况下,优先使用它, 并且如果想要作用到所有的路由上。可以使用 spring.cloud.gateway.default-filters 配置上。另外 Global Filter 可能在未来的版本有一定的变化。

Gateway的过滤器的执行分成了前置 pre 和 后置 post 两个阶段,其中低排序值的过滤器在pre 阶段先执行, 高排序值得过滤器在post阶段限制性。 示例代码 。 创建了三个 filter

@Component
@Order(1)
@Slf4j
public class FGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("[F, [pre]]");
        return chain.filter(exchange).then(Mono.<Void>fromRunnable(()->log.info("[F, [post]]")));
    }
}

@Component
@Order(2)
@Slf4j
public class SGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("[S, [pre]]");
        return chain.filter(exchange).then(Mono.<Void>fromRunnable(()->log.info("[S, [post]]")));
    }
}

@Component
@Order(3)
@Slf4j
public class TGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("[T, [pre]]");
        return chain.filter(exchange).then(Mono.<Void>fromRunnable(()->log.info("[T, [post]]")));
    }
}

重启接口,发现打印和我们之前说的一样

2020-09-13 01:24:05.068  INFO 16968 --- [ioEventLoop-4-1] cn.fllday.filter.FGlobalFilter           : [F, [pre]]
2020-09-13 01:24:05.070  INFO 16968 --- [ioEventLoop-4-1] cn.fllday.filter.SGlobalFilter           : [S, [pre]]
2020-09-13 01:24:05.070  INFO 16968 --- [ioEventLoop-4-1] cn.fllday.filter.TGlobalFilter           : [T, [pre]]
2020-09-13 01:24:05.198  INFO 16968 --- [ctor-http-nio-4] cn.fllday.filter.TGlobalFilter           : [T, [post]]
2020-09-13 01:24:05.198  INFO 16968 --- [ctor-http-nio-4] cn.fllday.filter.SGlobalFilter           : [S, [post]]
2020-09-13 01:24:05.198  INFO 16968 --- [ctor-http-nio-4] cn.fllday.filter.FGlobalFilter           : [F, [post]]

2.4 Gateway 监控端点

Gateway的 actuate 模块, 基于 SpringBoot Actuator , 提供了 自动以监控断点 gateway, 提供了 Gateway 的各种监控管理的功能

路径 用途
GET /globalfilters 获取所有的 GlobalFilter
GET /routefilters 获取所有GatewayFilterFactory
GET /routepredicates 后去所有的RoutePredicateFactory
GET /routes 获取所有的路由
GET /routes/{id} 获取指定路由
GET /routes/{id}/combinedfilters 获得指定路由的过滤器
POST /routes/{id} 新增或修改,参数为 RouteDefinition
DELETE /routes/{id} 删除指定路由
POST /refresh 刷新路由缓存

注意: 所有的基础路径为 /actuator/gateway,所以 /globalfilters 对应的完整路径是 /actuator/gateway/globalfilters

引入 pom 依赖

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>

修改 yml 文件 , 配置 Spring Boot Actuator

management:
  endpoints:
    web:
      exposure:
        include: '*' # 需要开放的端点,默认只开放 health 和 info 两个端点,通过 * 开放所有的端点。
  endpoint:
    health:
      enabled: true  # 是否开启, 默认true 开启
      show-details: always # 何时显示完整的健康信息。 默认 never 都不展示。 可选择 WHEN_AUTHORIZED 当经过授权的用户, 可选 always 总是显示

获取 filter 和 predicate
GlobalFilter: http://localhost:9527/actuator/gateway/globalfilters
SpringCloud(六)Gateway 路由网关_第14张图片
GatewayFilterFactory: http://localhost:9527/actuator/gateway/routefiltersSpringCloud(六)Gateway 路由网关_第15张图片
Predicates: http://localhost:9527/actuator/gateway/routepredicates
SpringCloud(六)Gateway 路由网关_第16张图片
路由管理
使用 PostMan 请求GET /actuator/gateway/routes/{id} 获取一个路由详情
SpringCloud(六)Gateway 路由网关_第17张图片
然后根据这个 我们新建一个路由
POST /actuator/gateway/routes/{id}
SpringCloud(六)Gateway 路由网关_第18张图片
添加完成之后,我们通过 使用 PostMan 请求GET /actuator/gateway/routes/baidutieba 获取一个路由详情
SpringCloud(六)Gateway 路由网关_第19张图片
如果不行的话,记得使用refresh 刷新一下 POST /actuator/gateway/refresh
然后不要重启服务。 直接访问 http://localhost:9527/ 就会直接跳转到百度贴吧咯
SpringCloud(六)Gateway 路由网关_第20张图片

2.6 日志级别

修改 yml 文件。配置日记级别如下

logging:
  level: 
    reactor.netty: DEBUG
    org.springframework.cloud.gateway: TRACE

这样,SpringCloudGateway 和 Reactor Netty 可以打印更多的日志
重启网关。
SpringCloud(六)Gateway 路由网关_第21张图片
Gateway日志。 其他 Reactor Netty 日志太多。 就不截图了。

27 Wiretap

Reactor Netty 提供了 Wiretap 窃听功能。 让 Reactor Netty 打印包含请求和响应信息的日志, 比如说请求和响应的Header Body 等等, 开启 Reactor Netty 的Wiretap 功能一共有三个配置项

  • 设置 reactor.netty 的配置项为DEBUG 和 TRACE
  • 设置 spring.cloud.gateway.httpserver.wiretap 配置项为 true 开启 HttpServer Wiretap 功能
  • 设置 spring.cloud.gateway.httpclient.wiretap 配置项为 true 开启 Http Client wiretap 功能
      httpserver: # 配置 Reactor Netty 相关配置
        wiretap: true
      httpclient:
        wiretap: true

好多东西。截图放着了。
SpringCloud(六)Gateway 路由网关_第22张图片

总结

感谢B站尚硅谷老师哈哈 ~~~ 本文参考了
芋道源码 感谢!@!!

你可能感兴趣的:(Spring,Cloud,学习,SpringCloud,Gateway)