前言:

本篇介绍SpringCloud的网关模块的RoutePredicateFactory,通过提供的RoutePredicateFactory来实现配置不同的路由规则。目前支持12种RoutePredicateFactory,简介如下:

  • QueryRoutePredicateFactory:根据query参数路由

  • PathRoutePredicateFactory:根据Path路由

  • AfterRoutePredicateFactory:根据请求的时间路由,在指定时间之后

  • BeforeRoutePredicateFactory:根据请求的时间路由,在指定时间之前

  • BetweenRoutePredicateFactory:根据请求的时间路由,在指定时间范围内

  • CookieRoutePredicateFactory:根据请求头中的Cookie路由

  • HeaderRoutePredicateFactory:根据请求头中的值路由

  • HostRoutePredicateFactory:根据请求头中的Host路由

  • MethodRoutePredicateFactory:根据请求Method路由

  • RemoteAddrRoutePredicateFactory:根据请求的远程IP路由

  • WeightRoutePredicateFactory:根据权重路由

  • ReadBodyPredicateFactory :暂未官方文档说明

 

1、QueryRoutePredicateFactory

根据查询的参数路由,配置关键词:Query,示例如下:

spring:
  cloud:
    gateway:
      routes:
        - id: query_hello          # 自定义的路由ID
          uri: forward:/hello      # 跳转的路径,这里forward协议是跳转到当前的服务的API
          predicates:
            - Query=q, hello       # 路由规则,根据查询条件,需要满足{url}?q=hello
  • Query={key}:只需要满足query参数中存在key即可

  • Query={key}, {value},需要满足query的参数中存在key的参数值必须为value

  • 其中,key不支持正则表达式,value支持正则表达式

源码:

public boolean test(ServerWebExchange exchange) {
    //如果没有配置{value},则只需要判断{key}是否存在即可
   if (!StringUtils.hasText(config.regexp)) {
      return exchange.getRequest().getQueryParams()
            .containsKey(config.param);
   }
    //校验{value}是否匹配
   List<String> values = exchange.getRequest().getQueryParams()
         .get(config.param);
   if (values == null) {
      return false;
   }
   for (String value : values) {
      if (value != null && value.matches(config.regexp)) {
         return true;
      }
   }
   return false;
}

2、PathRoutePredicateFactory

根据请求Path路由,配置关键词:Path,示例如下:

