深入理解SpringCloud之Gateway

  • 2019 年 10 月 14 日
  • 筆記

虽然在服务网关有了zuul(在这里是zuul1),其本身还是基于servlet实现的,换言之还是同步阻塞方式的实现。就其本身来讲它的最根本弊端也是再此。而非阻塞带来的好处不言而喻,高效利用线程资源进而提高吞吐量,基于此Spring率先拿出针对于web的杀手锏,对,就是webflux。而Gateway本身就是基于webflux基础之上实现的。毕竟spring推出的技术,当然要得以推广嘛。不过就国内的软件公司而言为了稳定而选择保守,因此就这项技术的广度来说我本身还是在观望中。

1. Gateway快速上手

添加依赖:

    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'

这里请注意,springcloud-gateway是基于netty运行的环境,在servlet容器环境或者把它构建为war包运行的话是不允许的,因此在项目当中没有必要添加spring-boot-starter-web。在gateway当中有三个重要的元素他们分别是:

  • Route 是最核心的路由元素,它定义了ID,目标URI ,predicates的集合与filter的集合,如果Predicate聚合返回真,则匹配该路由
  • Predicate 基于java8的函数接口Predicate,其输入参数类型ServerWebExchange,其作用就是允许开发人员根据当前的http请求进行规则的匹配,比如说http请求头,请求时间等,匹配的结果将决定执行哪种路由
  • Filter为GatewayFilter,它是由特殊的工厂构建,通过Filter可以在下层请求路由前后改变http请求与响应

我们编辑application.yaml,定义如下配置:

    spring:        application:          name: gateway        cloud:          gateway:            routes:              - id: before_route                uri: http://www.baidu.com                predicates:                  - Path=/baidu      server:        port: 8088

此时当我们访问路径中包含/baidu的,gateway将会帮我们转发至百度页面

2. 工作流程

在这里我贴上官网的一张图:

