【微服务专题】深入理解与实践微服务架构(十三)之Gateway谓词与过滤器功能实践

1,994 阅读21分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

7. 如何配置谓词和过滤器

上面我们快速上手了网关路由的谓词配置过滤器配置,相当于已经使用过Gateway的谓词功能和过滤器功能了;但是除了“知其然”,还要"知其所以然"。因此,我们从官方文档出发,详细了解 Spring Cloud Gateway 的谓词工厂和过滤器工厂的具体配置方式:

下面是Spring官方文档说明:

两种配置谓词和过滤器的方法快捷方式完全扩展的参数,下面的大多数示例都使用快捷方式

名称和参数名称将code在每个部分的第一句或第二句中列出,参数通常按快捷方式配置所需的顺序列出。

快捷方式的配置

快捷方式配置由过滤器名称识别,后跟等号 ( =),后跟以逗号 ( ,) 分隔的参数值。

application.yml

spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: https://example.org
        predicates:
        - Cookie=mycookie,mycookievalue

前面的示例Cookie使用两个参数定义了路由谓词工厂,cookie 名称mycookie和要匹配的值mycookievalue

完全扩展参数方式的配置

完全扩展的参数看起来更像是带有名称/值对的标准 yaml 配置。通常,会有一把name钥匙和一把args钥匙。键是用于配置谓词或过滤器的args键值对映射。

application.yml

spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: https://example.org
        predicates:
        - name: Cookie
          args:
            name: mycookie
            regexp: mycookievalue

Cookie这是上面显示的谓词的快捷配置的完整配置。

8. 谓词(断言)工厂

Spring Cloud Gateway 将路由匹配为 Spring WebFlux HandlerMapping基础架构的一部分。Spring Cloud Gateway 包含许多内置的路由谓词工厂。所有这些谓词都匹配 HTTP 请求的不同属性。您可以将多个路由谓词工厂与逻辑 and 语句结合起来。

Spring Cloud Gateway内置了一系列的路由谓词工厂,以便我们可以在开发中灵活的使用Gateway进行请求转发。我这里将Gateway内置的所有路由谓词工厂整理成了表格,如下所示:

Predicate 参数列表

