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 更多強大的功能,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/192916.html
標籤:java
上一篇:property內置裝飾器