在这里我想结合源代码来说明其流程,这里面有个关键的类,叫RoutePredicateHandlerMapping,我们可以发现这个类有如下特点:

    public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {          // ....省略部分代码        @Override          protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {              // don't handle requests on management port if set and different than server port              if (this.managementPortType == DIFFERENT && this.managementPort != null                      && exchange.getRequest().getURI().getPort() == this.managementPort) {                  return Mono.empty();              }              exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());                return lookupRoute(exchange)                      // .log("route-predicate-handler-mapping", Level.FINER) //name this                      .flatMap((Function<Route, Mono<?>>) r -> {                          exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);                          if (logger.isDebugEnabled()) {                              logger.debug(                                      "Mapping [" + getExchangeDesc(exchange) + "] to " + r);                          }                            exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);                          return Mono.just(webHandler);                      }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {                          exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);                          if (logger.isTraceEnabled()) {                              logger.trace("No RouteDefinition found for ["                                      + getExchangeDesc(exchange) + "]");                          }                      })));          }          //...省略部分代码        }
  • 此类继承了AbstractHandlerMapping,注意这里的是reactive包下的,也就是webflux提供的handlermapping,其作用等同于webmvc的handlermapping,其作用是将请求映射找到对应的handler来处理。
  • 在这里处理的关键就是先寻找合适的route,关键的方法为lookupRoute():
       protected Mono<Route> lookupRoute(ServerWebExchange exchange) {              return this.routeLocator.getRoutes()                      // individually filter routes so that filterWhen error delaying is not a                      // problem                      .concatMap(route -> Mono.just(route).filterWhen(r -> {                          // add the current route we are testing                          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()))                      // .defaultIfEmpty() put a static Route not found                      // or .switchIfEmpty()                      // .switchIfEmpty(Mono.<Route>empty().log("noroute"))                      .next()                      // TODO: error handling                      .map(route -> {                          if (logger.isDebugEnabled()) {                              logger.debug("Route matched: " + route.getId());                          }                          validateRoute(route, exchange);                          return route;                      });                /*               * TODO: trace logging if (logger.isTraceEnabled()) {               * logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }               */          }
  • 其中RouteLocator的接口作用是获取Route定义,那么在GatewayAutoConfiguaration里有相关的配置,大家可自行查阅:
          @Bean              public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,                      List<GatewayFilterFactory> GatewayFilters,                      List<RoutePredicateFactory> predicates,                      RouteDefinitionLocator routeDefinitionLocator,                      @Qualifier("webFluxConversionService") ConversionService conversionService) {                  return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates,                          GatewayFilters, properties, conversionService);              }
  • 然后在注释add the current route we are testing处可以得到一个结论,其是根据Predicate的声明条件过滤出合适的Route
  • 最终拿到FilteringWebHandler作为它的返回值,这个类是真正意义上处理请求的类,它实现了webflux提供的WebHandler接口:
    public class FilteringWebHandler implements WebHandler {

      //.....省略其它代码      @Override    public Mono<Void> handle(ServerWebExchange exchange) {      //拿到当前的route        Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);      //获取所有的gatewayFilter        List<GatewayFilter> gatewayFilters = route.getFilters();        //获取全局过滤器        List<GatewayFilter> 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);    }      //....省略其它代码  }

    在这里可以看到它的实际处理方式是委派给过滤器链进行处理请求操作的

3. Predicate

Spring Cloud Gateway包含许多内置的Predicate Factory。所有的Predicate都匹配HTTP请求的不同属性。如果配置类多个Predicate, 那么必须满足所有的predicate才可以,官网上列举的内置的Predicate,我在这里不做过多的说明,请大家参考:地址,predicate的实现可以在org.springframework.cloud.gateway.handler.predicate的包下找到。

3.1、自定义Predicate

先改一下application.yaml中的配置:

spring:    application:      name: gateway    cloud:      gateway:        routes:          - id: before_route            uri: http://www.baidu.com            predicates:              - Number=1

默认命名规则:名称RoutePredicateFactory,在这里我们可以看到如下代码规则用以解析Predicate的名称,该代码在NameUtils当中:

        public static String normalizeRoutePredicateName(                  Class<? extends RoutePredicateFactory> clazz) {              return removeGarbage(clazz.getSimpleName()                      .replace(RoutePredicateFactory.class.getSimpleName(), ""));          }

那么在这里我们就按照如上规则建立对应的NumberRoutePredicateFactory,代码如下:

    @Component      public class NumberRoutePredicateFactory extends AbstractRoutePredicateFactory<NumberRoutePredicateFactory.Config> {              public NumberRoutePredicateFactory() {              super(Config.class);          }            @Override          public List<String> shortcutFieldOrder() {              return Arrays.asList("number");          }            @Override          public ShortcutType shortcutType() {              return ShortcutType.GATHER_LIST;          }            @Override          public Predicate<ServerWebExchange> apply(Config config) {              return new GatewayPredicate() {                  @Override                  public boolean test(ServerWebExchange serverWebExchange) {                      String number = serverWebExchange.getRequest().getQueryParams().getFirst("number");                      return config.number == Integer.parseInt(number);                  }              };          }              public static class Config {              private int number;                public int getNumber() {                  return number;              }                public void setNumber(int number) {                  this.number = number;              }          }      }
  • 该类可以继承AbstractRoutePredicateFactory,同时需要注册为spring的Bean
  • 在此类当中按照规范来讲,需要定义一个内部类,该类的作用用于封装application.yaml中的配置,Number=1这个配置会按照规则进行封装,这个规则由以下几项决定:
    • ShortcutType,该值是枚举类型,分别是
      • DEFAULT :按照shortcutFieldOrder顺序依次赋值
      • GATHER_LIST:shortcutFiledOrder只能有一个值,如果参数有多个拼成一个集合
      • GATHER_LIST_TAIL_FLAG:shortcutFiledOrder只能有两个值,其中最后一个值为true或者false,其余的值变成一个集合付给第一个值
    • shortcutFieldOrder,这个值决定了Config中配置的属性,配置的参数都会被封装到该属性当中

4. Filter

Gateway中的filter可以分为(GlobalFilter)全局过滤器与普通过滤器,过滤器可以在路由到代理服务的前后改变请求与响应。在这里我会列举两个常见的filter给大家用作参考:

4.1、负载均衡的实现

与zuul类似,Gateway也可以作为服务端的负载均衡,那么负载均衡的处理关键就是与Ribbon集成,那么Gateway是利用GlobalFilter进行实现的,它的实现类是LoadBalancerClientFilter:

    public class LoadBalancerClientFilter implements GlobalFilter, Ordered {          protected final LoadBalancerClient loadBalancer;            private LoadBalancerProperties properties;            //....          @Override          @SuppressWarnings("Duplicates")          public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {                // preserve the original url              addOriginalRequestUrl(exchange, url);                log.trace("LoadBalancerClientFilter url before: " + url);            //选择一个服务实例              final ServiceInstance instance = choose(exchange);                if (instance == null) {                  throw NotFoundException.create(properties.isUse404(),                          "Unable to find instance for " + url.getHost());              }                URI uri = exchange.getRequest().getURI();                // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,              // if the loadbalancer doesn't provide one.          //判断协议类型              String overrideScheme = instance.isSecure() ? "https" : "http";              if (schemePrefix != null) {                  overrideScheme = url.getScheme();              }              //重构uri地址              URI requestUrl = loadBalancer.reconstructURI(                      new DelegatingServiceInstance(instance, overrideScheme), uri);                //...              return chain.filter(exchange);          }      }

在这里我们可以看到这里它是基于Spring-Cloud-Commons规范里的LoadBalanceClient包装实现的。

4.2、集成Hystrix

Gateway同样也可以和Hystrix进行集成,这里面的关键类是HystrixGatewayFilterFactory,这里面的关键是RouteHystrixCommand该类继承了HystrixObservableCommand:

    @Override              protected Observable<Void> construct() {            // 执行过滤器链                  return RxReactiveStreams.toObservable(this.chain.filter(exchange));//1              }                @Override              protected Observable<Void> resumeWithFallback() {                  if (this.fallbackUri == null) {                      return super.resumeWithFallback();                  }                    // TODO: copied from RouteToRequestUrlFilter                  URI uri = exchange.getRequest().getURI();                  // TODO: assume always?                  boolean encoded = containsEncodedParts(uri);                  URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null)                          .uri(this.fallbackUri).scheme(null).build(encoded).toUri();//2                  exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);                  addExceptionDetails();                    ServerHttpRequest request = this.exchange.getRequest().mutate()                          .uri(requestUrl).build();                  ServerWebExchange mutated = exchange.mutate().request(request).build();                  return RxReactiveStreams.toObservable(getDispatcherHandler().handle(mutated));//3              }
  • 在代码1处会执行滤器链,写到此处的代码会被统一加上hystrix的保护
  • 在代码2处再是执行回退的方法,根据fallbackUri构建一个回退请求地址
  • 在代码3处获取WebFlux的总控制器DispatcherHandler进行回退地址的处理

5、服务发现

服务发现对于Gateway来说也是个非常重要的内容,Gateway在这里定义了一个核心接口叫做:RouteDefinitionLocator,这个接口用于获取Route的定义,服务发现的机制实现了该接口:

    public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLocator {          @Override          public Flux<RouteDefinition> getRouteDefinitions() {            //....省略部分代码                return Flux.fromIterable(discoveryClient.getServices())//获取所有服务                      .map(discoveryClient::getInstances) //映射转换所有服务实例                      .filter(instances -> !instances.isEmpty()) //过滤出不为空的服务实例                      .map(instances -> instances.get(0)).filter(includePredicate)//根据properites里的include表达式过滤实例                      .map(instance -> {                  /*                      构建Route的定义                  */                          String serviceId = instance.getServiceId();                            RouteDefinition routeDefinition = new RouteDefinition();                          routeDefinition.setId(this.routeIdPrefix + serviceId);                          String uri = urlExpr.getValue(evalCtxt, instance, String.class);                          routeDefinition.setUri(URI.create(uri));                            final ServiceInstance instanceForEval = new DelegatingServiceInstance(                                  instance, properties);                  //添加Predicate                          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);                          }                          //添加filter                          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;                      });          }      }

由此我们可以知道,这里面利用DiscoveryClient获取所有的服务实例并将每个实例构建为一个Route,不过在此之前,在自动装配的类GatewayDiscoveryClientAutoConfiguration里已经配置了默认的Predicate与Filter,它会预先帮我们配置默认的Predicate与Filter:

    public static List<PredicateDefinition> initPredicates() {              ArrayList<PredicateDefinition> definitions = new ArrayList<>();              // TODO: add a predicate that matches the url at /serviceId?                // add a predicate that matches the url at /serviceId/**              PredicateDefinition predicate = new PredicateDefinition();              predicate.setName(normalizeRoutePredicateName(PathRoutePredicateFactory.class));              predicate.addArg(PATTERN_KEY, "'/'+serviceId+'/**'");              definitions.add(predicate);              return definitions;          }        public static List<FilterDefinition> initFilters() {              ArrayList<FilterDefinition> definitions = new ArrayList<>();                // add a filter that removes /serviceId by default              FilterDefinition filter = new FilterDefinition();              filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class));              String regex = "'/' + serviceId + '/(?<remaining>.*)'";              String replacement = "'/${remaining}'";              filter.addArg(REGEXP_KEY, regex);              filter.addArg(REPLACEMENT_KEY, replacement);              definitions.add(filter);                return definitions;          }

这里面主要会根据ServiceId构建为 Path=/serviceId/**的Predicate和路由至对应服务前把ServiceId去掉的filter

6、总结

根据上述说明,我仅仅选取了两个比较典型意义的Predicate与Filter代码进行说明,由于官网上没有说明自定义Predicate,我在这里索性写了个简单的例子,那么自定义Filter的例子可以参考官网地址:

这里需要吐槽一下官方 什么时候能把TODO补充完整的呢?

Gateway是基于Webflux实现的,它通过扩展HandlerMapping与WebHandler来处理用户的请求,先通过Predicate定位到Router然后在经过FilterChain的过滤处理,最后定位到下层服务。同时官方给我们提供了许多Prdicate与Filter,比如说限流的。从这点来说它的功能比zuul还强大呢,zuul里有的服务发现,断路保护等,Gateway分别通过GlobalFilter与Filter来实现。

最后至于Gateway能普及到什么样的程度,亦或者能不能最终成为统一的网关标准,这个我也不能再这里有所保证,那么就交给时间来证明吧。