名称说明示例
After是某个时间点后的请求- After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before是某个时间点之前的请求- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between是某两个时间点之前的请求- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie请求必须包含某些cookie- Cookie=chocolate, ch.p
Header请求必须包含某些header- Header=X-Request-Id, \d+
Host请求必须是访问某个host(域名)- Host= .somehost.org, .anotherhost.org
Method请求方式必须是指定方式- Method=GET,POST
Path请求路径必须符合指定规则- Path=/red/{segment},/blue/**
Query请求参数必须包含指定参数- Query=name, Jack或者- Query=name
RemoteAddr请求者的ip必须是指定范围- RemoteAddr=192.168.1.1/24
Weight权重处理- Weight=group1, 8

参数源码实现工厂类:

Spring Cloud Gateway 内置的路由谓词工厂

9. 内置、局部和全局过滤器

① 内置过滤器

内置过滤器又叫做默认(路由)过滤器,是Spring Cloud Gateway默认内置的路由匹配策略过滤器,作用范围与路由过滤器相同,只对当前服务的路由策略生效。

Spring Cloud Gateway路由过滤器(filters)的功能是:允许以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应。

Gateway 过滤器的生命周期

  • PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

多个 GlobalFilter 可以通过 @Order 或者 getOrder() 方法指定执行顺序,order值越小,执行的优先级越高。

注意:由于过滤器有 pre 和 post 两种类型,pre 类型过滤器如果 order 值越小,那么它就应该在pre过滤器链的顶层,post 类型过滤器如果 order 值越小,那么它就应该在 post 过滤器链的底层。示意图如下:

image-20220628234637316

Gateway提供了哪些过滤器类型

Gateway中一共提供了两种过滤器,一种是GatewayFilterFactory、GlobalFilter:

  • GatewayFilterFactory:Gateway网关过滤器,是针对单个路由的过滤器,又称局部过滤器,其功能是针对访问的URL起到一定的过滤效果。
  • GlobalFilter:从名称而言,那就是全局过滤器,是需要实现具体的Java类来实现GlobalFilter接口,这其中可以根据进行权限的验证,HTTP请求的头部添加等等。

过滤器工厂的顶级接口是 GatewayFilterFactory,我们可以直接继承它的两个抽象类来简化开发 AbstractGatewayFilterFactoryAbstractNameValueGatewayFilterFactory,这两个抽象类的区别就是前者接收一个参数(像 StripPrefix 和我们创建的这种),后者接收两个参数(像 AddResponseHeader)。

常见内置路由过滤器:

名称说明
AddRequestHeader给当前请求添加一个请求头
RemoveRequestHeader移除请求中的一个请求头
AddResponseHeader给响应结果中添加一个响应头
RemoveResponseHeader从响应结果中移除有一个响应头
RequestRateLimiter限制请求的流量

全部内置过滤器列表,如下所示:

过滤器工厂作用参数
AddRequestHeader为原始请求添加HeaderHeader的名称及值
AddRequestParameter为原始请求添加请求参数参数名称及值
AddResponseHeader为原始响应添加HeaderHeader的名称及值
DedupeResponseHeader剔除响应头中重复的值需要去重的Header名称及去重策略
Hystrix为路由引入Hystrix的断路器保护Hystrixcommand的名称
FallbackHeaders为fallbackUri的请求头中添加具体的异常信息Header的名称
PrefixPath为原始请求路径添加前缀前缀路径
PreserveHostHeader为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host
RequestRateLimiter用于对请求限流,限流算法为令牌桶keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus
RedirectTo将原始请求重定向到指定的URLhttp状态码及重定向的url
RemoveHopByHopHeadersFilter为原始请求删除IETF组织规定的一系列HeaderHeader名称
RemoveResponseHeader为原始请求删除某个HeaderHeader的名称
RewritePath重写原始的请求路径原始路径正则表达式以及重写后路径的正则表达式
RewriteResponseHeader重写原始响应中的某个HeaderHeader名称,值的正则表达式,重写后的值
SaveSession在转发请求之前,强制执行websession::save操作
secureHeaders为原始响应添加一系列起安全作用的响应头无,支持修改这些安全响应头的值
SetPath修改原始的请求路径修改后的路径
SetResponseHeader修改原始响应中某个Header的值Header名称,修改后的值
SetStatus修改原始响应的状态码HTTP状态码,可以是数字,也可以是字符串
StripPrefix用于截断原始请求的路径使用数字表示要截断的路径的数量
Retry针对不同的响应进行重试retries、statuses、methods、 series
RequestSize设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返413Payload Too Large请求包大小,单位为字节,默认值为5M
ModifyRequestBody在转发请求之前修改原始请求体内容修改后的请求体内容
ModifyResponseBody修改原始响应体的内容修改后的响应体内容

② 局部过滤器

局部过滤器(GatewayFilter)

局部过滤器(GatewayFilter)又叫做路由过滤器,是针对单个路由的过滤器,用于对访问的URL进行过滤、切面处理等。在Spring Cloud Gateway中通过GatewayFilter的形式内置了很多不同类型的局部过滤器,因此,其实内置过滤器就是路由过滤器

下面我们以内置添加请求头的AddRequestHeader内置为例,来进行内置路由过滤器的使用测试:

需求:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome!

我们以使用内置SetStatus过滤器为例,首先在配置文件中添加如下配置:

server:
  port: 9070
spring:
  application:
    name: service-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848   # nacos服务端地址
    gateway:
      discovery:
        locator:
          enabled: true # 启用探测器,让gateway可以发现nacos中的微服务
      routes: # 路由数组(路由:就是当指定请求满足什么条件的时候,转发到哪个微服务)
        - id: service-gateway  # 当前路由的标识,要求唯一。默认uuid
          uri: lb://service-gateway # lb指的是负载均衡(load balancing),service-gateway是nacos中微服务的名称
          order: 1  # 路由的优先级,数字越小级别越高
          predicates: # 断言(就是路由转发要满足的条件)
            - Path=/looptest/gateway/api/hello/** # 当请求路径满足Path指定的规则时,才进行路由转发
          filters:  # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1 # 转发之前去掉1层路径
            - SetStatus=220 # 修改原始响应的状态码

然后启动项目,使用浏览器请求进行测试:

http://localhost:9070/looptest/gateway/api/hello
image-20220627042208340

开发者面板中请求响应,如下所示:

image-20220627042134921

可以看到,测试成功,返回响应值(Status Code)为设置的220。

③ 自定义局部过滤器

首先局部过滤器也是一种路由过滤器工厂类,而过滤器工厂的顶级接口是GatewayFilterFactory

GatewayFilterFactory有两个具体实现的抽象类,分别为AbstractGatewayFilterFactoryAbstractNameValueGatewayFilterFactory,前者接收一个参数config,用于实现用户自定义参数类;后者接收两个参数,用于接收。

在Filter中编写自定义逻辑,可以实现下列功能:

  • 登录状态判断
  • 权限校验
  • 请求限流等

下面我们可以通过自定义的局部过滤器,来进行日志记录功能。

首先在yml配置文件中,添加自定义局部过滤器配置:

      routes:
        - id: service-provider-nacos             # 当前路由的标识, 要求唯一
          uri: lb://service-provider-nacos       # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略路由请求(动态路由)
          order: 10 # 路由的优先级,数字越小代表路由的优先级越高
          predicates: # 断言(就是路由转发要满足的条件)
            - Path=/provider-nacos/**             # 当请求路径满足Path指定的规则时,才进行路由转发
        # 我们⾃定义的路由 ID,保持唯⼀
        - id: service-gateway
          # ⽬标服务地址(部署多实例,不能加子路径)
#          uri: http://localhost:9070 # 指定具体的微服务地址(原真实服务地址还是可以访问,如果要限制走网关可以加token验证机制)
#          uri: https://www.baidu.com # 指定具体的微服务地址
          uri: lb://service-gateway
          # gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
          # 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
          predicates:
            # 当请求的路径为http://localhost:9070/looptest/gateway/api/hello时,转发到http://localhost:9070/gateway/api/hello
            - Path=/looptest/gateway/api/hello # 本身就是基于path的反向代理
#            - Path=/**
          filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1 # 转发之前去掉1层路径(去除原始请求路径中的前1级路径,即/looptest去除)
#            - SetStatus=220 # 修改原始响应的状态码
            # 名称必须为过滤器工厂类名的前缀(Log),而且参数只能有两个,由于NameValueConfig里只定义了两个属性
            - Log=testName,testValue #自定义局部过滤器

注意:自定义过滤器工厂类时,按照Spring Cloud Stream的约定,类名须为“过滤器名(本例子中:Log)” + GatewayFilterFactory,并且yml配置文件中的过滤器参数必须为自定义局部过滤器工厂类前缀(Log)。

然后启动项目,使用curl命令或者浏览器请求进行测试:

C:\Users\deepinsea>curl http://localhost:9070/looptest/gateway/api/hello
hello, 这里是service-gateway网关, 恭喜你请求了正确的路径!

调用成功,查看控制台日志,输出如下:

2022-06-27 05:39:36.823  INFO 52652 --- [ctor-http-nio-2] c.d.c.filter.LogGatewayFilterFactory     : 配置参数:testName, testValue
2022-06-27 05:39:36.823  INFO 52652 --- [ctor-http-nio-2] c.d.c.filter.LogGatewayFilterFactory     : 当前调用时间为:22-6-27 上午5:39
2022-06-27 05:39:36.844  WARN 52652 --- [ctor-http-nio-2] c.l.c.ServiceInstanceListSupplierBuilder : LoadBalancerCacheManager not available, returning delegate without caching.

可以看到,控制台成功输出了日志,测试局部过滤器成功!

④ 全局过滤器

全局过滤器(GlobalFilter)

全局过滤器作用于所有路由,无需配置路由。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。

SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下

image-20220626230241886

⑤ 自定义全局过滤器

自定义全局过滤器需要实现 GlobalFilter 接口和 Ordered 接口,我们自定义一个IP信息全局过滤器:

@Component
public class IPAddressStatisticsFilter implements GlobalFilter, Ordered {
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        InetSocketAddress host = exchange.getRequest().getHeaders().getHost();
        if (host == null || host.getHostName() == null) {
            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
            return exchange.getResponse().setComplete();
        }
        String hostName = host.getHostName();
        AtomicInteger count = IpCache.CACHE.getOrDefault(hostName, new AtomicInteger(0));
        count.incrementAndGet();
        IpCache.CACHE.put(hostName, count);
        System.out.println("IP地址:" + hostName + ",访问次数:" + count.intValue());
        return chain.filter(exchange);
    }
​
    @Override
    public int getOrder() {
        return 10101;
    }
}
​
//用于保存次数的缓存
public class IpCache {
    public static final Map<String, AtomicInteger> CACHE = new ConcurrentHashMap<>();
}

先注释局部过滤器配置,然后启动项目,使用curl命令测试接口:

C:\Users\deepinsea>curl http://localhost:9070/looptest/gateway/api/hello
hello, 这里是service-gateway网关, 恭喜你请求了正确的路径!
C:\Users\deepinsea>curl http://localhost:9070/looptest/gateway/api/hello
hello, 这里是service-gateway网关, 恭喜你请求了正确的路径!
C:\Users\deepinsea>curl http://localhost:9070/looptest/gateway/api/hello
hello, 这里是service-gateway网关, 恭喜你请求了正确的路径!
C:\Users\deepinsea>curl http://localhost:9070/looptest/gateway/api/hello
hello, 这里是service-gateway网关, 恭喜你请求了正确的路径!

控制台日志如下:

2022-06-27 06:06:27.535  INFO 60224 --- [ctor-http-nio-2] c.d.common.filter.IPStatsGlobalFilter    : IP地址:localhost, 访问时间:22-6-27 上午6:06
2022-06-27 06:06:27.553  WARN 60224 --- [ctor-http-nio-2] c.l.c.ServiceInstanceListSupplierBuilder : LoadBalancerCacheManager not available, returning delegate without caching.
2022-06-27 06:06:29.811  INFO 60224 --- [ctor-http-nio-6] c.d.common.filter.IPStatsGlobalFilter    : IP地址:localhost, 访问时间:22-6-27 上午6:06
2022-06-27 06:06:31.097  INFO 60224 --- [ctor-http-nio-7] c.d.common.filter.IPStatsGlobalFilter    : IP地址:localhost, 访问时间:22-6-27 上午6:06
2022-06-27 06:06:32.016  INFO 60224 --- [ctor-http-nio-8] c.d.common.filter.IPStatsGlobalFilter    : IP地址:localhost, 访问时间:22-6-27 上午6:06

测试成功,自定义全局过滤器验证成功!

上面的Predicates和这里的Filters基本上把服务网关的功能都实现了,包括路由转发、权限拦截、流量统计、流量控制、服务熔断、日志记录等功能。

10. 动态路由功能

Spring Cloud Gateway结合Nacos的注册中心,可以实现根据微服务名自动路由到对应的微服务,也就是动态路由功能。

官网介绍

您可以将网关配置为基于在DiscoveryClient兼容服务注册表中注册的服务创建路由。

要启用此功能,请设置spring.cloud.gateway.discovery.locator.enabled=true并确保DiscoveryClient实现(例如 Netflix Eureka、Consul 或 Zookeeper)在类路径上并启用。

大致意思是,通过如下配置,可以实现自动根据服务发现为每一个服务创建了一个路由router,这个router将以服务名开头的请求路径转发到对应的服务:

spring.cloud.gateway.discovery.locator.enabled=true

本质上,这个功能的实现:是依赖于 Spring Cloud Commons 中的注册中心DiscoveryClient公共抽象类来获取所有服务信息,从而实现动态路由的功能。

并且在很多博客中,也可以发现类似的说法:

spring.cloud.gateway.discovery.locator.enabled为true,表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router, 这个router将以服务名开头的请求路径转发到对应的服务。

但是,我在测试中发现该配置并不起作用。

我们可以进行配置并测试来具体验证,首先将yml配置中的 routes 配置全部注释:

    # 网关
    gateway:
      # 启用开关(默认开启)
      enabled: true
      discovery:
        locator:
          enabled: true # 自动发现nacos中的微服务
          lower-case-service-id: true # 因为微服务中的服务名是大写,请求路径中的为小写,因此需要转为小写
#      # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务] --本质就是反向代理
#      routes:
#        - id: service-provider-nacos             # 当前路由的标识, 要求唯一
#          uri: lb://service-provider-nacos       # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略路由请求(动态路由)
#          order: 10 # 路由的优先级,数字越小代表路由的优先级越高
#          predicates: # 断言(就是路由转发要满足的条件)
#            - Path=/provider-nacos/**             # 当请求路径满足Path指定的规则时,才进行路由转发
#        # 我们⾃定义的路由 ID,保持唯⼀
#        - id: service-gateway
#          # ⽬标服务地址(部署多实例,不能加子路径)
##          uri: http://localhost:9070 # 指定具体的微服务地址(原真实服务地址还是可以访问,如果要限制走网关可以加token验证机制)
##          uri: https://www.baidu.com # 指定具体的微服务地址
#          uri: lb://service-gateway
#          # gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
#          # 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
#          predicates:
#            # 当请求的路径为http://localhost:9070/looptest/gateway/api/hello时,转发到http://localhost:9070/gateway/api/hello
#            - Path=/looptest/gateway/api/hello # 本身就是基于path的反向代理
##            - Path=/**
#          filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
#            - StripPrefix=1              # 转发之前去掉1层路径(去除原始请求路径中的前1级路径,即/looptest)
##      httpclient:
##        connect-timeout: 10000
##        response-timeout: 5s

启动service-provider-nacosservice-provider-apiservice-gateway 子项目,使用curl命令测试动态路由功能:

C:\Users\deepinsea>curl http://localhost:9070/provider-nacos/hello
{"timestamp":"2022-06-26T17:42:36.985+00:00","path":"/provider-nacos/hello","status":404,"error":"Not Found","message":null,"requestId":"c3af0882-3"}

发现调用失败动态路由功能测试失败

在GitHub中也找到相关问题:

image-20220627015615281

于是找了很多资料去佐证这个功能有效性,发现网上有这个功能的配置,但很少有人提及这个功能失效的问题。

上面有提到开启 lower-case-service-id: true 配置将大写转为小写,我开启这项配置后发现请求还是错误,并且注册中心服务名本来就是小写。

于是,我查看了开启动态路由的配置条件并检查了我请求URL是否有错误,最后发现原理是我请求路径前没有加微服务名(service-provider-nacos) ,可能是上面配置真实路径前没有加服务名所以误导了😅。

配置文件开启

正确的请求地址:http://localhost:9070/service-provider-nacos/provider-nacos/hello

    # 网关
    gateway:
      # 启用开关(默认开启)
      enabled: true
      discovery:
        locator:
          # 开启从注册中心动态创建路由的功能,利用微服务名进行动态路由(在真实服务请求路径上加上/服务名)
          # 例如:http://localhost:9070/service-provider-nacos/provider-nacos/hello
          enabled: true
          # 服务名转为小写(默认yml配置就是小写,这里只是保证一下)
          lower-case-service-id: true
#      # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务] --本质就是反向代理
#      routes:
#        - id: service-provider-nacos             # 当前路由的标识, 要求唯一
#          uri: lb://service-provider-nacos       # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略路由请求(动态路由)
#          order: 10 # 路由的优先级,数字越小代表路由的优先级越高
#          predicates: # 断言(就是路由转发要满足的条件)
#            - Path=/provider-nacos/**             # 当请求路径满足Path指定的规则时,才进行路由转发
##          filters:
##            - AddRequestHeader=gateway, blue
#          ## 配置过滤器(局部)
#          filters:
##            - AddResponseHeader=X-Response-Foo, Bar
#            - AddResponseHeader=licence, value
#            ## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
#            - Authorize=true
#        # 我们⾃定义的路由 ID,保持唯⼀
#        - id: service-gateway
#          # ⽬标服务地址(部署多实例,不能加子路径)
##          uri: http://localhost:9070 # 指定具体的微服务地址(原真实服务地址还是可以访问,如果要限制走网关可以加token验证机制)
##          uri: https://www.baidu.com # 指定具体的微服务地址
#          uri: lb://service-gateway
#          # gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
#          # 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
#          predicates:
#            # 当请求的路径为http://localhost:9070/looptest/gateway/api/hello时,转发到http://localhost:9070/gateway/api/hello
#            - Path=/looptest/gateway/api/hello # 本身就是基于path的反向代理
##            - Path=/**
#          filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
#            - StripPrefix=1 # 转发之前去掉1层路径(去除原始请求路径中的前1级路径,即/looptest去除)
##            - SetStatus=220 # 修改原始响应的状态码
#            # 名称必须为过滤器工厂类名的前缀(Log),而且参数只能有两个,由于NameValueConfig里只定义了两个属性
##            - Log=testName,testValue #自定义局部过滤器
##      httpclient:
##        connect-timeout: 10000
##        response-timeout: 5s

使用curl命令测试动态路由功能:

C:\Users\deepinsea>curl http://localhost:9070/service-provider-nacos/provider-nacos/hello
hi, this is service-provider-nacos!
C:\Users\deepinsea>curl http://localhost:9070/service-provider-nacos/provider-nacos/hello
hi, this is service-provider-api!
C:\Users\deepinsea>curl http://localhost:9070/gateway/api/hello
hello, 这里是service-gateway网关, 恭喜你请求了正确的路径!

测试成功,我们并没有配置路由信息,请求也能动态路由到后端服务上!

配置类开启

另外,网上还有一个大哥,直接从注册中心获取所有服务的路由信息并用一个数组保存,然后通过实现RouteDefinitionLocator路由定位接口来进行动态路由配置:

package com.deepinsea.common.route;
​
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import reactor.core.publisher.Flux;
​
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
​
/**
 * 根据微服务名,动态路由
 **/
