背景

SpringCloud Gateway 【请求重试】
在实现 请求重试 的过程中,发现开启 SpringCloud Gateway 的服务自动路由之后,请求重试的功能就失效了,网络上大部分文章都是描述 请求重试 是如何实现的,一两篇说注释掉配置后就可以生效了,但是具体为什么则没有描述。

这里主要描述一下SpringCloud Gateway 定位路由的过程,以及为什么配置 locator.enable=true 之后配置失效的原因。

路由

局限于 Spring Cloud Gateway Server 3.1.0-SNAPSHOT

众所周知,SpringCloud Gateway是基于SpringBoot的项目,自然配置也是基于SpringBoot的自动装配,从jar包的 spring.factories,我们可以发现如下信息

  1. # Auto Configure
  2. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  3. org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration,\
  4. org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\
  5. org.springframework.cloud.gateway.config.GatewayResilience4JCircuitBreakerAutoConfiguration,\
  6. org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration,\
  7. org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration,\
  8. org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration,\
  9. org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration,\
  10. org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration,\
  11. org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration,\
  12. org.springframework.cloud.gateway.config.GatewayReactiveOAuth2AutoConfiguration
  13. org.springframework.boot.env.EnvironmentPostProcessor=\
  14. org.springframework.cloud.gateway.config.GatewayEnvironmentPostProcessor
  15. # Failure Analyzers
  16. org.springframework.boot.diagnostics.FailureAnalyzer=\
  17. org.springframework.cloud.gateway.support.MvcFoundOnClasspathFailureAnalyzer

主要关注的是

  • GatewayAutoConfiguration: 加载基本的 GlobalFilterGatewayFilter,以及 locator 等等。
  • GatewayDiscoveryClientAutoConfiguration: 如果 locator.enable=true,注入一个 DiscoveryClientRouteDefinitionLocator 用于从注册中心生成路由

