背景
SpringCloud Gateway 【请求重试】
在实现 请求重试
的过程中,发现开启 SpringCloud Gateway
的服务自动路由之后,请求重试的功能就失效了,网络上大部分文章都是描述 请求重试
是如何实现的,一两篇说注释掉配置后就可以生效了,但是具体为什么则没有描述。
这里主要描述一下SpringCloud Gateway 定位路由的过程,以及为什么配置 locator.enable=true
之后配置失效的原因。
路由
局限于 Spring Cloud Gateway Server 3.1.0-SNAPSHOT
众所周知,SpringCloud Gateway是基于SpringBoot的项目,自然配置也是基于SpringBoot的自动装配,从jar包的 spring.factories
,我们可以发现如下信息
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayResilience4JCircuitBreakerAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration,\
org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration,\
org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayReactiveOAuth2AutoConfiguration
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.gateway.config.GatewayEnvironmentPostProcessor
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.cloud.gateway.support.MvcFoundOnClasspathFailureAnalyzer
主要关注的是
- GatewayAutoConfiguration: 加载基本的
GlobalFilter
和GatewayFilter
,以及locator
等等。 - GatewayDiscoveryClientAutoConfiguration: 如果
locator.enable=true
,注入一个DiscoveryClientRouteDefinitionLocator
用于从注册中心生成路由
经过配置的总结之后,会生成如下路由加载关系,他们的UML图如下所示
- CompositeRouteDefinitionLocator: 基本的组合模式,开源项目常用名词
- PropertiesRouteDefinitionLocator: 基于properties的路由,默认开启
- InMemoryRouteDefinitionRepository: 基于内存的Route Definition,默认开启
- DiscoveryClientRouteDefinitionLocator: 基于服务注册的路由(默认不开启,设置
locator.enable=true
之后开启), 开启之后优先级最高- 比如注册中心存在一个
nacos-provider
的实例,那么就会生成一个nacos-provider
的route
,path = /nacos-provider/**
- 比如注册中心存在一个
- 基于redis,这里没有贴出来
刷新明细
由于老外的注册中心和配置中心不是放一起的。
从上述的类图基本上就可以明确调用方向了,RouteLocator
对外暴露获取 Route
的方法,其内部是委托给 RouteDefinitionLocator
。 涉及到路由变更,那么肯定是涉及到三个 RouteDefinitionRoute
实现类的具体实现。
PropertiesRouteDefinitionLocator: route可以配置在Bootstrap中,也可以配置在nacos配置中心中,动态更新路由后,Gateway的路由配置便会生效。
public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator {
private final GatewayProperties properties;
public PropertiesRouteDefinitionLocator(GatewayProperties properties) {
this.properties = properties;
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(this.properties.getRoutes());
}
}
InMemoryRouteDefinitionRepository: 一般不用,不用关心 ```java public class InMemoryRouteDefinitionRepository implements RouteDefinitionRepository {
private final Map
routes = synchronizedMap(new LinkedHashMap ()); @Override public Flux
getRouteDefinitions() { Map<String, RouteDefinition> routesSafeCopy = new LinkedHashMap<>(routes);
return Flux.fromIterable(routesSafeCopy.values());
}
}
- DiscoveryClientRouteDefinitionLocator: 基于注册中心生成路由
```java
public Flux<RouteDefinition> getRouteDefinitions() {
SpelExpressionParser parser = new SpelExpressionParser();
Expression includeExpr = parser.parseExpression(properties.getIncludeExpression());
Expression urlExpr = parser.parseExpression(properties.getUrlExpression());
Predicate<ServiceInstance> includePredicate;
if (properties.getIncludeExpression() == null || "true".equalsIgnoreCase(properties.getIncludeExpression())) {
includePredicate = instance -> true;
}
else {
includePredicate = instance -> {
Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class);
if (include == null) {
return false;
}
return include;
};
}
//调用nacos获取的实例信息
return serviceInstances.filter(instances -> !instances.isEmpty()).flatMap(Flux::fromIterable)
.filter(includePredicate).collectMap(ServiceInstance::getServiceId)
// remove duplicates
.flatMapMany(map -> Flux.fromIterable(map.values())).map(instance -> {
RouteDefinition routeDefinition = buildRouteDefinition(urlExpr, instance);
final ServiceInstance instanceForEval = new DelegatingServiceInstance(instance, properties);
//从这里就可以知道生成的RouteDefinition,只有2个简简单单的Filter
//retry是没有的,所以开启了locator.enable=true之后,使用的是这里生成的route
//所以,retry不生效
for (PredicateDefinition original : this.properties.getPredicates()) {
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName(original.getName());
for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
String value = getValueFromExpr(evalCtxt, parser, instanceForEval, entry);
predicate.addArg(entry.getKey(), value);
}
routeDefinition.getPredicates().add(predicate);
}
for (FilterDefinition original : this.properties.getFilters()) {
FilterDefinition filter = new FilterDefinition();
filter.setName(original.getName());
for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
String value = getValueFromExpr(evalCtxt, parser, instanceForEval, entry);
filter.addArg(entry.getKey(), value);
}
routeDefinition.getFilters().add(filter);
}
return routeDefinition;
});
}
使用路由
无法使用的配置一无所用,路由的使用则是在 RoutePredicateHandlerMapping
中进行路由查找和定位. 这部分去掉 Flux
和 Mono
的异步操作后就是一个简单的 List
轮询遍历操作。
如果对于 Flux
和 Mono
不熟悉,可以暂时类比Flux: List
, Mono: 具体的实例对象
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
return lookupRoute(exchange)
.flatMap((Function<Route, Mono<?>>) r -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
return Mono.just(webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
})));
}
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
//委托给routeLocator, 其又委托给RouteDefinitionRoute,
return this.routeLocator.getRoutes()
// individually filter routes so that filterWhen error delaying is not a
// problem
.concatMap(route -> Mono.just(route).filterWhen(r -> {
//进行predict.test判定,具体则是配置文件所描述的路由定义
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return r.getPredicate().apply(exchange);
})
// instead of immediately stopping main flux due to error, log and
// swallow it
.doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e))
.onErrorResume(e -> Mono.empty()))
.next()
.map(route -> {
return route;
});
}
具体的业务逻辑则是进入
- HttpWebHandlerAdapter#handle: 委托给FilteringWebHandler执行
FilteringWebHandler#handle ```java @Override public Mono
handle(ServerWebExchange exchange) { Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR); List gatewayFilters = route.getFilters(); List
combined = new ArrayList<>(this.globalFilters); combined.addAll(gatewayFilters); // TODO: needed or cached? AnnotationAwareOrderComparator.sort(combined); if (logger.isDebugEnabled()) {//这里就是看日志的地方
logger.debug("Sorted gatewayFilterFactories: " + combined);
}
//这里就是责任链模式咯,代码罗列在下边 return new DefaultGatewayFilterChain(combined).filter(exchange); }
private static class DefaultGatewayFilterChain implements GatewayFilterChain {
private final int index;
private final List<GatewayFilter> filters;
DefaultGatewayFilterChain(List<GatewayFilter> filters) {
this.filters = filters;
this.index = 0;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < filters.size()) {
GatewayFilter filter = filters.get(this.index);
DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);
return filter.filter(exchange, chain);
}
else {
return Mono.empty(); // complete
}
});
}
}
总结
从 请求重试
出发,经历了 看日志
、Debug
、拉源码
对路由刷新和使用进行了一个整体流程上的梳理。
同时加强了对于SpringBoot自动装配的理解,不论是开源项目,还是公司封装的二方包,只要抓住装配的细节,同时辅以UML图进行了解,基本上可以将项目的设计和使用尽收眼底。