Spring Cloud Gateway入坑记

最近在做老系统的重构,重构完成后新系统中需要引入一个网关服务,作为新系统和老系统接口的适配和代理。之前,很多网关应用使用的是 Spring-Cloud-Netfilx 基于 Zuul1.x 版本实现的那套方案,但是鉴于 Zuul1.x 已经停止迭代,它使用的是比较传统的阻塞(B)IO + 多线程的实现方案,其实性能不太好。后来Spring团队干脆自己重新研发了一套网关组件,这个就是本次要调研的 Spring-Cloud-Gateway

Spring Cloud Gateway 依赖于 Spring Boot 2.0 , Spring WebFlux ,和 Project Reactor 。许多熟悉的同步类库(例如 Spring-Data Spring-Security )和同步编程模式在 Spring Cloud Gateway 中并不适用,所以最好先阅读一下上面提到的三个框架的文档。

Spring Cloud Gateway 依赖于 Spring Boot Spring WebFlux 提供的基于 Netty 的运行时环境,它并非构建为一个WAR包或者运行在传统的 Servlet 容器中。

  • 路由(Route):路由是网关的基本组件。它由ID,目标URI,谓词(Predicate)集合和过滤器集合定义。如果谓词聚合判断为真,则匹配路由。
  • 谓词(Predicate):使用的是Java8中基于函数式编程引入的 java.util.Predicate 。使用谓词(聚合)判断的时候,输入的参数是 ServerWebExchange 类型,它允许开发者匹配来自HTTP请求的任意参数,例如HTTP请求头、HTTP请求参数等等。
  • 过滤器(Filter):使用的是指定的 GatewayFilter 工厂所创建出来的 GatewayFilter 实例,可以在发送请求到下游之前或者之后修改请求(参数)或者响应(参数)。
  • 其实 Filter 还包括了 GlobalFilter ,不过在官方文档中没有提到。

    客户端向 Spring Cloud Gateway 发出请求,如果 Gateway Handler Mapping 模块处理当前请求如果匹配到一个目标路由配置,该请求就会转发到 Gateway Web Handler 模块。 Gateway Web Handler 模块在发送请求的时候,会把该请求通过一个匹配于该请求的过滤器链。上图中过滤器被虚线分隔的原因是:过滤器的处理逻辑可以在代理请求发送之前或者之后执行。所有 pre 类型的过滤器执行之后,代理请求才会创建(和发送),当代理请求创建(和发送)完成之后,所有的 post 类型的过滤器才会执行。

    见上图,外部请求进来后如果落入过滤器链,那么虚线左边的就是 pre 类型的过滤器,请求先经过 pre 类型的过滤器,再发送到目标被代理的服务。目标被代理的服务响应请求,响应会再次经过滤器链,也就是走虚线右侧的过滤器链,这些过滤器就是 post 类型的过滤器。

    注意,如果在路由配置中没有明确指定对应的路由端口,那么会使用如下的默认端口:

  • HTTP协议,使用80端口。
  • HTTPS协议,使用443端口。
  • 建议直接通过Train版本(其实笔者考究过,Train版本的代号其实是伦敦地铁站的命名,像当前的 Spring Cloud 最新版本是 Greenwich.SR1 Greenwich 可以在伦敦地铁站的地图查到这个站点,对应的 SpringBoot 版本是2.1.x)引入 Spring-Cloud-Gateway ,因为这样可以跟上最新稳定版本的 Spring-Cloud 版本,另外由于 Spring-Cloud-Gateway 基于 Netty 的运行时环境启动,不需要引入带 Servlet 容器的 spring-boot-starter-web

    父POM引入下面的配置:

    <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Greenwich.SR1</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>2.1.4.RELEASE</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
    </dependencyManagement>
    

    子模块或者需要引入Spring-Cloud-Gateway的模块POM引入下面的配置:

        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
        </dependencies>
    

    创建一个启动类即可:

    @SpringBootApplication
    public class RouteServerApplication {
    	public static void main(String[] args) {
    		SpringApplication.run(RouteServerApplication.class, args);
    

    网关配置最终需要转化为一个RouteDefinition的集合,配置的定义接口如下:

    public interface RouteDefinitionLocator {
    	Flux<RouteDefinition> getRouteDefinitions();
    

    通过YAML文件配置或者流式编程式配置(其实文档中还有配合Eureka的DiscoveryClient进行配置,这里暂时不研究),最终都是为了创建一个RouteDefinition的集合。

    Yaml配置

    配置实现是PropertiesRouteDefinitionLocator,关联着配置类GatewayProperties

    spring:
      cloud:
        gateway:
          routes:
           - id: datetime_after_route    # <------ 这里是路由配置的ID
            uri: http://www.throwable.club  # <------ 这里是路由最终目标Server的URI(Host)
            predicates:                     # <------ 谓词集合配置,多个是用and逻辑连接
             - Path=/blog    # <------- Key(name)=Expression,键是谓词规则工厂的ID,值一般是匹配规则的正则表示
    

    编程式流式配置

    编程式和流式编程配置需要依赖RouteLocatorBuilder,目标是构造一个RouteLocator实例:

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/blog")
                    .uri("http://www.throwable.club")
                .build();
    

    路由谓词工厂

    Spring Cloud Gateway将路由(Route)作为Spring-WebFluxHandlerMapping组件基础设施的一部分,也就是HandlerMapping进行匹配的时候,会把配置好的路由规则也纳入匹配机制之中。Spring Cloud Gateway自身包含了很多内建的路由谓词工厂。这些谓词分别匹配一个HTTP请求的不同属性。多个路由谓词工厂可以用and的逻辑组合在一起。

    目前Spring Cloud Gateway提供的内置的路由谓词工厂如下:

    指定日期时间规则路由谓词

    按照配置的日期时间指定的路由谓词有三种可选规则:

  • 匹配请求在指定日期时间之前。
  • 匹配请求在指定日期时间之后。
  • 匹配请求在指定日期时间之间。
  • 值得注意的是,配置的日期时间必须满足ZonedDateTime的格式:

    //年月日和时分秒用'T'分隔,接着-07:00是和UTC相差的时间,最后的[America/Denver]是所在的时间地区
    2017-01-20T17:42:47.789-07:00[America/Denver]
    

    例如网关的应用是2019-05-01T00:00:00+08:00[Asia/Shanghai]上线的,上线之后的请求都路由奥www.throwable.club,那么配置如下:

    server 
      port: 9090
    spring:
      cloud:
        gateway:
          routes:
           - id: datetime_after_route
            uri: http://www.throwable.club
            predicates:
             - After=2019-05-01T00:00:00+08:00[Asia/Shanghai]
    

    此时,只要请求网关http://localhost:9090,请求就会转发到http://www.throwable.club

    如果想要只允许2019-05-01T00:00:00+08:00[Asia/Shanghai]之前的请求,那么只需要改为:

    server 
      port: 9091
    spring:
      cloud:
        gateway:
          routes:
           - id: datetime_before_route
            uri: http://www.throwable.club
            predicates:
             - Before=2019-05-01T00:00:00+08:00[Asia/Shanghai]
    

    如果只允许两个日期时间段之间的时间进行请求,那么只需要改为:

    server 
      port: 9090
    spring:
      cloud:
        gateway:
          routes:
           - id: datetime_between_route
            uri: http://www.throwable.club
            predicates:
             - Between=2019-05-01T00:00:00+08:00[Asia/Shanghai],2019-05-02T00:00:00+08:00[Asia/Shanghai]
    

    那么只有2019年5月1日0时到5月2日0时的请求才能正常路由。

    Cookie路由谓词

    CookieRoutePredicateFactory需要提供两个参数,分别是Cookie的name和一个正则表达式(value)。只有在请求中的Cookie对应的name和value和Cookie路由谓词中配置的值匹配的时候,才能匹配命中进行路由。

    server 
      port: 9090
    spring:
      cloud:
        gateway:
          routes:
           - id: cookie_route
            uri: http://www.throwable.club
            predicates:
             - Cookie=doge,throwable
    

    请求需要携带一个Cookie,name为doge,value需要匹配正则表达式"throwable"才能路由到http://www.throwable.club

    这里尝试本地搭建一个订单Order服务,基于SpringBoot2.1.4搭建,启动在9091端口:

    // 入口类
    @RestController
    @RequestMapping(path = "/order")
    @SpringBootApplication
    public class OrderServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(OrderServiceApplication.class, args);
        @GetMapping(value = "/cookie")
        public ResponseEntity<String> cookie(@CookieValue(name = "doge") String doge) {
            return ResponseEntity.ok(doge);
    

    订单服务application.yaml配置:

    spring:
      application:
        name: order-service
    server:
      port: 9091
    

    网关路由配置:

    spring:
      application:
        name: route-server
      cloud:
        gateway:
          routes:
            - id: cookie_route
              uri: http://localhost:9091
              predicates:
                - Cookie=doge,throwable
    
    curl http://localhost:9090/order/cookie --cookie "doge=throwable"
    //响应结果
    throwable
    

    Header路由谓词

    HeaderRoutePredicateFactory需要提供两个参数,分别是Header的name和一个正则表达式(value)。只有在请求中的Header对应的name和value和Header路由谓词中配置的值匹配的时候,才能匹配命中进行路由。

    订单服务中新增一个/header端点:

    @RestController
    @RequestMapping(path = "/order")
    @SpringBootApplication
    public class OrderServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(OrderServiceApplication.class, args);
        @GetMapping(value = "/header")
        public ResponseEntity<String> header(@RequestHeader(name = "accessToken") String accessToken) {
            return ResponseEntity.ok(accessToken);
    

    网关的路由配置如下:

    spring:
      cloud:
        gateway:
          routes:
            - id: header_route
              uri: http://localhost:9091
              predicates:
                - Header=accessToken,Doge
    
    curl -H "accessToken:Doge" http://localhost:9090/order/header
    //响应结果
    

    Host路由谓词

    HostRoutePredicateFactory只需要指定一个主机名列表,列表中的每个元素支持Ant命名样式,使用.作为分隔符,多个元素之间使用,区分。Host路由谓词实际上针对的是HTTP请求头中的Host属性。

    订单服务中新增一个/header端点:

    @RestController
    @RequestMapping(path = "/order")
    @SpringBootApplication
    public class OrderServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(OrderServiceApplication.class, args);
        @GetMapping(value = "/host")
        public ResponseEntity<String> host(@RequestHeader(name = "Host") String host) {
            return ResponseEntity.ok(host);
    

    网关的路由配置如下:

    spring:
      cloud:
        gateway:
          routes:
            - id: host_route
              uri: http://localhost:9091
              predicates:
                - Host=localhost:9090
    
    curl http://localhost:9090/order/host
    //响应结果
    localhost:9091  # <--------- 这里要注意一下,路由到订单服务的时候,Host会被修改为localhost:9091
    

    其实可以定制更多样化的Host匹配模式,甚至可以支持URI模板变量。

    - Host=www.throwable.**,**.throwable.**
    - Host={sub}.throwable.club
    

    请求方法路由谓词

    MethodRoutePredicateFactory只需要一个参数:要匹配的HTTP请求方法。

    网关的路由配置如下:

    spring:
      cloud:
        gateway:
          routes:
            - id: method_route
              uri: http://localhost:9091
              predicates:
                - Method=GET
    

    这样配置,所有的进入到网关的GET方法的请求都会路由到http://localhost:9091

    订单服务中新增一个/get端点:

    @GetMapping(value = "/get")
    public ResponseEntity<String> get() {
        return ResponseEntity.ok("get");
    
    curl http://localhost:9090/order/get
    //响应结果
    

    请求路径路由谓词

    PathRoutePredicateFactory需要PathMatcher模式路径列表和一个可选的标志位参数matchOptionalTrailingSeparator。这个是最常用的一个路由谓词。

    spring:
      cloud:
        gateway:
          routes:
            - id: path_route
              uri: http://localhost:9091
              predicates:
                - Path=/order/path
    
    @GetMapping(value = "/path")
    public ResponseEntity<String> path() {
        return ResponseEntity.ok("path");
    
    curl http://localhost:9090/order/path
    //响应结果
    

    此外,可以通过{segment}占位符配置路径如/foo/1/foo/bar/bar/baz,如果通过这种形式配置,在匹配命中进行路由的时候,会提取路径中对应的内容并且将键值对放在ServerWebExchange.getAttributes()集合中,KEY为ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE,这些提取出来的属性可以供GatewayFilter Factories使用。

    请求查询参数路由谓词

    QueryRoutePredicateFactory需要一个必须的请求查询参数(param的name)以及一个可选的正则表达式(regexp)。

    spring:
      cloud:
        gateway:
          routes:
          - id: query_route
            uri: http://localhost:9091
            predicates:
            - Query=doge,throwabl.
    

    这里配置的param就是doge,正则表达式是throwabl.

    @GetMapping(value = "/query")
    public ResponseEntity<String> query(@RequestParam("name") String doge) {
      return ResponseEntity.ok(doge);
    
    curl http://localhost:9090/order/query?doge=throwable
    //响应结果
    throwable 
    

    远程IP地址路由谓词

    RemoteAddrRoutePredicateFactory匹配规则采用CIDR符号(IPv4或IPv6)字符串的列表(最小值为1),例如192.168.0.1/16(其中192.168.0.1是远程IP地址并且16是子网掩码)。

    spring:
      cloud:
        gateway:
          routes:
          - id: remoteaddr_route
            uri: http://localhost:9091
            predicates:
            - RemoteAddr=127.0.0.1
    
    @GetMapping(value = "/remote")
    public ResponseEntity<String> remote() {
      return ResponseEntity.ok("remote");
    
    curl http://localhost:9090/order/remote
    //响应结果
    remote 
    

    关于远程IP路由这一个路由谓词其实还有很多扩展手段,这里暂时不展开。

    多个路由谓词组合

    因为路由配置中的predicates属性其实是一个列表,可以直接添加多个路由规则:

    spring:
      cloud:
        gateway:
          routes:
          - id: remoteaddr_route
            uri: http://localhost:9091
            predicates:
            - RemoteAddr=xxxx
            - Path=/yyyy
            - Query=zzzz,aaaa
    

    这些规则是用and逻辑组合的,例如上面的例子相当于:

    request = ...
    if(request.getRemoteAddr == 'xxxx' && request.getPath match '/yyyy' && request.getQuery('zzzz') match 'aaaa') {
        return true;
    return false;
    

    GatewayFilter工厂

    路由过滤器GatewayFilter允许修改进来的HTTP请求内容或者返回的HTTP响应内容。路由过滤器的作用域是一个具体的路由配置。Spring Cloud Gateway提供了丰富的内建的GatewayFilter工厂,可以按需选用。

    因为GatewayFilter工厂类实在太多,笔者这里举个简单的例子。

    如果我们想对某些请求附加特殊的HTTP请求头,可以选用AddRequestHeaderX-Request-Foo:Barapplication.yml如下:

    spring:
      cloud:
        gateway:
          routes:
          - id: add_request_header_route
            uri: https://example.org
            filters:
            - AddRequestHeader=X-Request-Foo,Bar
    

    那么所有的从网关入口的HTTP请求都会添加一个特殊的HTTP请求头:X-Request-Foo:Bar

    目前GatewayFilter工厂的内建实现如下:

    GatewayFilter工厂使用的时候需要知道其ID以及配置方式,配置方式可以看对应工厂类的公有静态内部类XXXXConfig

    GlobalFilter工厂

    GlobalFilter的功能其实和GatewayFilter是相同的,只是GlobalFilter的作用域是所有的路由配置,而不是绑定在指定的路由配置上。多个GlobalFilter可以通过@Order或者getOrder()方法指定每个GlobalFilter的执行顺序,order值越小,GlobalFilter执行的优先级越高。

    注意,由于过滤器有pre和post两种类型,pre类型过滤器如果order值越小,那么它就应该在pre过滤器链的顶层,post类型过滤器如果order值越小,那么它就应该在pre过滤器链的底层。示意图如下:

    例如要实现负载均衡的功能,application.yml配置如下:

    spring:
      cloud:
        gateway:
          routes:
          - id: myRoute
            uri: lb://myservice   # <-------- lb特殊标记会使用LoadBalancerClient搜索目标服务进行负载均衡
            predicates:
            - Path=/service/**
    

    目前Spring Cloud Gateway提供的内建的GlobalFilter如下:

    内建的GlobalFilter大多数和ServerWebExchangeUtils的属性相关,这里就不深入展开。

    网关可以通过配置来控制全局的CORS行为。全局的CORS配置对应的类是CorsConfiguration,这个配置是一个URL模式的映射。例如application.yaml文件如下:

    spring:
      cloud:
        gateway:
          globalcors:
            corsConfigurations:
              '[/**]':
                allowedOrigins: "https://docs.spring.io"
                allowedMethods:
                - GET
    

    在上面的示例中,对于所有请求的路径,将允许来自docs.spring.io并且是GET方法的CORS请求。

    Actuator端点相关

    引入spring-boot-starter-actuator,需要做以下配置开启gateway监控端点:

    management.endpoint.gateway.enabled=true 
    management.endpoints.web.exposure.include=gateway
    

    目前支持的端点列表:

    HTTP方法

    笔者虽然是一个底层的码畜,但是很久之前就向身边的朋友说:

    反应式编程结合同步非阻塞IO或者异步非阻塞IO是目前网络编程框架的主流方向,最好要跟上主流的步伐掌握这些框架的使用,有能力最好成为它们的贡献者。

    目前常见的反应式编程框架有:

  • ReactorRxJava2,其中Reactor在后端的JVM应用比较常见,RxJava2在安卓编写的APP客户端比较常见。
  • Reactor-Netty,这个是基于ReactorNetty封装的。
  • Spring-WebFluxSpring-Cloud-Gateway,其中Spring-Cloud-Gateway依赖Spring-WebFlux,而Spring-WebFlux底层依赖于Reactor-Netty
  • 根据这个链式关系,最好系统学习一下ReactorNetty

    参考资料:

  • Spring-Cloud-Gateway官方文档
  • Reactor官方文档
  • 选用Spring-Cloud-Gateway不仅仅是为了使用新的技术,更重要的是它的性能有了不俗的提升,基准测试项目spring-cloud-gateway-bench的结果如下:

    代理组件(Proxy) 平均交互延迟(Avg Latency) 平均每秒处理的请求数(Avg Requests/Sec)
  • Github Page:http://www.throwable.club/2019/05/04/spring-cloud-gateway-guide
  • Coding Page:http://throwable.coding.me/2019/05/04/spring-cloud-gateway-guide
  • (本文完 c-3-d e-a-20190504)

    技术公众号(《Throwable文摘》),不定期推送笔者原创技术文章(绝不抄袭或者转载):

    娱乐公众号(《天天沙雕》),甄选奇趣沙雕图文和视频不定期推送,缓解生活工作压力: