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 #表示1到18都可以访问
复制代码
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。加油!