前言

前段时间业务拓展搭建了一个新的微服务,然后要在网关配置路由,配置路由的时候配置了一个鉴权的过滤器,结果发现过滤器怎么都不生效,哇~~ 人都麻了。后来经过一通扒源码底裤,彻底搞清楚了原因,竟然是一个服务发现的配置导致的。和广大掘友一起分享~~

在 application.yml 中配置路由

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true ## 这个属性很关键,后面会详细介绍
          lower-case-service-id: true #小写服务名
      routes:
        - id: hive-admin
          uri: lb://hive-admin
          predicates:
            - Path=/hive-admin/**
          filters:
            - name: AdminBackend
复制代码

我们依照已存在的其他微服务示例配置路由,至于 predicates 下面的几种 Route Predicate Factory 比较简单,这里就不详细介绍了,不清楚的可以参考官网文档详细说明 SpringCloud Gateway 配置详解

过滤器不生效

配置好之后我们从网关请求 /hive-admin/xxx 你会发现这个请求怎么都不会走到我们配置的 AdminBackendFilterFactory 过滤器, 直接就路由到目标微服务了。

 

下面我们来分析为什么过滤器未生效。在分析这个问题之前我们先了解一下 GatewayRouteDefinition 这个类。

RouteDefinition

从这个名字就能看出这个类就是 “路由定义”。其实我们在 application.yml 配置文件中配的就是这个,针对这段配置

- id: hive-admin
  uri: lb://hive-admin #这种写法代表去注册中心找服务
  predicates:   #官网有 11 种断言
    - Path=/hive-admin/**
  filters:
    - name: AdminBackend
复制代码

意思就是当我们访问 /hive-admin/** 的时候我们先过 AdminBackendFilterFactory 这个过滤器,如果放行就将请求转到注册中心中 hive-admin 这个服务。现在请求是正常过去了,但是没有走这个过滤器,那到底是啥原因?

遇到问题不要慌

很多新手程序员一遇到涉及框架源码内部的问题时就会很茫然,无从下手只能问度娘、Google,当然这是很正常的一件事,我当年也是这样。而且我还特别疑惑别的大牛都是怎么从源码排查出问题的,他们是怎么知道要看哪一块代码?该查哪个类哪一块代码的?下面我就分享一下遇到这种问题该如何去排查。真的是详细干货,所有源码问题都是类似的方式排查

 

开始源码分析

先预估断点进行 debug

我们要阅读源码解决问题,必定要通过 debug ,所以我们得先找到程序会经过的相关源码片段,然后打断点,查看断点前后的每一个方法调用栈,只要能调试起来,一切都是小问题。那如何找到呢?还记得我们的配置文件吧?应用启动源码肯定是要加载这个的,直接按住 ctrl 鼠标左键点 filters 属性进入到映射的 Java

 

点进来跳到 GatewayProperties,发现了成员变量

private List<RouteDefinition> routes = new ArrayList<>();
复制代码

我们继续点进 RouteDefinition 类,发现了

@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList<>();

@Valid
private List<FilterDefinition> filters = new ArrayList<>();
复制代码

找到 predicategetPredicates() 方法,再按住 ctrl 点鼠标左键查找哪里调用了这个方法

 

发现了这三个地方,第一个一看就不是我们要找的东西。后面两个我们可以都点进去看看,结果发现 DiscoveryClientRouteDefinitionLocator 实现了 RouteDefinitionLocator 接口。RouteDefinitionLocator 接口里面只有一个方法 getRouteDefinitions()

好家伙这不是就是获取 RouteDefinitions 路由定义吗?然后我们看 DiscoveryClientRouteDefinitionLocator.getRouteDefinitions() 的具体实现。在这个方法打上断点,发现应用启动会走到这里!

观察构造 RouteDefinition 的核心代码

观察 DiscoveryClientRouteDefinitionLocator.getRouteDefinitions() 发现核心代码

serviceInstances.filter(instances -> !instances.isEmpty()).map(instances -> instances.get(0))
      .filter(includePredicate).map(instance -> {
         RouteDefinition routeDefinition = buildRouteDefinition(urlExpr, instance);
         //省略构造 predicate ......(后面会详细介绍)
         //省略构造 filter ......   (后面会详细介绍)
         return routeDefinition;
      });
复制代码

可以看到这里用 serviceInstances 构造了 RouteDefinition 然后我们继续去查 serviceInstances 是个什么东西,结果发现这玩意是从 eureka 注册中心读取的服务实例,其实从名字早就该发现了~~

由此得知 DiscoveryClientRouteDefinitionLocator 是专门从注册中心读取服务实例然后构造 RouteDefinition 的。然后我们通过它的构造方法找到 Bean 定义的位置

@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled")
public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(
      ReactiveDiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
   return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);
}
复制代码

注意看 ConditionalOnProperty 里面的 name 这个 key 不就是我们配置文件里面配的吗,由此得出结论,这个配置为 true 时代表我们开启从 eureka 注册中心拉取实例信息构造 RouteDefinition。(我一开始还真不知道,因为这都是前辈配置好的......)

找出所有构造 RouteDefinition 的地方

然后我们观察 RouteDefinitionLocator 的其他实现类

 

其他的先不说,第五个 PropertiesRouteDefinitionLocator 不用我说你都猜到了吧?就是从配置文件读取我们的配置构造 RouteDefinition。其他的从名字上一眼看不出来,这不重要,我们只要知道这些地方都是构造 RouteDefinition 的就行了。

我们将断点打到 DiscoveryClientRouteDefinitionLocator.getRouteDefinitions() 方法,寻找它是从哪里执行过来的

 

结果发现它是从 CompositeRouteDefinitionLocator 执行过来的,于是查看它的源码发现它有一个成员变量是

private final Flux<RouteDefinitionLocator> delegates;
复制代码

进而我们通过构造方法可以找到一个 Bean 的定义

@Bean
@Primary
public RouteDefinitionLocator routeDefinitionLocator(List<RouteDefinitionLocator> routeDefinitionLocators) {
   return new CompositeRouteDefinitionLocator(Flux.fromIterable(routeDefinitionLocators));
}
复制代码

柳暗花明了,原来这个类是集成了所有的 RouteDefinitionLocator (路由定位器),然后构造出所有的 RouteDefinition。其实英语好的同志应该早就发现了,因为我 Google 了一下 Composite 这个单词的意思就是 “混合的、复合的”,它集成了所有路由定位器。

初步确诊

到这里我们其实就大概确诊了过滤器未生效的原因,我们代码中实际使用的 RouteDefinition 肯定不是配置文件里面我们配置的,应该是其他几个实现类的覆盖了我们配置文件中的配置

- id: hive-admin
  uri: lb://hive-admin #这种写法代表去注册中心找服务
  predicates:   #官网有 11 种断言
    - Path=/hive-admin/**
  filters:
    - name: AdminBackend
复制代码

也就是说上面这段手动配置被源码中的自动配置覆盖了,而且源码中的配置没有带 AdminBackendFilterFactory 过滤器.

断点打到 CompositeRouteDefinitionLocator 定义 Bean 的地方

 

注意右边的 List 元素顺序,因为源码中最终就是通过遍历这个 List 构造出所有的 RouteDefinition

逐步追踪

通过 CompositeRouteDefinitionLocator -> CachingRouteDefinitionLocator -> RouteDefinitionRouteLocator -> CachingRouteLocator -> RoutePredicateHandlerMapping 最终定位到匹配请求的 RoutePredicateHandlerMapping,看到 XxxHandlerMapping 是不是熟悉了?这不就和 SpringMVC 类似嘛~

最终确诊

观察核心方法 RoutePredicateHandlerMapping.lookupRoute() 的核心代码

    this.routeLocator.getRoutes()
      .concatMap(route -> Mono.just(route).filterWhen(r -> {
         exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
         return r.getPredicate().apply(exchange); //匹配断言 ,决定该请求要走哪一个 RouteDefinition / Route
      });
复制代码

由于服务发现定义的 RouteDefinition 在我们配置文件的前面,然后两个 RouteDefinitionpredicate 又相同,所以走到这里的时候会用服务发现 DiscoveryRouteDefinitionLocator 构造出的 RouteDefinition,然后我们再回去看 DiscoveryRouteDefinitionLocator.getRouteDefinitions() 前面被我们省略的构造 filter 的代码

for (FilterDefinition original : this.properties.getFilters()) {
   FilterDefinition filter = new FilterDefinition();
   filter.setName(original.getName());
   for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
      String value = getValueFromExpr(evalCtxt, parser, instanceForEval, entry);
      filter.addArg(entry.getKey(), value);
   }
   routeDefinition.getFilters().add(filter);
}
复制代码

问题的关键在这个 private final DiscoveryLocatorProperties properties; ,我们找到定义 Bean 的地方

@Bean
public DiscoveryLocatorProperties discoveryLocatorProperties() {
   DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties();
   properties.setPredicates(initPredicates());
   properties.setFilters(initFilters());
   return properties;
}
复制代码

再点进去这个 initFilters() 源码

public static List<FilterDefinition> initFilters() {
   ArrayList<FilterDefinition> definitions = new ArrayList<>();
   FilterDefinition filter = new FilterDefinition();
   //省略...
   filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class));
   definitions.add(filter);
   return definitions;
}
复制代码

好了,最终答案出来了,通过服务发现构造的 RouteDefinition 默认给的过滤器只有一个 RewritePathGatewayFilterFactory ,它的作用是比如当我们访问 https://gateway域名/hive-admin/user/{id} 的时候,到下游 hive-admin 服务会变成 https://ip+port/user/{id}

再看 initPredicate() 源码

public static List<PredicateDefinition> initPredicates() {
   ArrayList<PredicateDefinition> definitions = new ArrayList<>();
   // add a predicate that matches the url at /serviceId/**
   PredicateDefinition predicate = new PredicateDefinition();
   predicate.setName(normalizeRoutePredicateName(PathRoutePredicateFactory.class));
   predicate.addArg(PATTERN_KEY, "'/'+serviceId+'/**'");
   definitions.add(predicate);
   return definitions;
}
复制代码

注释的明明白白,当我们访问 /serviceId/**(这里 serviceId 就是子微服务的 spring.application.name) 的时候就会匹配上服务发现的 predicate ,那自然走到的服务发现生成的 RouteDefinition 定义的过滤器 RewritePathGatewayFilterFactory

结论

其实就是因为我们配置文件中的 predicate 和 服务发现自己生成的 RouteDefinitionpredicate 相同,又因为加载的时候服务发现生成的 RouteDefinition 在我们配置文件生成的 RouteDefinition 前面,导致 RoutePredicateHandlerMapping 进行匹配的时候匹配到了服务发现生成的 Route,而服务发现生成的 Route 只有一个 RewritePathGatewayFilterFactory 过滤器,所以我们的过滤器没生效

我为什么通过源码排查这个问题

其实通过源码排查问题是一个耗时费力的事情,我当时通过源码来排查这个问题其实只是因为我 Google 没有找到相关资料......居然没有前辈出现过这种问题么!!!这里我要甩锅给前辈,你为啥好好的要配置 spring.cloud.gateway.discovery.locator.enabled = true

 

如果能简单的 Google 和度娘就能解决,为了节省时间肯定是直接 Google 。不过值得一提的是通过源码去排查问题会有很多收益!

说到这里我就想多提一句,不知道为什么现在很多面试官总是问这个源码那个源码......如果候选人答不上来,候选人的面试分就会打低。其实我很想吐槽这种面试官啊!我平时也很少去看源码,不带着问题去看的话会很枯燥的,但是不代表我没有阅读源码解决问题的能力啊!带着问题去看源码我分分钟排查出问题啊!

带着问题,有目的性的去阅读源码,效率会很高~

解决方案

知道了问题发生的原因,解决起来就很简单了

配置和服务发现不同的 predicate

只要我们的 predicate 和服务发现构造出的 predicate 不同即可,例如我们可以这样配置

    - id: hive-admin
      uri: lb://hive-admin
      predicates:
        - Host=**.admin-hive.yinshantech.cn
        - Path=/hive-admin-xx/**
      filters:
        - name: AdminBackend 
复制代码

不开启服务发现构造 RouteDefinition

直接在配置文件中 spring.cloud.gateway.discovery.locator.enabled = false

思维拓展

所以到这里大家应该都知道了,只要我们开启了 spring.cloud.gateway.discovery.locator.enabled = true ,可以理解为下面这段配置就是系统自带的,即使我们不配置。

    routes:
        - id: hive-admin
          uri: lb://hive-admin
          predicates:
            - Path=/hive-admin/**
          filters:
            - name: RewritePathGateway
复制代码

那你有没有想到一个问题?有些时候我们可能会有一些内部服务也注册在 eureka,但是我们并不想对外开放,但是由于开启了服务发现路由定位器导致我们可以从公网经过网关去访问......让我们的内部应用暴露在了公网。只要通过以下 url 即可进行访问

https://gateway域名/{serviceId}/xxx
复制代码

我也是这样发现了我们很多用于处理消息、修补数据、定时任务的应用全部裸奔在网关,你们公司不会也这样吧......

解决裸奔问题

最简单的解决方案就是我们上面说的第二种,不开启服务发现构造 RouteDefinition 。但是说起来容易做起来难啊!因为现在配置已经是开启了,又有几十个微服务,你也不知道哪些用了服务发现,哪些是用自己配置的。如果直接干掉这个配置,影响太大了,我们得考虑兼容,这就和 APP 发版是一样的,你得考虑老版本兼容问题。

于是我想到了一个思路,先关闭 DiscoveryClientRouteDefinitionLocator,然后自己定制一个路由定位器来替代它,然后在 getRouteDefinitions() 实现中去排除掉一些我们在注册中心里面但是又不想暴露给公网的应用。

首先我们定义一个配置 Bean 来动态扩展不想暴露的服务

@ConfigurationProperties(prefix = "spring.cloud.gateway.discovery.locator.hidden")
@Component
@Data
public class SpecificHiddenDiscoveryClientRouteDefinitionLocatorProperties {
    /**
     * 要在 RouteDefinition 中隐藏的服务名
     * */
    private List<String> serviceInstances = new ArrayList<>();
}
复制代码
spring.cloud.gateway.discovery.locator.hidden.service-instances = ap-INTERNAL,HIVE-INTERNAL
复制代码

