Spring Cloud Gateway源码解析-06-内置Predicate

SCG的Predicate是使用工厂方法模式来实现的,类关系如下。
在这里插入图片描述
SCG包括了很多内置的Predicate工厂,如下
在这里插入图片描述
在每个RoutePredicateFactory中都有一个Config类,该类用于存储对应RoutePredicate的配置

AfterRoutePredicateFactory

匹配请求时间满足在配置时间之后的请求。

public class AfterRoutePredicateFactory
		extends AbstractRoutePredicateFactory<AfterRoutePredicateFactory.Config> {
  @Override
	public Predicate<ServerWebExchange> apply(Config config) {
		return new GatewayPredicate() {
			@Override
			public boolean test(ServerWebExchange serverWebExchange) {
				final ZonedDateTime now = ZonedDateTime.now();
				return now.isAfter(config.getDatetime());
			}

			@Override
			public String toString() {
				return String.format("After: %s", config.getDatetime());
			}
		};
	}
}
复制代码

代码很简单,获取到我们配置的时间,判断当前时间是否大于配置的时间。

Test

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - After=2021-03-26T16:05:22.631+08:00[Asia/Shanghai]
复制代码

当前时间16:11,小于我们配置的时间,当进行断言的时候,返回了true,表示符合条件。
在这里插入图片描述
当配置日期为27号时,则不符合条件,返回false,不匹配该路由。

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - After=2021-03-27T16:05:22.631+08:00[Asia/Shanghai]
复制代码

在这里插入图片描述

BeforeRoutePredicateFactory

匹配请求时间满足在配置时间之前的请求。与AfterRoutePredicateFactory类似,不做过多阐述。

配置

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - Before=2021-03-27T16:05:22.631+08:00[Asia/Shanghai]
复制代码

BetweenRoutePredicateFactory

匹配请求时间满足在配置时间之间的请求。与上边两种类似,不做过多阐述

配置

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - Between=2021-03-26T16:05:22.631+08:00[Asia/Shanghai],2021-03-27T16:05:22.631+08:00[Asia/Shanghai]
复制代码

CookieRoutePredicateFactory

匹配带有指定cookie的请求,key需要相等,value满足配置的正则表达式.

public class CookieRoutePredicateFactory
		extends AbstractRoutePredicateFactory<CookieRoutePredicateFactory.Config> {

	/**
	 * Name key.
	 */
	public static final String NAME_KEY = "name";

	/**
	 * Regexp key.
	 */
	public static final String REGEXP_KEY = "regexp";

	@Override
	public Predicate<ServerWebExchange> apply(Config config) {
		return new GatewayPredicate() {
			@Override
			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;
			}
		};
	}
}
复制代码

配置

CookieRoutePredicateFactory需要配置两个参数,name和value,通过逗号分隔,value支持正则表达式。

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - Cookie=502819cookie,502819.*
复制代码

在这里插入图片描述
在这里插入图片描述

HeaderRoutePredicateFactory

匹配带有指定header的请求,key需要相等,value满足配置的正则表达式.与CookieRoutePredicateFactory类似,不过多阐述。

配置

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - Header=502819header,502819.*
复制代码

HostRoutePredicateFactory

匹配请求Host复合配置的正则表达式。可以配置多个

public class HostRoutePredicateFactory
	extends AbstractRoutePredicateFactory<HostRoutePredicateFactory.Config> {

    .............省略部分代码............

    @Override
	public Predicate<ServerWebExchange> apply(Config config) {
		return new GatewayPredicate() {
			@Override
			public boolean test(ServerWebExchange exchange) {
				String host = exchange.getRequest().getHeaders().getFirst("Host");
				Optional<String> optionalPattern = config.getPatterns().stream()
						.filter(pattern -> pathMatcher.match(pattern, host)).findFirst();

				if (optionalPattern.isPresent()) {
					Map<String, String> variables = pathMatcher
							.extractUriTemplateVariables(optionalPattern.get(), host);
					ServerWebExchangeUtils.putUriTemplateVariables(exchange, variables);
					return true;
				}

				return false;
			}
		};
  .............省略部分代码............
	}
public static class Config {
		//因为是List,所以可以配置多个
		private List<String> patterns = new ArrayList<>();
.............省略部分代码............
	}

}
复制代码

Test

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - Host=*.spring.io,*.502819lhj.*

复制代码

在这里插入图片描述
在这里插入图片描述