@Component
public class MyRouteDefinitionLocator implements RouteDefinitionLocator {
​
    private static final String SERVICE_URL = "http://localhost:8848/nacos/v1/ns/catalog/services?hasIpCount=true&withInstances=false&pageNo=1&pageSize=1000";
    private static RestTemplate restTemplate = new RestTemplate();
​
//    @Autowired
//    private DiscoveryClient discoveryClient;
​
    @Value("${spring.application.name}")
    private String gatewayName;
​
    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        // 该方法会返回已经不存在的服务
//        List<String> serviceList = discoveryClient.getServices();
​
        Map<String,Object> map = restTemplate.getForObject(SERVICE_URL, Map.class);
        if (map == null || map.get("serviceList") == null){
            return Flux.empty();
        }
        List<Map<String,Object>> dtos= (List<Map<String, Object>>) map.get("serviceList");
        List<String> serviceList = new ArrayList<>(dtos.size());
        for (Map<String, Object> dto : dtos) {
            serviceList.add(dto.get("name").toString());
        }
​
        System.out.println("serviceList = "+serviceList);
        // 只有网关服务,直接返回
        if (serviceList.size() < 2){
            return Flux.empty();
        }
        // 服务的个数
        int serviceNum = serviceList.size();
        if (serviceList.contains(gatewayName)){
            // 排除网关服务
            serviceNum--;
​
        }
        RouteDefinition[] routeDefinitions = new RouteDefinition[serviceNum];
        int count = 0;
        for (String service : serviceList) {
            if (service.equalsIgnoreCase(gatewayName)){
                continue;
            }
            RouteDefinition definition = new RouteDefinition();
            definition.setId(service);
            definition.setUri(URI.create("lb://"+service));
            definition.setPredicates(Collections.singletonList(new PredicateDefinition("Path=/"+service+"/**")));
            definition.setFilters(Collections.singletonList(new FilterDefinition("StripPrefix=1")));
            routeDefinitions[count++] = definition;
        }
        return Flux.just(routeDefinitions);
    }
}