然后定义自己定制的 RouteDefinitionLocator 实现

@Configuration
@Slf4j
public class DiscoveryClientRouteDefinitionLocatorConfiguration {
    @Bean
    @ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled",havingValue = "false")
    public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(
            ReactiveDiscoveryClient discoveryClient,
            DiscoveryLocatorProperties properties,
            SpecificHiddenDiscoveryClientRouteDefinitionLocatorProperties hiddenServiceInstanceProperties) {
        log.info("加载自定义 DiscoveryClientRouteDefinition ...");
        return new SpecificDiscoveryClientRouteDefinitionLocator(discoveryClient, properties,hiddenServiceInstanceProperties);
    }
}
复制代码

注意细节,在这里我们当 spring.cloud.gateway.discovery.locator.enabled = false 的时候才向 Spring 提交这个 Bean,当我们把这个配置关闭时,系统的DiscoveryClientRouteDefinitionLocator 就不会被 Spring 加载了,为了兼容我们必须得这么做。

然后我们在 SpecificDiscoveryClientRouteDefinitionLocator 实现中排除掉我们配置不想暴露的服务,核心代码

public SpecificDiscoveryClientRouteDefinitionLocator(ReactiveDiscoveryClient discoveryClient, 
                                                        DiscoveryLocatorProperties properties,
                                                     SpecificHiddenDiscoveryClientRouteDefinitionLocatorProperties hiddenServiceInstanceProperties) {
   //省略......
   //去掉我们配置要隐藏的内部应用,从而去掉 RouteDefinition
    serviceInstances = discoveryClient.getServices()
            .flatMap(service ->
                    discoveryClient.getInstances(service)
                            .filter(hiddenPredicate(hiddenServiceInstanceProperties))
                            .collectList());
}

/**
 * 隐藏过滤断言,不区分服务名大小写
 * */
private Predicate<? super ServiceInstance> hiddenPredicate(
    SpecificHiddenDiscoveryClientRouteDefinitionLocatorProperties hiddenServiceInstanceProperties) {
    List<String> serviceInstances = hiddenServiceInstanceProperties.getServiceInstances();
    return s ->
            !serviceInstances.contains(s.getServiceId().toLowerCase()) && 
            !serviceInstances.contains(s.getServiceId().toUpperCase());
}
复制代码

结语

百度不到问题的解决方案不重要,重要的是自己能掌握通过源码解决问题的能力。通过这次源码分析,让我一个从未花时间学习过 SpringCloud Gateway 组件的人几乎对它已经了如指掌~~~~

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