• T1 - 简介

    Spring Cloud Gateway提供了一个基于Spring生态系统的API网关,是依赖于webflux的响应式框架,不能在传统的Servlet容器中工作。网关的常见作用包括路由转发、鉴权、限流、过滤等,以下为Spring Cloud Gateway的工作流程图:

    spring_cloud_gateway_diagram.png
    主要组件:

    • Route:路由网关的基本构建块,由ID、目标URI、一系列Predicates和Filters组成。路由根据predicate返回是否为true进行批评。
    • Predicate:Java8的函数式接口,输入类型是Spring的ServerWebExchange。Predicate可用于匹配HTTP请求的各种内容,如头部和参数。
    • Filter:使用特定工厂构建的实例,请求和响应都可以在被发送到下游请求之前或之后被修改

    流程:request->RouteLocator->RoutePredicateHandlerMapping(predicate)->FilteringWebHandler(filter)->Service->filter
    1.RouteLocatorBuilder.build()生成RouteLocator
    2.RoutePredicateHandlerMapping.lookupRoute()通过RouteLocator获取路由映射调用配置的predicate
    3.FilteringWebHandler.handle()调用filter

    gateway的rout、predicate、filter都可通过实例Bean或配置文件进行配置,且都是基于COC(Convention Over Configuration-约定由于配置)的设计方式,在gateway中显示为根据特定命名方式设计、实例化Bean。
  • T2 - Predicate用法

    gateway的所有Predicate都由XxxRoutePredicateFactory工厂类进行实例化,在配置文件中只需设置{Xxx=paramA[, paramB…]}即可完成一个Predicate类的实例化。下图为PredicateFactory的类层级图:
    在这里插入图片描述
    predicate与filter都可通过bean或spring配置文件进行实例化,Demo如下:

    • java bean配置

      @Bean
      public RouteLocator pathRouteLocator(RouteLocatorBuilder builder) {
          return builder.routes()
                  .route(p -> p
                          .path("/**")
                          .and()
                          .header("token", "\\w+")
                          .uri("lb://user-consumer"))
                  .build();
      }     
      
    • application.yml配置

      server:
        port: 8999
      spring:
        application:
          name: gateway-server
        cloud:
          gateway:
            discovery:
              locator:
                enabled: false
                lower-case-service-id: true
            routes:
              - uri: lb://user-consumer
                predicates:
                  - Header=token, \w+
                  - Path=/**
      

    以上例子配置了HeaderRoutePredicateFactory与PathRoutePredicateFactory,Header Predicate设置若header中包含符合\w+的token header,请求将路由到user-consumer服务。无论是通过bean还是application.yml配置,都是通过predicate的前缀名进行bean的配置,具体应用为bean route()中调用PredicateSpec.path()实现PathRoutePredicate,调用header方法实现HeaderRoutePredicate。使用配置文件的配置格式为:{Predicate-prefix}=param1[,param2,param3…],参数顺序以XxxRoutePredicateFactory.shortcutFieldOrder()的顺序为准,以HeaderRoutePredicateFactory为例,列表中第一个为header,第二个为regexp,则配置格式为Header={header},{regexp}。HeaderRoutePredicateFactory部分源码:

    /**
     * Header key.
     */
    public static final String HEADER_KEY = "header";
    
    /**
     * Regexp key.
     */
    public static final String REGEXP_KEY = "regexp";
    
    public HeaderRoutePredicateFactory() {
      super(Config.class);
    }
    
    @Override
    public List<String> shortcutFieldOrder() {
      return Arrays.asList(HEADER_KEY, REGEXP_KEY);
    }
    
  • T2 - Filter用法

    FilterTree
    上图为filter的所在包与依赖树,其配置方式与predicate类似,配置Demo如下:

    • java bean配置

      @Bean
      public RouteLocator pathRouteLocator(RouteLocatorBuilder builder) {
          return builder.routes()
                  .route(p -> p
                          .path("/**")
                          .and()
                          .header("token", "\\w+")
                          .filters(f -> f.stripPrefix(1)
                                  .addResponseHeader("response-header", "head-val"))
                          .uri("lb://user-consumer"))
                  .build();
      } 
      
    • application.yml配置

      server:
        port: 8999
      spring:
        application:
          name: gateway-server
        cloud:
          gateway:
            discovery:
              locator:
                enabled: false
                lower-case-service-id: true
            routes:
              - uri: lb://user-consumer
                predicates:
                  - Header=token, \w+
                  - Path=/**
                filters:
                  - StripPrefix=1
                  - AddResponseHeader=response-header, head-val
      

    以上例子基于predicate的基础上添加了StripPrefixGatewayFilterFactory与AddResponseHeaderGatewayFilterFactory的过滤器,StripPrefixGatewayFilterFactory为截取路径中的前i个路径(如i=2,请求路径为/a/b/hi,则由网关请求到service的路径为/hi),AddResponseHeaderGatewayFilterFactory则对service返回结果进行加工添加header,然后再返回结果。这2个体现了filter可对请求到service前|后进行参数、header、路径、响应的处理,即官方文档中的"pre"与"post"两种filter,对响应的处理(post)Filter工厂类一般名称中包含Response,配置文件的配置方式同predicate。

  • T3 - Eureka服务注册发现

    当没有配置注册中心时,每次添加新服务都需在网关添加新的服务配置,请求流程为Web->Gateway->Service;当添加注册中心后,网关便可通过注册中心访问中心上的服务,无论服务新增还是减少都无需重新配置网关路由,请求流程转变为Web->Gateway->Registry Center->Service。集成注册中心eureka配置Demo:

    eureka:  
      client:  
        service-url:  
          defaultZone: http://localhost:8000/eureka/  
    logging:  
      level:  
        org.springframework.cloud.gateway: debug  
    spring:  
      profiles: gateway-eureka  
      cloud:  
        gateway:  
          discovery:  
            locator:  
              # 启用DiscoveryClient网关集成,通过网关地址与serviceName访问注册中心的service  
              enabled: true  
              # 服务名转小写  
              lower-case-service-id: true
    

    配置注册中心后便可通过 http://{ip}.{gatewayPort}/{serviceName}/{api} 访问注册中心上的服务接口,由T2可知gateway可通过Predicate将gateway接收到的请求路由到其它url,但配置注册中心后会发现我们无需配置具体服务路由gateway就可将请求分发到相应的service上,由此我联想到了以下2个问题:

    1. 那么gateway是怎么分发这些路由的呢?
    2. 没有配置注册中心时是在routes中配置predicate与filter的,那配置注册中心后该如何配置呢?
    • T3.1 - gateway分发路由规则

      以下是gateway的自动化配置类GatewayDiscoveryClientAutoConfiguration.java:

      @Configuration  
      @ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)  
      @AutoConfigureBefore(GatewayAutoConfiguration.class)  
      @AutoConfigureAfter(CompositeDiscoveryClientAutoConfiguration.class)  
      @ConditionalOnClass({ DispatcherHandler.class, DiscoveryClient.class })  
      @EnableConfigurationProperties  
      public class GatewayDiscoveryClientAutoConfiguration {  
      
        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;  
        }  
      
        @Bean  
        @ConditionalOnBean(DiscoveryClient.class)  
        @ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled")  
        public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(  
               DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {  
          return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);  
        }  
      
        @Bean  
        public DiscoveryLocatorProperties discoveryLocatorProperties() {  
            DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties();  
          properties.setPredicates(initPredicates());  
          properties.setFilters(initFilters());  
          return properties;  
        }  
      }
      

      由以上类可看出gateway默认配置了匹配/serviceId/**的PathRoutePredicate与RewritePathGatewayFilter,spring.cloud.gateway.discovery.locator.enabled为true时会将predicate和filter配置到类DiscoveryClientRouteDefinitionLocator,DiscoveryClientRouteDefinitionLocator会以SpEL解析predicate、filter配置,serviceId会被解析成对应的service,当有服务注册到注册中心时,gateway会检测并将predicate与filter应用到服务,如下图所示,问题1解决。

      eureka-default-config.png

    • T3.2 - 统一配置服务predicate与filter

      配置注册中心后,可用过spring.cloud.gateway.discovery.locator.predicates|filters对注册中心中的服务进行predicate与filter的统一配置,但会覆盖默认的predicate与filter配置,此时可添加PathRoutePredicate与RewritePathGatewayFilter默认配置保持默认功能,配置Demo如下图所示:

      spring:
        profiles: gateway-eureka
        cloud:
          gateway:
            discovery:
              locator:
                # 启用DiscoveryClient网关集成,可通过网关地址与serviceName访问注册中心的service
                enabled: true
                # 服务名转小写
                lower-case-service-id: true
                predicates:
                  - name: Path
                    args:
                      patterns: "'/'+serviceId+'/**'"
                  - name: Header
                    args:
                      header: "'token'"
                      regexp: "'\\w+'"
                filters:
                  - name: RewritePath
                    args:
                      replacement: "'/${remaining}'"
                      regexp: "'/' + serviceId + '/(?<remaining>.*)'"
                  - name: AddResponseHeader
                    args:
                      name: "'response-test-head'"
                      value: "'response-test'"
      
  • T4 - 自定义全局Filter

    当配置注册中心后,可能就会出现各服务统一Token鉴权、统一日志记录、超时设置等需求,这些可以统一在gateway进行处理。gateway全局过滤器都需实现GlobalFilter,一般还需实现Ordered接口控制Bean实例化顺序,以一个简单token全局过滤器为例:

    @Slf4j
    @Component
    @ConditionalOnProperty(value = "spring.cloud.gateway.discovery.locator.global-token-filter-enabled", havingValue = "true")
    public class GlobalTokenFilter implements GlobalFilter, Ordered {
        @PostConstruct
        public void init() {
            log.info("GlobalTokenFilter init finish");
        }
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            Map<String, String> headers = exchange.getRequest().getHeaders().toSingleValueMap();
            boolean hasToken = headers.containsKey("token") && StringUtils.isNotBlank(headers.get("token"));
            if (!hasToken) {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
            log.info("token value:" + headers.get("token"));
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            return -100;
        }
    }
    

    请求的各种信息都可通过ServerWebExchange获取,当有请求到达gateway时,GlobalTokenFilter检测是否含token的头部,没有的话gateway将返回404。我们也可以通过继承AbstractGatewayFilterFactory来自定义过滤器,如简单的请求记录过滤器:

    @Slf4j
    @Component
    public class RequestRecordGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestRecordGatewayFilterFactory.Config> {
    
        public static final String PARTS_KEY = "print";
    
        public RequestRecordGatewayFilterFactory() {
            super(Config.class);
        }
    
        @Override
        public List<String> shortcutFieldOrder() {
            return Arrays.asList(PARTS_KEY);
        }
    
        @Override
        public GatewayFilter apply(Config config) {
            return (exchange, chain) -> {
                if (config.print != null && config.print) {
                    // 打印相对路径
                    log.info("requestRecord1:{},参数:{}", exchange.getRequest().getPath().value(), exchange.getRequest().getQueryParams().toSingleValueMap());
                    // 打印绝对路径
                    log.info("requestRecord1:{},参数:{}", exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR), exchange.getRequest().getQueryParams().toSingleValueMap());
                }
                return chain.filter(exchange);
            };
        }
    
        @Override
        public GatewayFilter apply(Consumer<Config> consumer) {
            Config config = newConfig();
            consumer.accept(config);
            return apply(config);
        }
    
        @ToString
        public static class Config {
            private Boolean print;
    
            public boolean isPrint() {
                return print;
            }
    
            public void setPrint(boolean print) {
                this.print = print;
            }
        }
    }
    

    同时application.yml添加RequestRecordFilter配置:

    spring:
      profiles: gateway-eureka
      cloud:
        gateway:
          discovery:
            locator:
              # 启用DiscoveryClient网关集成,可通过网关地址与serviceName访问注册中心的service
              enabled: true
              # 服务名转小写
              lower-case-service-id: true
              predicates:
                - name: Path
                  args:
                    patterns: "'/'+serviceId+'/**'"
                - name: Header
                  args:
                    header: "'token'"
                    regexp: "'\\w+'"
              filters:
                - name: RewritePath
                  args:
                    replacement: "'/${remaining}'"
                    regexp: "'/' + serviceId + '/(?<remaining>.*)'"
                - name: AddResponseHeader
                  args:
                    name: "'response-test-head'"
                    value: "'response-test'"
                - name: RequestRecord
                  args:
                    print: true
    

    请求的参数日志也可通过一个GlobalFilter去实现,区别是GlobalFilter的ServerWebExchange尚未把原始请求的url设置到属性中(Attrubute),所以打印出的绝对路径为null,且继承AbstractGatewayFilterFactory获取的相对路径是截掉了serviceId的,因为在其它过滤器操作执行前路径已被RewritePath过滤器进行了路径处理,具体效果图如下:
    global-filter

    若要自定义Predicate只需继承AbstractRoutePredicateFactory类根据业务需求重写相应方法,具体可参考AbstractRoutePredicateFactory的子类,与自定义Fitler大同小异,这里便不唠叨了。

  • T5 - 总结

    该文章主要讲gateway的主要用法并根据个人的思考扩展了一些gateway的用例,大部分知识点都是基于官方文档进行总结,且由以上功能Demo可看出gateway的处理流程并不复杂,由Web->Gateway(Predicate->GlobalFilter->Pre Filter)->Service->Gateway(Post Gateway)->Web

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