经过配置的总结之后,会生成如下路由加载关系,他们的UML图如下所示 SpringCloud Gateway【 路由刷新】 - 图1

  • CompositeRouteDefinitionLocator: 基本的组合模式,开源项目常用名词
  • PropertiesRouteDefinitionLocator: 基于properties的路由,默认开启
  • InMemoryRouteDefinitionRepository: 基于内存的Route Definition,默认开启
  • DiscoveryClientRouteDefinitionLocator: 基于服务注册的路由(默认不开启,设置locator.enable=true 之后开启), 开启之后优先级最高
    • 比如注册中心存在一个 nacos-provider的实例,那么就会生成一个 nacos-providerroutepath = /nacos-provider/**
  • 基于redis,这里没有贴出来

    刷新明细

    由于老外的注册中心和配置中心不是放一起的。

从上述的类图基本上就可以明确调用方向了,RouteLocator 对外暴露获取 Route 的方法,其内部是委托给 RouteDefinitionLocator。 涉及到路由变更,那么肯定是涉及到三个 RouteDefinitionRoute 实现类的具体实现。

  • PropertiesRouteDefinitionLocator: route可以配置在Bootstrap中,也可以配置在nacos配置中心中,动态更新路由后,Gateway的路由配置便会生效。

    1. public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator {
    2. private final GatewayProperties properties;
    3. public PropertiesRouteDefinitionLocator(GatewayProperties properties) {
    4. this.properties = properties;
    5. }
    6. @Override
    7. public Flux<RouteDefinition> getRouteDefinitions() {
    8. return Flux.fromIterable(this.properties.getRoutes());
    9. }
    10. }
  • InMemoryRouteDefinitionRepository: 一般不用,不用关心 ```java public class InMemoryRouteDefinitionRepository implements RouteDefinitionRepository {

    private final Map routes = synchronizedMap(new LinkedHashMap());

    @Override public Flux getRouteDefinitions() {

    1. Map<String, RouteDefinition> routesSafeCopy = new LinkedHashMap<>(routes);
    2. return Flux.fromIterable(routesSafeCopy.values());

    }

}

  1. - DiscoveryClientRouteDefinitionLocator: 基于注册中心生成路由
  2. ```java
  3. public Flux<RouteDefinition> getRouteDefinitions() {
  4. SpelExpressionParser parser = new SpelExpressionParser();
  5. Expression includeExpr = parser.parseExpression(properties.getIncludeExpression());
  6. Expression urlExpr = parser.parseExpression(properties.getUrlExpression());
  7. Predicate<ServiceInstance> includePredicate;
  8. if (properties.getIncludeExpression() == null || "true".equalsIgnoreCase(properties.getIncludeExpression())) {
  9. includePredicate = instance -> true;
  10. }
  11. else {
  12. includePredicate = instance -> {
  13. Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class);
  14. if (include == null) {
  15. return false;
  16. }
  17. return include;
  18. };
  19. }
  20. //调用nacos获取的实例信息
  21. return serviceInstances.filter(instances -> !instances.isEmpty()).flatMap(Flux::fromIterable)
  22. .filter(includePredicate).collectMap(ServiceInstance::getServiceId)
  23. // remove duplicates
  24. .flatMapMany(map -> Flux.fromIterable(map.values())).map(instance -> {
  25. RouteDefinition routeDefinition = buildRouteDefinition(urlExpr, instance);
  26. final ServiceInstance instanceForEval = new DelegatingServiceInstance(instance, properties);
  27. //从这里就可以知道生成的RouteDefinition,只有2个简简单单的Filter
  28. //retry是没有的,所以开启了locator.enable=true之后,使用的是这里生成的route
  29. //所以,retry不生效
  30. for (PredicateDefinition original : this.properties.getPredicates()) {
  31. PredicateDefinition predicate = new PredicateDefinition();
  32. predicate.setName(original.getName());
  33. for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
  34. String value = getValueFromExpr(evalCtxt, parser, instanceForEval, entry);
  35. predicate.addArg(entry.getKey(), value);
  36. }
  37. routeDefinition.getPredicates().add(predicate);
  38. }
  39. for (FilterDefinition original : this.properties.getFilters()) {
  40. FilterDefinition filter = new FilterDefinition();
  41. filter.setName(original.getName());
  42. for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
  43. String value = getValueFromExpr(evalCtxt, parser, instanceForEval, entry);
  44. filter.addArg(entry.getKey(), value);
  45. }
  46. routeDefinition.getFilters().add(filter);
  47. }
  48. return routeDefinition;
  49. });
  50. }

使用路由

无法使用的配置一无所用,路由的使用则是在 RoutePredicateHandlerMapping中进行路由查找和定位. 这部分去掉 FluxMono 的异步操作后就是一个简单的 List 轮询遍历操作。
如果对于 FluxMono 不熟悉,可以暂时类比Flux: List, Mono: 具体的实例对象

  1. @Override
  2. protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
  3. return lookupRoute(exchange)
  4. .flatMap((Function<Route, Mono<?>>) r -> {
  5. exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
  6. exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
  7. return Mono.just(webHandler);
  8. }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
  9. exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
  10. })));
  11. }
  12. protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
  13. //委托给routeLocator, 其又委托给RouteDefinitionRoute,
  14. return this.routeLocator.getRoutes()
  15. // individually filter routes so that filterWhen error delaying is not a
  16. // problem
  17. .concatMap(route -> Mono.just(route).filterWhen(r -> {
  18. //进行predict.test判定,具体则是配置文件所描述的路由定义
  19. exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
  20. return r.getPredicate().apply(exchange);
  21. })
  22. // instead of immediately stopping main flux due to error, log and
  23. // swallow it
  24. .doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e))
  25. .onErrorResume(e -> Mono.empty()))
  26. .next()
  27. .map(route -> {
  28. return route;
  29. });
  30. }

具体的业务逻辑则是进入

  • 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()) {//这里就是看日志的地方

    1. logger.debug("Sorted gatewayFilterFactories: " + combined);

    }

    //这里就是责任链模式咯,代码罗列在下边 return new DefaultGatewayFilterChain(combined).filter(exchange); }

private static class DefaultGatewayFilterChain implements GatewayFilterChain {

  1. private final int index;
  2. private final List<GatewayFilter> filters;
  3. DefaultGatewayFilterChain(List<GatewayFilter> filters) {
  4. this.filters = filters;
  5. this.index = 0;
  6. }
  7. @Override
  8. public Mono<Void> filter(ServerWebExchange exchange) {
  9. return Mono.defer(() -> {
  10. if (this.index < filters.size()) {
  11. GatewayFilter filter = filters.get(this.index);
  12. DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);
  13. return filter.filter(exchange, chain);
  14. }
  15. else {
  16. return Mono.empty(); // complete
  17. }
  18. });
  19. }
  20. }

```

总结

请求重试 出发,经历了 看日志Debug拉源码 对路由刷新和使用进行了一个整体流程上的梳理。
同时加强了对于SpringBoot自动装配的理解,不论是开源项目,还是公司封装的二方包,只要抓住装配的细节,同时辅以UML图进行了解,基本上可以将项目的设计和使用尽收眼底。