原始碼分析SpringCloud Gateway如何載入斷言(predicates)與過濾器(filters)

努力的小雨發表於2021-05-08

  我們今天的主角是Gateway閘道器,一聽名字就知道它基本的任務就是去分發路由。根據不同的指定名稱去請求各個服務,下面是Gateway官方的解釋:

https://spring.io/projects/spring-cloud-gateway,其他的博主就不多說了,大家多去官網看看,只有官方的才是最正確的,迴歸主題,我們的過濾器與斷言如何載入進來的,並且是如何進行對請求進行過濾的。

  大家如果對SpringBoot自動載入的熟悉的話,一定知道要看一個程式碼的原始碼,要找到META-INF下的spring.factories,具體為啥的博主就不多說了,網上也有很多講解自動載入的原始碼分析,今天就講解Gateway,所有專案三板斧:加依賴、寫註解、弄配置

  依賴:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

  註解:啟動類上需要新增@EnableDiscoveryClient,啟動服務發現

  配置:

spring:
  cloud:
    gateway:
      routes:
       - id: after-route #id必須要唯一
         uri: lb://product-center
         predicates:
          - After=2030-12-16T15:53:22.999+08:00[Asia/Shanghai]
        filters:
          - PrefixPath=/product-api    

  大家看到這個配置的時候,為什麼我們寫After斷言與PrefixPath過濾器,gateway就會自動識別呢,那我們有沒有那一個地方可以看到所有的自帶的屬性呢?當然有,而且我們本篇就主要講解為什麼gateway會自動識別,並且我們要自己實現並且新增自定義屬性。開始原始碼解析第一步,找到自動載入的類一探究竟;

 

 

   看到這裡的時候,第一步就成功了,剩下的就是找到org.springframework.cloud.gateway.config.GatewayAutoConfiguration這個關鍵類,我們主要看看裡面的兩個類

    @Bean
    public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,
                                                   List<GatewayFilterFactory> GatewayFilters,
                                                   List<RoutePredicateFactory> predicates,
                                                   RouteDefinitionLocator routeDefinitionLocator) {
        return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates, GatewayFilters, properties);
    }

    @Bean
    @Primary
    //TODO: property to disable composite?
    public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
        return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
    }

  這倆個類配置,大家可能非常熟悉,大家上手一個新知識點的時候,肯定會找一些快速入門的文章看看,博主還是習慣直接找官方的quick start來看,大家可以看看這些快速上手專案:https://spring.io/guides/gs/gateway/

  所以博主直接就找到了RouteLocator這個類配置,果不其然,我們找到了斷言與過濾器的注入,雖然實在方法體內作為引數傳入,但是會被spring解析到,直接去工廠裡拿到,具體怎麼拿呢?我們再來看看:

 1 public BeanWrapper instantiateUsingFactoryMethod(
 2             String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {
 3 
 4         .....
 5 
 6             for (Method candidate : candidates) {
 7                 Class<?>[] paramTypes = candidate.getParameterTypes();
 8 
 9                 if (paramTypes.length >= minNrOfArgs) {
10                     ArgumentsHolder argsHolder;
11 
12                     if (explicitArgs != null) {
13                         // Explicit arguments given -> arguments length must match exactly.
14                         if (paramTypes.length != explicitArgs.length) {
15                             continue;
16                         }
17                         argsHolder = new ArgumentsHolder(explicitArgs);
18                     }
19                     else {
20                         // Resolved constructor arguments: type conversion and/or autowiring necessary.
21                         try {
22                             String[] paramNames = null;
23                             ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer();
24                             if (pnd != null) {
25                                 paramNames = pnd.getParameterNames(candidate);
26                             }
27                             //主要就是會進入到這裡去解析每一個引數型別
28                             argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw,
29                                     paramTypes, paramNames, candidate, autowiring, candidates.length == 1);
30                         }
31                         catch (UnsatisfiedDependencyException ex) {
32                             if (logger.isTraceEnabled()) {
33                                 logger.trace("Ignoring factory method [" + candidate + "] of bean '" + beanName + "': " + ex);
34                             }
35                             // Swallow and try next overloaded factory method.
36                             if (causes == null) {
37                                 causes = new LinkedList<>();
38                             }
39                             causes.add(ex);
40                             continue;
41                         }
42                     }
43 
44                     int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
45                             argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));
46                     // Choose this factory method if it represents the closest match.
47                     if (typeDiffWeight < minTypeDiffWeight) {
48                         factoryMethodToUse = candidate;
49                         argsHolderToUse = argsHolder;
50                         argsToUse = argsHolder.arguments;
51                         minTypeDiffWeight = typeDiffWeight;
52                         ambiguousFactoryMethods = null;
53                     }
54                     // Find out about ambiguity: In case of the same type difference weight
55                     // for methods with the same number of parameters, collect such candidates
56                     // and eventually raise an ambiguity exception.
57                     // However, only perform that check in non-lenient constructor resolution mode,
58                     // and explicitly ignore overridden methods (with the same parameter signature).
59                     else if (factoryMethodToUse != null && typeDiffWeight == minTypeDiffWeight &&
60                             !mbd.isLenientConstructorResolution() &&
61                             paramTypes.length == factoryMethodToUse.getParameterCount() &&
62                             !Arrays.equals(paramTypes, factoryMethodToUse.getParameterTypes())) {
63                         if (ambiguousFactoryMethods == null) {
64                             ambiguousFactoryMethods = new LinkedHashSet<>();
65                             ambiguousFactoryMethods.add(factoryMethodToUse);
66                         }
67                         ambiguousFactoryMethods.add(candidate);
68                     }
69                 }
70             }
71 
72             .....
73         return bw;
74     }

  每一個引數都需要解析,但是看這裡不像沒關係,繼續往下走:就會看到

    private ArgumentsHolder createArgumentArray(
            String beanName, RootBeanDefinition mbd, @Nullable ConstructorArgumentValues resolvedValues,
            BeanWrapper bw, Class<?>[] paramTypes, @Nullable String[] paramNames, Executable executable,
            boolean autowiring, boolean fallback) throws UnsatisfiedDependencyException {

        ....
        //這下就是了,每個引數都被進行解析
        for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) {
            ....
                try {
                //我們的引數就是在這裡被進行解析的--resolveAutowiredArgument
                    Object autowiredArgument = resolveAutowiredArgument(
                            methodParam, beanName, autowiredBeanNames, converter, fallback);
                    args.rawArguments[paramIndex] = autowiredArgument;
                    args.arguments[paramIndex] = autowiredArgument;
                    args.preparedArguments[paramIndex] = new AutowiredArgumentMarker();
                    args.resolveNecessary = true;
                }
                catch (BeansException ex) {
                    throw new UnsatisfiedDependencyException(
                            mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), ex);
                }
            }
        }
        //其他不重要的,直接忽略掉
        ...
        return args;
    }

  開始解析的時看到了,我們需要把斷言和過濾器列表都加在進來,那spring是如何載入的呢?是根據方法體內傳入的型別找到所有實現了斷言和過濾器工廠介面的類並且進行獲取例項,我們仔細這些工廠的實現類,就會找到我們的使用的一些屬性,比如我們例子中的PrefixPath過濾器和Path斷言;

    protected Map<String, Object> findAutowireCandidates(
            @Nullable String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {
        //主要的就是這個,beanNamesForTypeIncludingAncestors方法,該方法就是從bean工廠中獲取所有當前類的實現例項名稱,
        String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                this, requiredType, true, descriptor.isEager());
        Map<String, Object> result = new LinkedHashMap<>(candidateNames.length);
        ...
        //遍歷名稱,進行例項化
        for (String candidate : candidateNames) {
            if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
                addCandidateEntry(result, candidate, descriptor, requiredType);
            }
        }
        .....
        return result;
    }

 

 

 

   這下我們知道了,系統配置的斷言和過濾器是如何被載入 的了,那我們還有一個問題,如果我自定義一個,如何被系統識別呢?並且怎麼進行配置呢?不難發現我們之前看原始碼時,他是被spring通過找工廠實現類找到並且載入進來的,那我們自己實現工廠介面並且使用@Component註解,讓spring載入進來不就的了嗎?但是你會發現系統自定義的屬性斷言或者過濾器都有工廠名字的字尾,這是為什麼呢?影響我們自定義 的類被載入到gateway中且生效嗎?事實是會影響,那為什麼影響呢?我們還是看原始碼。因為我們之前的類載入還沒有看完,我們最開始的時候就找到了兩個@bean 的自動載入,那這兩個類例項化的時候都做了哪些工作,我們還沒有細看;

    public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator,
                                       List<RoutePredicateFactory> predicates,
                                       List<GatewayFilterFactory> gatewayFilterFactories,
                                       GatewayProperties gatewayProperties) {
        this.routeDefinitionLocator = routeDefinitionLocator;
        initFactories(predicates);
        gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
        this.gatewayProperties = gatewayProperties;
    }

  initFactories(predicates):這段程式碼主要是進行解析斷言工廠實現類;並且放入一個Map中,

  gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory)):跟斷言的程式碼幾乎一樣,因為沒有其他多餘的邏輯,所以沒有封裝到方法中,直接使用java8 的流特性,寫完了遍歷的過程。大家要注意一段程式碼就是factory.name(),這裡使用了一個方法;

    default String name() {
        return NameUtils.normalizeRoutePredicateName(getClass());
    }

  主要就是把當前類包含工廠名字的部分去掉了,然後用剩下的字串當key值,所以我們可以使用工廠名字做後墜,也可以不用,但是剩下的字元則是你要寫進配置的關鍵字,不過博主基本都是按照系統自帶屬性一樣,用的是工廠介面的名字做的字尾。

   好了,今天就講解這麼多,下次在講解gateway接到請求後,是如何進行一步一步過濾的,何時進行斷言校驗的。一次不講這麼多,消化了就好。


相關文章