spring:
  cloud:
    gateway:
      routes:
          - id: path_baidu
            uri: https://www.baidu.com
            predicates:
                - Path=/baidu
          - id: path_var
           uri: forward:/hello?var={ziya}
           predicates:
               - Path=/var/{ziya}
           filters:
               - AddRequestParameter=ziya, zy-{ziya}   # 在AddRequestParameter Filter中引用URI模板变量:ziya
         - id: path_regex
           uri: forward:/hello
           predicates:
               - Path=/regex/**

支持的配置格式:

  • Path=/baidu:固定路径

  • Path=/var/{ziya}:支持定义URL模板变量,该变量可以在自定义的GatewayFilter中使用,也可以结合springcloud-gateway自带的GatewayFilter使用,在配置文件中使用该变量

//在自定义的GatewayFilter中读取URL模板变量:hello
String value = ServerWebExchangeUtils.getUriTemplateVariables(exchange).get("hello");
  • Path=/regex/**:支持模糊匹配的路径,该配置将/regex下的所有请求转到指定的uri。

源码:

//根据配置的路径创建Pattern
synchronized (this.pathPatternParser) {
   pathPatternParser.setMatchOptionalTrailingSeparator(
         config.isMatchOptionalTrailingSeparator());
   config.getPatterns().forEach(pattern -> {
      PathPattern pathPattern = this.pathPatternParser.parse(pattern);
      pathPatterns.add(pathPattern);
   });
}

public boolean test(ServerWebExchange exchange) {
    //获得Path
   PathContainer path = parsePath(
         exchange.getRequest().getURI().getRawPath());
    //用编译好的Patterns来匹配Path
   Optional<PathPattern> optionalPathPattern = pathPatterns.stream()
         .filter(pattern -> pattern.matches(path)).findFirst();

    //如果匹配到,将URI中的模板参数提取出来,放到Exchange中
   if (optionalPathPattern.isPresent()) {
      PathPattern pathPattern = optionalPathPattern.get();
      traceMatch("Pattern", pathPattern.getPatternString(), path, true);
      PathMatchInfo pathMatchInfo = pathPattern.matchAndExtract(path);
      putUriTemplateVariables(exchange, pathMatchInfo.getUriVariables());
      return true;
   }
   else {
      traceMatch("Pattern", config.getPatterns(), path, false);
      return false;
   }
}

3、AfterRoutePredicateFactory

根据请求时间路由,配置关键词:After,示例如下:

spring:
  cloud:
    gateway:
      routes:
        - id: after_world
          uri: forward:/world
          predicates:
            - After=2020-05-28T22:10:00.000+08:00[Asia/Shanghai]
        - id: after_baidu
          uri: https://www.baidu.com
          predicates:
            - After=2020-05-28T22:00:00.000+08:00[Asia/Shanghai]

当请求的时间在指定时间之后,就会将请求路由到指定的uri上

注意:

  • 如果有多个满足条件的after配置,则会路由到第一个uri。

  • 参数格式,满足ZonedDateTime格式,

        如:2020-05-28T22:00:00.000+08:00[Asia/Shanghai]

源码:

public boolean test(ServerWebExchange serverWebExchange) {
   final ZonedDateTime now = ZonedDateTime.now();
   return now.isAfter(config.getDatetime());
}

4、BeforeRoutePredicateFactory

根据请求时间路由,配置关键词:Before,示例如下:

spring:
  cloud:
    gateway:
      routes:
        - id: before_hello
          uri: forward:/hello
          predicates:
            - Before=2020-05-29T22:01:00.000+08:00[Asia/Shanghai]
        - id: before_world
          uri: forward:/world
          predicates:
            - Before=2020-05-29T22:00:00.000+08:00[Asia/Shanghai]

当请求的时间在指定时间之前,将会将请求路由到指定的uri上

注意(同after):

  • 如果有多个满足条件的before配置,则会路由到第一个uri。

  • 参数格式,满足ZonedDateTime格式,

    如:2020-05-28T22:00:00.000+08:00[Asia/Shanghai]

源码:

public boolean test(ServerWebExchange serverWebExchange) {
   final ZonedDateTime now = ZonedDateTime.now();
   return now.isBefore(config.getDatetime());
}

5、BetweenRoutePredicateFactory

根据请求时间路由,配置关键词:Between,示例如下:

spring:
  cloud:
    gateway:
      routes:
        - id: before_world
          uri: forward:/world
          predicates:
            - Between=2020-05-29T10:00:00.000+08:00[Asia/Shanghai], 2020-05-29T11:00:00.000+08:00[Asia/Shanghai]
        - id: before_hello
          uri: forward:/hello
          predicates:
            - Between=2020-05-29T10:40:00.000+08:00[Asia/Shanghai], 2020-05-29T10:55:00.000+08:00[Asia/Shanghai]

当请求的时间在指定时间范围内,将会将请求路由到指定的uri上

注意(同after):

  • 如果有多个满足条件的between配置,则会路由到第一个uri。

  • 参数格式,满足ZonedDateTime格式,

    如:2020-05-28T22:00:00.000+08:00[Asia/Shanghai]

  • 如果同时存在:before,after,between配置,会路由到第一个满足条件的uri

源码:

public boolean test(ServerWebExchange serverWebExchange) {
   final ZonedDateTime now = ZonedDateTime.now();
   return now.isAfter(config.getDatetime1())
         && now.isBefore(config.getDatetime2());
}

6、CookieRoutePredicateFactory

根据请求头中的Cookie路由,配置关键词:Cookie,示例如下:

spring:
  cloud:
    gateway:
      routes:
        - id: cookie_hello2
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, cookie-regexp
          predicates:
            - Cookie=X-key, v.*
        - id: cookie_hello
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, cookie-no-value
          predicates:
            - Cookie=X-key, value

    当请求中存在X-key=value的Cookie,则将请求路由到指定的uri,如果有多个满足,则路由到第一个匹配的URI。匹配规则如下:

  • Cookie=X-key, value:指定Cookie的值

  • Cookie=X-key, v.*:正则表达式匹配Cookie的值

源码:

public boolean test(ServerWebExchange exchange) {
   List<HttpCookie> cookies = exchange.getRequest().getCookies()
         .get(config.name);
   if (cookies == null) {
      return false;
   }
   for (HttpCookie cookie : cookies) {
       //正则表达式匹配
      if (cookie.getValue().matches(config.regexp)) {
         return true;
      }
   }
   return false;
}

7、HeaderRoutePredicateFactory

根据请求头中的值路由,配置关键词:Header,示例如下:

spring:
  cloud:
    gateway:
      routes:
        - id: header_hello2
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, header-value-zy
          predicates:
            - Header=X-zy, v-zy
        - id: header_hello1
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, header-no-value
          predicates:
            - Header=X-zy

当请求中存在X-key=value的Header,则将请求路由到指定的uri,如果有多个满足,则路由到第一个匹配的URI。配置Value时,支持正则表达式。

 注意:如果需要按先匹配指定值优先,然后再匹配Header名,则需要将指定Header值的路由配置在前面。

  源码:

//是否配置了Header值
boolean hasRegex = !StringUtils.isEmpty(config.regexp);

public boolean test(ServerWebExchange exchange) {
   List<String> values = exchange.getRequest().getHeaders()
         .getOrDefault(config.header, Collections.emptyList());
   //如果不存在指定的Header名,返回不匹配
   if (values.isEmpty()) {
      return false;
   }
   //如果指定了Header值
   if (hasRegex) {
      // 通过正则表达式匹配
      return values.stream()
            .anyMatch(value -> value.matches(config.regexp));
   }
   //如果配置的时没有指定Header值,则不用判断值,直接匹配
   return true;
}

8、HostRoutePredicateFactory

根据请求头中的Host路由,配置关键词:Host,示例如下

spring:
  cloud:
    gateway:
      routes:
        - id: host-hello-dev
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, dev
          predicates:
            - Host=dev.wx.com
        - id: host-hello-multi
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, multi
          predicates:
            - Host=dev.wx.com, qa.wx.com
        - id: host-hello-multi-var
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, {env}
          predicates:
            - Host={env}.wx.com, {env}.bd.com
        - id: host-hello-star
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, env-star
          predicates:
            - Host=**.wx.com
        - id: host-hello-env
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, env-{env}
          predicates:
            - Host={env}.wx.com

根据请求头中的Host属性来匹配,如果能匹配到,则将请求路由到指定URI上,如果配置了多个Host,则路由到第一个匹配。匹配规则如下:

  • Host=dev.wx.com:根据固定的Host值匹配

  • Host=**.wx.com:根据通配符匹配

  • Host={env}.wx.com:和通配符效果相同,区别是:可以在自定义的GatewayFilter中通过API获取URI模板变量或者通过配置springcloud-gateway提供的GatewayFilter时引用URI模板变量。

  • Host=dev.wx.com, qa.wx.com:可以配置多个Host来进行匹配

URI模板变量的使用方式:

//通过API获取
ServerWebExchangeUtils.getUriTemplateVariables(exchange).get("env");
//配置文件中配置
filter:
    - AddRequestParameter=var, env-{env}

源码:

public boolean test(ServerWebExchange exchange) {
    //取Header中的第一个host
   String host = exchange.getRequest().getHeaders().getFirst("Host");
   //如果配置了多个pattern,查找第一个匹配的pattern
   Optional<String> optionalPattern = config.getPatterns().stream()
         .filter(pattern -> pathMatcher.match(pattern, host)).findFirst();

   if (optionalPattern.isPresent()) {
       //将URI中的模板变量添加到exchange中,供其他GatewayFilter使用
      Map<String, String> variables = pathMatcher
            .extractUriTemplateVariables(optionalPattern.get(), host);
      ServerWebExchangeUtils.putUriTemplateVariables(exchange, variables);
      return true;
   }

   return false;
}

 注意:

  • 通过源码发现,要注意的是获取Header中的第一个Host,如果通过代理添加了多个Host,其他的会被忽略。

  • 如果配置:Host=dev.wx.com, {env}.bd.com,而其他地方引用了{env}这个模板变量,当通过dev.wx.com访问时,就会出现500错误,无法获取{env}变量。

9、MethodRoutePredicateFactory

根据请求Method路由,配置关键词:Method,示例如下

spring:
  cloud:
    gateway:
      routes:
        - id: method-post
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, post
          predicates:
            - Method=post
        - id: method-get
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, get
          predicates:
            - Method=get, post

根据请求的Method进行匹配,如果能匹配到,则将请求路由到指定URI上;支持指定多个Method;配置的method不区分大小写;如果有多个匹配的Method,则路由到匹配到的第一个URI。

源码:

public boolean test(ServerWebExchange exchange) {
   HttpMethod requestMethod = exchange.getRequest().getMethod();
   return stream(config.getMethods())
         .anyMatch(httpMethod -> httpMethod == requestMethod);
}

10、RemoteAddrRoutePredicateFactory

根据请求的远程IP路由,配置关键词:RemoteAddr,示例如下

spring:
  cloud:
    gateway:
      routes:
        - id: remote-addr1
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, addr1
          predicates:
            - RemoteAddr=192.168.0.100/24
        - id: method-addr2
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, addr2
          predicates:
            - RemoteAddr=192.168.0.100
        - id: method-addr3
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, addr2
          predicates:
            - RemoteAddr=192.168.0.100,192.168.0.101,192.168.0.102

根据请求的远程IP进行匹配,如果能匹配到,则将请求路由到指定URI上;配置规则如下:

  • 指定一个固定的IP

  • 指定多个IP,用逗号隔开

  • 如果配置的IP没有指定子网掩码({ip}/{子网掩码}),则默认32(255.255.255.255)

源码:

public boolean test(ServerWebExchange exchange) {
   InetSocketAddress remoteAddress = config.remoteAddressResolver
         .resolve(exchange);
   if (remoteAddress != null && remoteAddress.getAddress() != null) {
      String hostAddress = remoteAddress.getAddress().getHostAddress();
      String host = exchange.getRequest().getURI().getHost();

      for (IpSubnetFilterRule source : sources) {
         if (source.matches(remoteAddress)) {
            return true;
         }
      }
   }
   return false;
}

11、WeightRoutePredicateFactory

根据权重路由,配置关键词:Weight,示例如下:

spring:
  cloud:
    gateway:
      routes:
        - id: weight-high
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, high
          predicates:
            - Weight=group1, 15
            - Path=/weight
        - id: method-low
          uri: forward:/hello
          filters:
            - AddRequestParameter=var, low
          predicates:
            - Weight=group1, 5
            - Path=/weight

根据配置的权重来路由,配置权重时需要指定分组,即Weight属性的第一个参数,为分组名,不同group单独计算权重,所以可以配置多个组。权重值为第二个参数,同一组里的权重值的总和 没有要求,最终计算会转换成小于1的double类型。

 源码:

  • WeightCalculatorWebFilter

//项目启动时执行,解析配置的权重,并将整型的权重转换成小于1的double权重,按照从小到大的顺序放入到List中。
//将最终解析的结果存储在变量groupWeights中
void addWeightConfig(WeightConfig weightConfig) {   
   String group = weightConfig.getGroup();
   GroupWeightConfig config;
   if (groupWeights.containsKey(group)) {
      config = new GroupWeightConfig(groupWeights.get(group));
   }else {
      config = new GroupWeightConfig(group);
   }
   config.weights.put(weightConfig.getRouteId(), weightConfig.getWeight());
    //将权重的int类型转换成double类型
   int weightsSum = 0;
   for (Integer weight : config.weights.values()) {
      weightsSum += weight;
   }

   final AtomicInteger index = new AtomicInteger(0);
   for (Map.Entry<String, Integer> entry : config.weights.entrySet()) {
      String routeId = entry.getKey();
      Integer weight = entry.getValue();
      Double nomalizedWeight = weight / (double) weightsSum;
      config.normalizedWeights.put(routeId, nomalizedWeight);
      config.rangeIndexes.put(index.getAndIncrement(), routeId);
   }

   config.ranges.clear()
   config.ranges.add(0.0);

   List<Double> values = new ArrayList<>(config.normalizedWeights.values());
   for (int i = 0; i < values.size(); i++) {
      Double currentWeight = values.get(i);
      Double previousRange = config.ranges.get(i);
      Double range = previousRange + currentWeight;
      config.ranges.add(range);
   }
   // only update after all calculations
   groupWeights.put(group, config);
}

//当接收到请求时执行,从变量groupWeights中读取配置,根据生成的随机值,选择routeId,
//再放入到ServerWebExchange的Attributes中,key为:{pre}.routeWeight
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
   Map<String, String> weights = getWeights(exchange);
   for (String group : groupWeights.keySet()) {
      GroupWeightConfig config = groupWeights.get(group);
      if (config == null) {
         continue; // nothing we can do, but this is odd
      }
      //生成随机变量
      double r = this.random.nextDouble();
      List<Double> ranges = config.ranges;
      //根据随机值选择RouteId
      for (int i = 0; i < ranges.size() - 1; i++) {
         if (r >= ranges.get(i) && r < ranges.get(i + 1)) {
            String routeId = config.rangeIndexes.get(i);
            weights.put(group, routeId);
            break;
         }
      }
   }
   return chain.filter(exchange);
}
  • WeightRoutePredicateFactory

//从ServerWebExchange中读取{pre}.routeWeight属性,该属性读取到的就是根据权重计算的routeId
public boolean test(ServerWebExchange exchange) {
   Map<String, String> weights = exchange.getAttributeOrDefault(WEIGHT_ATTR,
         Collections.emptyMap());
   //该值是在RoutePredicateHandlerMapping的lookupRoute方法中,遍历所有配置的路由
   //每次遍历,将值放入到GATEWAY_PREDICATE_ROUTE_ATTR属性中,有所有的RoutePredicateFactory来执行
   String routeId = exchange.getAttribute(GATEWAY_PREDICATE_ROUTE_ATTR);
   String group = config.getGroup();
   if (weights.containsKey(group)) {
      String chosenRoute = weights.get(group);
      return routeId.equals(chosenRoute);
   }
   return false;
}

12、ReadBodyPredicateFactory

  • 缓存请求体,这样可以在后续多次读取请求体

  • 该Predicate在官方文档上没有说明。

  • 参考网上资料:

    https://www.cnblogs.com/hyf-huangyongfei/p/12849406.html

最后:

  • 不同的Predicate可以组合配置,但是需要同时满足才匹配成功。

  • 如果一个请求能满足多个配置,则请求会被转发到第一个匹配的URI。

 

###更多文章请关注公众号,不定期更新笔记###

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