SpringCloud Gateway 系列文章共五篇,由我行开发工程师 @Aaron 提供,带大家深入剖析 Gateway 工作原理,及如何基于 Gateway 进行定制化开发以适应企业特定环境需求。
第一篇:SpringCloud Gateway 动态路由。
第二篇:SpringCloud Gateway 路由数量对性能的影响研究。
第三篇:SpringCloud Gateway 路由转发性能优化。
第四篇:SpringCloud Gateway 断言。
第五篇:SpringCloud Gateway 过滤器。
断言即判断一个命题的真伪,路由断言则用于判断该路由是否可应用于当前请求。
路由定义
见上图,Route 各属性分别为:
id,路由的唯一编号可用于定义一个路由,gateway 中用,一些的过滤器会根据 id 是否属于某个集合做特定的操作;
order,路由的优先级,值越小,优先级越高(路由表按此排序);
metadata,可以定义连接超时、响应超时等参数,将来可能会拓展用途;
uri,上游地址,即 upstream,可以是一个确定的服务地址,也可以是一个 lb 开头的地址,表示可通过服务发现,发现上游节点;
filters,过滤器列表,请求根据断言命中一个路由后,会依次执行过滤列表的过滤器(《SpringCloud Gateway 过滤器》将详细介绍);
predicate,路由断言,本文重点,用于判断路由是否匹配当前路由。
接下来我们看 AsyncPredicate 是如何定义的:
public interface AsyncPredicate<T> extends Function<T, Publisher<Boolean>> {
//...
}
复制代码
我们对比下 jdk 中的内置 FunctionalInterface:
public interface Predicate<T> {
//...
}
public interface Function<T, R> {
//...
}
复制代码
如果你能认识到Predicate<T>
其实是一个特殊的Function<T, R>
,它的返回类型R
规定为Boolean
,你就能轻松的理解 AsyncPredicate<T>
:它是一个响应式编程模型下的Predicate<T>
,返回值为Mono<Boolean>
。
工作原理
在前文动态路由章节,我们已经在路由查找过程中,看到过断言时如何工作的,见RoutePredicateHandlerMapping 源码 126-134 行
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
// routeLocator.getRoutes() 获取的路由已经排序
return this.routeLocator.getRoutes()
// concatMap 与 flatmap的区别就是能保持执行顺序不变
.concatMap(route ->
// filterWhen 发生订阅时执行,就相当于按序遍历符合条件的记录
Mono.just(route).filterWhen(r -> {
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
// 从route中获取断言,并判断是否为真,并返回
return r.getPredicate().apply(exchange);
})
//...
}
复制代码
filterWhen
接收的是一个Function<T, Publisher<Boolean>>
的函数式接口,也就是上面我们介绍的 AsyncPredicate<T>
。
前文动态路由章节,我们介绍过Route
是有RouteDefinition
转换而来,执行转换的类为
RouteDefinitionRouteLocator,源码 168-174 行
private Route convertToRoute(RouteDefinition routeDefinition) {
// combinePredicates 根据路由定义中的断言定义组合为路由断言
AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);
List<GatewayFilter> gatewayFilters = getFilters(routeDefinition);
return Route.async(routeDefinition).asyncPredicate(predicate)
.replaceFilters(gatewayFilters).build();
}
复制代码
源码 241-286 行
// 该方法使用 and 关系 组合了所有的断言定义
private AsyncPredicate<ServerWebExchange> combinePredicates(
RouteDefinition routeDefinition) {
List<PredicateDefinition> predicates = routeDefinition.getPredicates();
if (predicates == null || predicates.isEmpty()) {
// this is a very rare case, but possible, just match all
return AsyncPredicate.from(exchange -> true);
}
AsyncPredicate<ServerWebExchange> predicate = lookup(routeDefinition,
predicates.get(0));
for (PredicateDefinition andPredicate : predicates.subList(1,
predicates.size())) {
AsyncPredicate<ServerWebExchange> found = lookup(routeDefinition,
andPredicate);
// and 表明多个断言的关系,也就是必须所有条件都符合才能命中一个路由
predicate = predicate.and(found);
}
return predicate;
}
// 根据路由定义中的断言定义,从指定 断言工厂 获取一个实例化 断言
private AsyncPredicate<ServerWebExchange> lookup(RouteDefinition route,
PredicateDefinition predicate) {
RoutePredicateFactory<Object> factory = this.predicates.get(predicate.getName());
// 省略非关键代码
return factory.applyAsync(config);
}
复制代码
断言工厂
上文我们已经解析清楚,一个请求是否命中某个路由,最终是由路由定义
中的断言定义
对应的断言工厂的applyAsync
方法决定。
见上图,RoutePredicateFactory
是一个FunctionalInterface
其唯一抽象方法为:
Predicate<ServerWebExchange> apply(C config);
复制代码
具体的RoutePredicateFactory
的实现工厂类,需实现上述方法。
Gateway 内置的路由工厂有:
以上每个工厂的apply
都实现了如何根据配置及ServerWebExchange
来判断当前请求是否匹配断言(如果需要自定义断言工厂,也需要实现此逻辑)。
每个路由工厂如何配置,官方文档均有示例。
我们看如下示例:
spring:
cloud:
gateway:
routes:
- id: path_route
uri: https://example.org
predicates:
- Path=/red/**,/blue/**
复制代码
关键字 Path
表示采用 PathRoutePredicateFactory
工厂创建该断言;
Path
后是逗号分隔的有序字符串数据,表示只要匹配任意一个 path,该断言即为真(见apply
方法),路由命中。
下面方法可用于创建动态路由时,生成断言定义:
/**
* 生成断言定义
*
* @param type 断言名称
* @param orderedArgs 有序的参数数组
* @return 路由断言定义
*/
public static PredicateDefinition createPredicate(@NotNull PredicateType type,
@NotNull String... orderedArgs) {
PredicateDefinition definition = new PredicateDefinition();
definition.setName(type.name());
int order = 0;
for (String arg : orderedArgs) {
if (StringUtils.isNotBlank(arg)) {
definition.addArg(NameUtils.generateName(order++), arg);
}
}
return definition;
}
复制代码
断言定义的 Name 即为断言工厂的前缀字符串。
总结
本文较为详细的分析了 Gateway 是如何判断一个请求应采用哪个路由进行处理的原理,并进一步分析了断言工厂的实现,以及配置文件及断言定义如何与断言工厂相对应。
希望通过本文,你能帮助你加深对 Gateway 的理解。
评论