筆記-- Spring Cloud(十):Spring Cloud Gateway(路由)

2020-10-25 14:00:44

Spring Cloud(十):Spring Cloud Gateway(路由)

本篇文章主要介紹了什麼是 Spring Cloud Gateway,

在這裡插入圖片描述

概述

Spring Cloud Gateway 是 Spring Cloud 的一個全新專案,該專案是基於 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的閘道器,它旨在為微服務架構提供一種簡單有效的統一的 API 路由管理方式。

Spring Cloud Gateway 作為 Spring Cloud 生態系統中的閘道器,目標是替代 Netflix Zuul,其不僅提供統一的路由方式,並且基於 Filter 鏈的方式提供了閘道器基本的功能,例如:安全、監控、埋點和限流等。

Spring Cloud Gateway 的特徵:

  • 基於 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
  • 動態路由
  • Predicates 和 Filters 作用於特定路由
  • 整合 Hystrix 斷路器
  • 整合 Spring Cloud DiscoveryClient
  • 易於編寫的 Predicates 和 Filters
  • 限流
  • 路徑重寫

vs Netflix Zuul

Zuul 基於 Servlet 2.5(使用 3.x),使用阻塞 API,它不支援任何長連線,如 WebSockets。而 Spring Cloud Gateway 建立在 Spring Framework 5,Project Reactor 和 Spring Boot 2 之上,使用非阻塞 API,支援 WebSockets,並且由於它與 Spring 緊密整合,所以將會是一個更好的開發體驗。

術語

  • Route(路由):這是閘道器的基本構建塊。它由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。如果斷言為真,則路由匹配。
  • Predicate(斷言):這是一個 Java 8 的 Predicate。輸入型別是一個ServerWebExchange。我們可以使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或引數。
  • Filter(過濾器):這是org.springframework.cloud.gateway.filter.GatewayFilter的範例,我們可以使用它修改請求和響應。

流程

在這裡插入圖片描述

使用者端向 Spring Cloud Gateway 發出請求。然後在 Gateway Handler Mapping 中找到與請求相匹配的路由,將其傳送到 Gateway Web Handler。Handler 再通過指定的過濾器鏈來將請求傳送到我們實際的服務執行業務邏輯,然後返回。
過濾器之間用虛線分開是因為過濾器可能會在傳送代理請求之前(「pre」)或之後(「post」)執行業務邏輯。

實戰

我們之前使用 Zuul 實現了一個閘道器,這裡我們就用 Spring Cloud Gateway 來替代它實現相同的功能。在這裡我們繼續沿用之前已經寫好的服務並依次啟動:

  • eureka
  • producer
  • consumer

新建一個標準的 Spring Boot 工程,命名為 gateway,然後在 pom.xml 中引入以下依賴座標

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

application.yml 組態檔內容如下

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
#      routes:
#        - id: default_path_to_http
#          uri: https://myurl
#          order: 10000
#          predicates:
#            - Path=/**
#          filters:
#            - SetPath=/
server:
  port: 10000
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka/
logging:
  level:
    org.springframework.cloud.gateway: debug

設定說明:

  • spring.cloud.gateway.discovery.locator.enabled:是否與服務註冊於發現元件進行結合,通過 serviceId 轉發到具體的服務範例。預設為false,設為true便開啟通過服務中心的自動根據 serviceId 建立路由的功能。
  • spring.cloud.gateway.routes用於配合具體的路由規則,是一個陣列。這裡我建立了一個 id 為default_path_to_http的路由,其中的設定是將未匹配的請求轉發到https://myurl。實際上開啟了服務發現後,如果只使用預設建立的路由規則,這個 routes 不設定也是可以的,所以我就先註釋掉了不用它了。
  • 閘道器服務監聽 10000 埠
  • 指定註冊中心的地址,以便使用服務發現功能
  • 調整相關包的 log 級別,以便排查問題

Spring Boot 的啟動類不用修改,直接啟動即可,啟動後我們便可在 Eureka 中看到我們的閘道器服務

在這裡插入圖片描述