这种方式也是可以的,我们可以先将yml配置中的动态路由配置关闭:

    # 网关
    gateway:
      # 启用开关(默认开启)
      enabled: true
#      discovery:
#        locator:
#          # 开启从注册中心动态创建路由的功能,利用微服务名进行动态路由(在真实服务请求路径上加上/服务名)
#          # 例如:http://localhost:9070/service-provider-nacos/provider-nacos/hello
#          enabled: true
#          # 服务名转为小写(默认yml配置就是小写,这里只是保证一下)
#          lower-case-service-id: true
#      # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务] --本质就是反向代理
#      routes:
#        - id: service-provider-nacos             # 当前路由的标识, 要求唯一
#          uri: lb://service-provider-nacos       # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略路由请求(动态路由)
#          order: 10 # 路由的优先级,数字越小代表路由的优先级越高
#          predicates: # 断言(就是路由转发要满足的条件)
#            - Path=/provider-nacos/**             # 当请求路径满足Path指定的规则时,才进行路由转发
##          filters:
##            - AddRequestHeader=gateway, blue
#          ## 配置过滤器(局部)
#          filters:
##            - AddResponseHeader=X-Response-Foo, Bar
#            - AddResponseHeader=licence, value
#            ## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
#            - Authorize=true
#        # 我们⾃定义的路由 ID,保持唯⼀
#        - id: service-gateway
#          # ⽬标服务地址(部署多实例,不能加子路径)
##          uri: http://localhost:9070 # 指定具体的微服务地址(原真实服务地址还是可以访问,如果要限制走网关可以加token验证机制)
##          uri: https://www.baidu.com # 指定具体的微服务地址
#          uri: lb://service-gateway
#          # gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
#          # 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
#          predicates:
#            # 当请求的路径为http://localhost:9070/looptest/gateway/api/hello时,转发到http://localhost:9070/gateway/api/hello
#            - Path=/looptest/gateway/api/hello # 本身就是基于path的反向代理
##            - Path=/**
#          filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
#            - StripPrefix=1 # 转发之前去掉1层路径(去除原始请求路径中的前1级路径,即/looptest去除)
##            - SetStatus=220 # 修改原始响应的状态码
#            # 名称必须为过滤器工厂类名的前缀(Log),而且参数只能有两个,由于NameValueConfig里只定义了两个属性
##            - Log=testName,testValue #自定义局部过滤器
##      httpclient:
##        connect-timeout: 10000
##        response-timeout: 5s

然后重新启动项目,使用curl命令进行测试:

C:\Users\deepinsea>curl http://localhost:9070/service-provider-nacos/provider-nacos/hello
hi, this is service-provider-nacos!
C:\Users\deepinsea>curl http://localhost:9070/service-provider-nacos/provider-nacos/hello
hi, this is service-provider-api!
C:\Users\deepinsea>curl http://localhost:9070/gateway/api/hello
hello, 这里是service-gateway网关, 恭喜你请求了正确的路径!

同样生效,配置类配置动态路由也是可以的。动态路由可以开启的,这可以使我们少配置许多重复的路由;但是对于具体服务的自定义路由配置项,比如局部拦截器以及断言等功能,还是需要对服务路由进行详细配置的!

注意:使用yml配置文件开启动态路由配置后,由于构建路由时间较长,会导致后面构建路由的服务接口请求404的问题(加上Order、webflux延迟调用返回等问题会导致启动一段时间内服务接口404不可用)!

建议使用自定义动态路由配置的构建方式,并持久化路由配置,这样可以保证接口路由正常调用。

欢迎点赞,谢谢大佬了ヾ(◍°∇°◍)ノ゙