MethodRoutePredicateFactory

匹配指定HTTP Method的请求。

public enum HttpMethod {

	GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
}
复制代码

Test

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - Method=POST,DELETE
复制代码

在这里插入图片描述
当时用GET请求时,断言失败。
在这里插入图片描述

PathRoutePredicateFactory

匹配复合配置Path的请求。

PathRoutePredicateFactory可以设置模板变量,模板变量会被放入ServerWebExchange的attributes中,可以供GatewayFilter使用。

public class PathRoutePredicateFactory
		extends AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {
      @Override
	public Predicate<ServerWebExchange> apply(Config config) {
		..........省略部分代码.........
		return new GatewayPredicate() {
			@Override
			public boolean test(ServerWebExchange exchange) {
				PathContainer path = parsePath(
						exchange.getRequest().getURI().getRawPath());

				Optional<PathPattern> optionalPathPattern = pathPatterns.stream()
						.filter(pattern -> pattern.matches(path)).findFirst();

				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;
				}
			}
		};
	}
}
class ServerWebExchangeUtils {
public static void putUriTemplateVariables(ServerWebExchange exchange,
			Map<String, String> uriVariables) {
		if (exchange.getAttributes().containsKey(URI_TEMPLATE_VARIABLES_ATTRIBUTE)) {
			Map<String, Object> existingVariables = (Map<String, Object>) exchange
					.getAttributes().get(URI_TEMPLATE_VARIABLES_ATTRIBUTE);
			HashMap<String, Object> newVariables = new HashMap<>();
			newVariables.putAll(existingVariables);
			newVariables.putAll(uriVariables);
			//放入ServerWebExchange中
			exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, newVariables);
		}
		else {
			exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables);
		}
	}
}
复制代码

配置

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - Path=/api/{hello}
        - Path=/api/hello
复制代码

QueryRoutePredicateFactory

匹配带有指定请求参数的请求。

可以只配置需要匹配的请求参数名称,也可以同时配置需要匹配的请求参数名称和请求参数值,请求参数值支持正则表达式。

public class QueryRoutePredicateFactory
		extends AbstractRoutePredicateFactory<QueryRoutePredicateFactory.Config> {
  .......省略部分代码.......
  @Override
	public Predicate<ServerWebExchange> apply(Config config) {
		return new GatewayPredicate() {
			@Override
			public boolean test(ServerWebExchange exchange) {
				if (!StringUtils.hasText(config.regexp)) {
					//如果参数值正则为空则只匹配参数名
					// check existence of header
					return exchange.getRequest().getQueryParams()
							.containsKey(config.param);
				}
				
				List<String> values = exchange.getRequest().getQueryParams()
						.get(config.param);
				if (values == null) {
					return false;
				}
				for (String value : values) {
					//判断参数值是否match配置的正则
					if (value != null && value.matches(config.regexp)) {
						return true;
					}
				}
				return false;
			}
		};
	}
}
复制代码

Test

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - Query=name,502819.*
复制代码

在这里插入图片描述

ReadBodyRoutePredicateFactory

用来检查请求体的内容的断言,属于测试版本,未来可能会更改,不做过多的阐述。

RemoteAddrRoutePredicateFactory

匹配请求客户端的IP符合指定的IP地址。可以配置IP段也可以通过逗号配置多个。
源代码没什么难度,就不多说了。

Test

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - RemoteAddr=169.254.183.16,192.168.5.14
        - RemoteAddr=169.254.183.1/18 #表示118都可以访问

复制代码

WeightRoutePredicateFactory

WeightRoutePredicateFactory可以配置两个参数,group和weight

配置

spring:
  cloud:
    gateway:
      routes:
      - id: hello_route
        uri: http://localhost:8088/api/hello
        predicates:
        - Weight=group1,2
      - id: hello_route2
        uri: http://localhost:8089/api/hello
        predicates:
        - Weight=group1,8
复制代码

WeightRoutePredicateFactory会对同一组内的路由进行权重计算,根据配置的权重进行访问。
上边的配置下,访问 http://localhost:8088/api/hello的概率为20%,访问http://localhost:8089/api/hello的概率为80%。

解析

WeightRoutePredicateFactory的设计较为复杂且有趣。

public class WeightRoutePredicateFactory
		extends AbstractRoutePredicateFactory<WeightConfig>
		implements ApplicationEventPublisherAware {
      @Override
	public Predicate<ServerWebExchange> apply(WeightConfig config) {
		return new GatewayPredicate() {
			@Override
			public boolean test(ServerWebExchange exchange) {
        //获取到所有的权重信息,key:group ,value:路由ID
        //WEIGHT_ATTR由WeightCalculatorWebFilter放入
				Map<String, String> weights = exchange.getAttributeOrDefault(WEIGHT_ATTR,
						Collections.emptyMap());
				//获取到当前遍历的路由ID
				String routeId = exchange.getAttribute(GATEWAY_PREDICATE_ROUTE_ATTR);

				// all calculations and comparison against random num happened in
				// WeightCalculatorWebFilter
				//获取到当前路由的group
				String group = config.getGroup();
				//判定权重信息中是否包含当前路由的group
				if (weights.containsKey(group)) {
					//根据group获取权重信息中的路由ID
					String chosenRoute = weights.get(group);
					if (log.isTraceEnabled()) {
						log.trace("in group weight: " + group + ", current route: "
								+ routeId + ", chosen route: " + chosenRoute);
					}
					//判断当前路由的ID与权重信息中的路由ID是否相等,如果不相等,不匹配当前路由
					//到这里其实能够看出来,weights中就是当前请求应该请求的route
					return routeId.equals(chosenRoute);
				}
				else if (log.isTraceEnabled()) {
					log.trace("no weights found for group: " + group + ", current route: "
							+ routeId);
				}

				return false;
			}
		};
	}
}
复制代码

接下来解析第10行的weights是怎么来的,SCG定义了一个WebFilter(WeightCalculatorWebFilter),会先对请求进行处理。
RouteDefinitionRouteLocator#lookup方法中,会在生成配置信息的时候发布一个PredicateArgsEvent事件。

@SuppressWarnings("unchecked")
	private AsyncPredicate<ServerWebExchange> lookup(RouteDefinition route,
													 PredicateDefinition predicate) {
	...........省略部分代码...............
		//每个RoutePredicateFactory实现中都有Config,可以理解为我们配置的参数规则,生成此Config
		// @formatter:off
		Object config = this.configurationService.with(factory)
				.name(predicate.getName())
				.properties(predicate.getArgs())
				//发布事件
				.eventFunction((bound, properties) -> new PredicateArgsEvent(
						RouteDefinitionRouteLocator.this, route.getId(), properties))
				.bind();
		// @formatter:on
		//生成异步断言
		return factory.applyAsync(config);
	}
复制代码

WeightCalculatorWebFilter中监听了这个事件。

WeightCalculatorWebFilter

@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof PredicateArgsEvent) {
			//监听到RouteDefinitionRouteLocator发布的事件
			handle((PredicateArgsEvent) event);
		}
    ...........省略部分代码...............

	}
复制代码

在handle方法中会获取到发布事件的路由的ID,并且通过configurationService获取到路由的配置信息。

public void handle(PredicateArgsEvent event) {
		Map<String, Object> args = event.getArgs();

		if (args.isEmpty() || !hasRelevantKey(args)) {
			return;
		}

		WeightConfig config = new WeightConfig(event.getRouteId());
		//获取到当前路由的配置信息
		this.configurationService.with(config).name(WeightConfig.CONFIG_PREFIX)
				.normalizedProperties(args).bind();

		addWeightConfig(config);
	}