然後我們像之前使用 Zuul 那樣存取 http://localhost:10000/consumer/hello/yujian

我們期待像直接存取 consumer 那樣能返回 「Hello yujian!」,但是實際上卻出錯了,返回了 404 錯誤。我們來看一下 log

2020-08-23 15:05:09.562 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_EUREKA-PRODUCER applying {pattern=/EUREKA-PRODUCER/**} to Path
2020-08-23 15:05:09.564 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_EUREKA-PRODUCER applying filter {regexp=/EUREKA-PRODUCER/(?<remaining>.*), replacement=/${remaining}} to RewritePath
2020-08-23 15:05:09.566 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: ReactiveCompositeDiscoveryClient_EUREKA-PRODUCER
2020-08-23 15:05:09.567 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_EUREKA-CONSUMER applying {pattern=/EUREKA-CONSUMER/**} to Path
2020-08-23 15:05:09.569 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_EUREKA-CONSUMER applying filter {regexp=/EUREKA-CONSUMER/(?<remaining>.*), replacement=/${remaining}} to RewritePath
2020-08-23 15:05:09.571 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: ReactiveCompositeDiscoveryClient_EUREKA-CONSUMER
2020-08-23 15:05:09.572 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_CLOUD-GATEWAY applying {pattern=/CLOUD-GATEWAY/**} to Path
2020-08-23 15:05:09.574 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_CLOUD-GATEWAY applying filter {regexp=/CLOUD-GATEWAY/(?<remaining>.*), replacement=/${remaining}} to RewritePath
2020-08-23 15:05:09.576 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: ReactiveCompositeDiscoveryClient_CLOUD-GATEWAY

可以看到 Spring Cloud Gateway 確實為我們的 producer 和 consumer 自動建立了對應的路由,但是這裡的 pattern/regexp 裡都是大寫的。那我們就換成大寫的來試一下。

存取 http://localhost:20000/EUREKA-CONSUMER/hello/?name=yujian 確實返回了 「Hello, yujian!」,這時再看 log

2020-08-23 15:06:07.664 DEBUG 16484 --- [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping   : Route matched: ReactiveCompositeDiscoveryClient_EUREKA-CONSUMER
2020-08-23 15:06:07.664 DEBUG 16484 --- [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping   : Mapping [Exchange: GET http://localhost:20000/EUREKA-CONSUMER/hello/?name=yujian] to Route{id='ReactiveCompositeDiscoveryClient_EUREKA-CONSUMER', uri=lb://EUREKA-CONSUMER, order=0, predicate=Paths: [/EUREKA-CONSUMER/**], match trailing slash: true, gatewayFilters=[[[RewritePath /EUREKA-CONSUMER/(?<remaining>.*) = '/${remaining}'], order = 1]], metadata={jmx.port=52177, management.port=9000}}
2020-08-23 15:06:07.664 DEBUG 16484 --- [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping   : [a6395175-5] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@514419ac
2020-08-23 15:06:07.665 DEBUG 16484 --- [ctor-http-nio-3] o.s.c.g.handler.FilteringWebHandler      : Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@4f263698}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@7810580e}, order = -2147482648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@c42b297}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@15a7e17}, order = 0], [[RewritePath /EUREKA-CONSUMER/(?<remaining>.*) = '/${remaining}'], order = 1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@70115457}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@a96a3c8}, order = 10100], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@7fb61a52}, order = 2147483646], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@5e56c74a}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@1815b188}, order = 2147483647]]
2020-08-23 15:06:09.609 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_EUREKA-PRODUCER applying {pattern=/EUREKA-PRODUCER/**} to Path
2020-08-23 15:06:09.612 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_EUREKA-PRODUCER applying filter {regexp=/EUREKA-PRODUCER/(?<remaining>.*), replacement=/${remaining}} to RewritePath
2020-08-23 15:06:09.613 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: ReactiveCompositeDiscoveryClient_EUREKA-PRODUCER
2020-08-23 15:06:09.615 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_EUREKA-CONSUMER applying {pattern=/EUREKA-CONSUMER/**} to Path
2020-08-23 15:06:09.616 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_EUREKA-CONSUMER applying filter {regexp=/EUREKA-CONSUMER/(?<remaining>.*), replacement=/${remaining}} to RewritePath
2020-08-23 15:06:09.617 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: ReactiveCompositeDiscoveryClient_EUREKA-CONSUMER
2020-08-23 15:06:09.617 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_CLOUD-GATEWAY applying {pattern=/CLOUD-GATEWAY/**} to Path
2020-08-23 15:06:09.618 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition ReactiveCompositeDiscoveryClient_CLOUD-GATEWAY applying filter {regexp=/CLOUD-GATEWAY/(?<remaining>.*), replacement=/${remaining}} to RewritePath
2020-08-23 15:06:09.619 DEBUG 16484 --- [freshExecutor-0] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: ReactiveCompositeDiscoveryClient_CLOUD-GATEWAY

可以看出,Spring Cloud Gateway 自動的為我們的 consumer 建立了一個路由,類似於下邊這樣

routes:
  - id: CompositeDiscoveryClient_CONSUMER
    uri: lb://CONSUMER
    order: 0
    predicates:
      - Path=/CONSUMER/**
    filters:
      - RewritePath=/CONSUMER/(?<segment>.*), /$\{segment}

但是這種必須將 serviceId 大寫的方式還是比較蛋疼的,雖然 Eureka 註冊中心預設顯示的都是大寫的,但是這大寫的路徑放在 URL 真的好嗎?我唯一能想到的好處就是能清晰分辨出 serviceId 和 path。

如果大寫的 URL 在瀏覽器裡自動變成了小寫的,可以試試:清空快取、使用無痕模式(command+shift+n)、在終端直接用curl

上邊是基於服務發現的預設路由規則,如果我們要自定義路由規則怎麼辦呢?

比如我們的這個服務是跟客戶服務相關的(嗯,目前它功能比較單一,只會跟客戶 say hi,但是這沒有影響),我們希望這個服務的 path 以 /customer/ 開頭,具體到這個例子,就是 /costomer/hello/{name}。並且,我們還要為每個響應新增一個響應頭X-Response-Default-Foo: Default-Bar

讓我們來修改一下設定,主要是增加一個 route,其他設定不變

routes:
  - id: service_customer
    uri: lb://CONSUMER
    order: 0
    predicates:
      - Path=/customer/**
    filters:
      - StripPrefix=1
      - AddResponseHeader=X-Response-Default-Foo, Default-Bar

新增的StripPrefix可以接受一個非負整數,對應的具體實現是StripPrefixGatewayFilterFactory,從名字就可以看出它的作用是去掉字首的,那個整數即對應層數。具體到本例中,我們通過 Spring Cloud Gateway 存取 /customer/hello/yibo,那麼當閘道器服務向後轉發請求時,會去掉/customer,微服務收到的就是/hello/yujian

我們現在存取 http://localhost:20000/customer/hello/cloud 可以看到能正常返回資料並且響應頭也加上了。這時候 http://localhost:20000/EUREKA-CONSUMER/hello/cloud 雖然依舊能正常返回資料,但是並沒有我們自定義的響應頭。

在這裡插入圖片描述

Spring Cloud Gateway 也支援通過 Java 的流式 API 進行路由的定義,如下就是一個和上邊通過組態檔設定的等效的路由,並且可以和組態檔搭配使用。

@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
    // @formatter:off
    return builder.routes()
            .route(r -> r.path("/fluent/customer/**")
                         .filters(f -> f.stripPrefix(2)
                                        .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
                         .uri("lb://CONSUMER")
                         .order(0)
                         .id("fluent_customer_service")
            )
            .build();
    // @formatter:on
}

總結

本文我們簡單瞭解了 Spring Cloud Gateway,並用它實現了一個簡單的閘道器服務。既介紹了通過結合註冊中心 Eureka 來為微服務提供預設的路由,也介紹瞭如何通過組態檔和 API 去自定義路由,相信大家對 Spring Cloud Gateway 已經有了個初步的認識。後面的文章我們也會繼續去發現 Spring Cloud Gateway 更多強大的功能。