复制代码
void addWeightConfig(WeightConfig weightConfig) {
		//获取当前路由的group
		String group = weightConfig.getGroup();
		GroupWeightConfig config;
		// only create new GroupWeightConfig rather than modify
		// and put at end of calculations. This avoids concurency problems
		// later during filter execution.
		//判断groupWeights是否包含了已经包含了同group的权重配置,此处的groupWeights是所有的路由权重信息
		if (groupWeights.containsKey(group)) {
			//如果有也创建一个信息,并将前边的该group下路由权重信息初始化进去
			config = new GroupWeightConfig(groupWeights.get(group));
		}
		else {
			config = new GroupWeightConfig(group);
		}
		//添加当前路由+路由权重
		config.weights.put(weightConfig.getRouteId(), weightConfig.getWeight());

		// recalculate

		// normalize weights
		int weightsSum = 0;
		//计算配置的所有的路由权重和
		//假设,我们配置rout1、route2、route3三个路由的权重分别为2,7,1,那么weightSum计算后为10
		for (Integer weight : config.weights.values()) {
			weightsSum += weight;
		}

		final AtomicInteger index = new AtomicInteger(0);
		//遍历
		for (Map.Entry<String, Integer> entry : config.weights.entrySet()) {
			//获取到路由ID
			String routeId = entry.getKey();
			//获取到路由的权重
			Integer weight = entry.getValue();
			//计算出当前路由的权重占比
			//rout1:0.2,route2:0.7,route3:0.1
			Double nomalizedWeight = weight / (double) weightsSum;
			//放入normalizedWeights
			config.normalizedWeights.put(routeId, nomalizedWeight);

			// recalculate rangeIndexes
			config.rangeIndexes.put(index.getAndIncrement(), routeId);
		}

		// TODO: calculate ranges
		config.ranges.clear();
		//放入0号位置数0.0
		config.ranges.add(0.0);
		/**
		 * normalizedWeights:rout1:0.2,route2:0.7,route3:0.1
		 */
		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);
		}
		//ranges :大约为 0.0, 0.2, 0.9, 1.0
		//相邻两个index之间代表的是一个路由的范围,
		//如rout1:0.2,route2:0.7,route3:0.1  那ranges的元素为0.0, 0.2, 0.9, 1.0
		//0.0到0.2则表示route1的权重范围,0.2到0.9表示的route2的权重范围以此类推
		if (log.isTraceEnabled()) {
			log.trace("Recalculated group weight config " + config);
		}
		// only update after all calculations
		//添加权重分组,key:分组 value:分组下的所有路由的权重信息
		groupWeights.put(group, config);
	}


static class GroupWeightConfig {

		String group;
		//key:路由ID  value:权重
		LinkedHashMap<String, Integer> weights = new LinkedHashMap<>();
		//路由的权重占比
		LinkedHashMap<String, Double> normalizedWeights = new LinkedHashMap<>();

		LinkedHashMap<Integer, String> rangeIndexes = new LinkedHashMap<>();
		//相邻两个index之间代表的是一个路由的范围,
		//如rout1:0.2,route2:0.7,route3:0.1  那ranges的元素为0.0, 0.2, 0.9, 1.0
		//0.0到0.2则表示route1的权重范围,0.2到0.9表示的route2的权重范围以此类推
		List<Double> ranges = new ArrayList<>();

		GroupWeightConfig(String group) {
			this.group = group;
		}

		GroupWeightConfig(GroupWeightConfig other) {
			this.group = other.group;
			this.weights = new LinkedHashMap<>(other.weights);
			this.normalizedWeights = new LinkedHashMap<>(other.normalizedWeights);
			this.rangeIndexes = new LinkedHashMap<>(other.rangeIndexes);
		}
}
复制代码

filter方法

此方法会被WebFilterChain调用。用来为当前请求添加group及路由,方便WeightRoutePredicateFactory获取及匹配。

@Override
	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) {
				if (log.isDebugEnabled()) {
					log.debug("No GroupWeightConfig found for group: " + group);
				}
				continue; // nothing we can do, but this is odd
			}
			//生成随机数
			double r = this.random.nextDouble();
			//获取到当前分组的所有路由的权重范围
			List<Double> ranges = config.ranges;

			if (log.isTraceEnabled()) {
				log.trace("Weight for group: " + group + ", ranges: " + ranges + ", r: "
						+ r);
			}

			for (int i = 0; i < ranges.size() - 1; i++) {
				//如果生成的随机数大于等于当前的元素,并且小于下一元素,说明属于当前路由,则获取到路由ID放入weights中返回
				
        //WeightRoutePredicateFactory只需要判断weights中是否有当前路由的group,
        //如果有,则进一步判断当前路由ID是否为这里计算出来的路由id即可
        if (r >= ranges.get(i) && r < ranges.get(i + 1)) {
					String routeId = config.rangeIndexes.get(i);
					weights.put(group, routeId);
					break;
				}
			}
		}

		if (log.isTraceEnabled()) {
			log.trace("Weights attr: " + weights);
		}

		return chain.filter(exchange);
	}
复制代码

总结

目前为止,2.2.6.RELEASE版本的SCG内置的RoutePredicateFactory我们就解析完了,基本上能够满足我们平时的业务场景,也可以根据需要进行自定义。后边会解析GatewayFilterFactory。加油!

猜你喜欢

转载自juejin.im/post/7033258834017648677