Spring Cloud Gateway入坑記

Spring Cloud Gateway入坑記

前提

最近在作老系統的重構,重構完成後新系統中須要引入一個網關服務,做爲新系統和老系統接口的適配和代理。以前,不少網關應用使用的是Spring-Cloud-Netfilx基於Zuul1.x版本實現的那套方案,可是鑑於Zuul1.x已經中止迭代,它使用的是比較傳統的阻塞(B)IO + 多線程的實現方案,其實性能不太好。後來Spring團隊乾脆本身從新研發了一套網關組件,這個就是本次要調研的Spring-Cloud-Gatewayhtml

簡介

Spring Cloud Gateway依賴於Spring Boot 2.0, Spring WebFlux,和Project Reactor。許多熟悉的同步類庫(例如Spring-DataSpring-Security)和同步編程模式在Spring Cloud Gateway中並不適用,因此最好先閱讀一下上面提到的三個框架的文檔。java

Spring Cloud Gateway依賴於Spring BootSpring WebFlux提供的基於Netty的運行時環境,它並不是構建爲一個WAR包或者運行在傳統的Servlet容器中。react

專有名詞

  • 路由(Route):路由是網關的基本組件。它由ID,目標URI,謂詞(Predicate)集合和過濾器集合定義。若是謂詞聚合判斷爲真,則匹配路由。
  • 謂詞(Predicate):使用的是Java8中基於函數式編程引入的java.util.Predicate。使用謂詞(聚合)判斷的時候,輸入的參數是ServerWebExchange類型,它容許開發者匹配來自HTTP請求的任意參數,例如HTTP請求頭、HTTP請求參數等等。
  • 過濾器(Filter):使用的是指定的GatewayFilter工廠所建立出來的GatewayFilter實例,能夠在發送請求到下游以前或者以後修改請求(參數)或者響應(參數)。

其實Filter還包括了GlobalFilter,不過在官方文檔中沒有提到。git

工做原理

s-c-g-e-1.png

客戶端向Spring Cloud Gateway發出請求,若是Gateway Handler Mapping模塊處理當前請求若是匹配到一個目標路由配置,該請求就會轉發到Gateway Web Handler模塊。Gateway Web Handler模塊在發送請求的時候,會把該請求經過一個匹配於該請求的過濾器鏈。上圖中過濾器被虛線分隔的緣由是:過濾器的處理邏輯能夠在代理請求發送以前或者以後執行。全部pre類型的過濾器執行以後,代理請求才會建立(和發送),當代理請求建立(和發送)完成以後,全部的post類型的過濾器纔會執行。github

見上圖,外部請求進來後若是落入過濾器鏈,那麼虛線左邊的就是pre類型的過濾器,請求先通過pre類型的過濾器,再發送到目標被代理的服務。目標被代理的服務響應請求,響應會再次通過濾器鏈,也就是走虛線右側的過濾器鏈,這些過濾器就是post類型的過濾器。web

注意,若是在路由配置中沒有明確指定對應的路由端口,那麼會使用以下的默認端口:正則表達式

  • HTTP協議,使用80端口。
  • HTTPS協議,使用443端口。

引入依賴

建議直接經過Train版本(其實筆者考究過,Train版本的代號實際上是倫敦地鐵站的命名,像當前的Spring Cloud最新版本是Greenwich.SR1Greenwich能夠在倫敦地鐵站的地圖查到這個站點,對應的SpringBoot版本是2.1.x)引入Spring-Cloud-Gateway,由於這樣能夠跟上最新穩定版本的Spring-Cloud版本,另外因爲Spring-Cloud-Gateway基於Netty的運行時環境啓動,不須要引入帶Servlet容器的spring-boot-starter-webspring

父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引入下面的配置:json

<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提供的內置的路由謂詞工廠以下:

s-c-g-e-2.png

指定日期時間規則路由謂詞

按照配置的日期時間指定的路由謂詞有三種可選規則:

  • 匹配請求在指定日期時間以前。
  • 匹配請求在指定日期時間以後。
  • 匹配請求在指定日期時間之間。

值得注意的是,配置的日期時間必須知足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

//響應結果
Doge
複製代碼

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

//響應結果
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

//響應結果
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工廠的內建實現以下:

ID 類名 類型 功能
StripPrefix StripPrefixGatewayFilterFactory pre 移除請求URL路徑的第一部分,例如原始請求路徑是/order/query,處理後是/query
SetStatus SetStatusGatewayFilterFactory post 設置請求響應的狀態碼,會從org.springframework.http.HttpStatus中解析
SetResponseHeader SetResponseHeaderGatewayFilterFactory post 設置(添加)請求響應的響應頭
SetRequestHeader SetRequestHeaderGatewayFilterFactory pre 設置(添加)請求頭
SetPath SetPathGatewayFilterFactory pre 設置(覆蓋)請求路徑
SecureHeader SecureHeadersGatewayFilterFactory pre 設置安全相關的請求頭,見SecureHeadersProperties
SaveSession SaveSessionGatewayFilterFactory pre 保存WebSession
RewriteResponseHeader RewriteResponseHeaderGatewayFilterFactory post 從新響應頭
RewritePath RewritePathGatewayFilterFactory pre 重寫請求路徑
Retry RetryGatewayFilterFactory pre 基於條件對請求進行重試
RequestSize RequestSizeGatewayFilterFactory pre 限制請求的大小,單位是byte,超過設定值返回413 Payload Too Large
RequestRateLimiter RequestRateLimiterGatewayFilterFactory pre 限流
RequestHeaderToRequestUri RequestHeaderToRequestUriGatewayFilterFactory pre 經過請求頭的值改變請求URL
RemoveResponseHeader RemoveResponseHeaderGatewayFilterFactory post 移除配置的響應頭
RemoveRequestHeader RemoveRequestHeaderGatewayFilterFactory pre 移除配置的請求頭
RedirectTo RedirectToGatewayFilterFactory pre 重定向,須要指定HTTP狀態碼和重定向URL
PreserveHostHeader PreserveHostHeaderGatewayFilterFactory pre 設置請求攜帶的屬性preserveHostHeader爲true
PrefixPath PrefixPathGatewayFilterFactory pre 請求路徑添加前置路徑
Hystrix HystrixGatewayFilterFactory pre 整合Hystrix
FallbackHeaders FallbackHeadersGatewayFilterFactory pre Hystrix執行若是命中降級邏輯容許經過請求頭攜帶異常明細信息
AddResponseHeader AddResponseHeaderGatewayFilterFactory post 添加響應頭
AddRequestParameter AddRequestParameterGatewayFilterFactory pre 添加請求參數,僅僅限於URL的Query參數
AddRequestHeader AddRequestHeaderGatewayFilterFactory pre 添加請求頭

GatewayFilter工廠使用的時候須要知道其ID以及配置方式,配置方式能夠看對應工廠類的公有靜態內部類XXXXConfig

GlobalFilter工廠

GlobalFilter的功能其實和GatewayFilter是相同的,只是GlobalFilter的做用域是全部的路由配置,而不是綁定在指定的路由配置上。多個GlobalFilter能夠經過@Order或者getOrder()方法指定每一個GlobalFilter的執行順序,order值越小,GlobalFilter執行的優先級越高。

注意,因爲過濾器有pre和post兩種類型,pre類型過濾器若是order值越小,那麼它就應該在pre過濾器鏈的頂層,post類型過濾器若是order值越小,那麼它就應該在pre過濾器鏈的底層。示意圖以下:

s-c-g-e-3.png

例如要實現負載均衡的功能,application.yml配置以下:

spring:
 cloud:
 gateway:
 routes:
 - id: myRoute
 uri: lb://myservice   # <-------- lb特殊標記會使用LoadBalancerClient搜索目標服務進行負載均衡
 predicates:
 - Path=/service/**
複製代碼

目前Spring Cloud Gateway提供的內建的GlobalFilter以下:

類名 功能
ForwardRoutingFilter 重定向
LoadBalancerClientFilter 負載均衡
NettyRoutingFilter Netty的HTTP客戶端的路由
NettyWriteResponseFilter Netty響應進行寫操做
RouteToRequestUrlFilter 基於路由配置更新URL
WebsocketRoutingFilter Websocket請求轉發到下游

內建的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
複製代碼

目前支持的端點列表:

ID 請求路徑 HTTP方法 描述
globalfilters /actuator/gateway/globalfilters GET 展現路由配置中的GlobalFilter列表
routefilters /actuator/gateway/routefilters GET 展現綁定到對應路由配置的GatewayFilter列表
refresh /actuator/gateway/refresh POST 清空路由配置緩存
routes /actuator/gateway/routes GET 展現已經定義的路由配置列表
routes/{id} /actuator/gateway/routes/{id} GET 展現對應ID已經定義的路由配置
routes/{id} /actuator/gateway/routes/{id} POST 添加一個新的路由配置
routes/{id} /actuator/gateway/routes/{id} DELETE 刪除指定ID的路由配置

其中/actuator/gateway/routes/{id}添加一個新的路由配置請求參數的格式以下:

{
  "id": "first_route",
  "predicates": [{
    "name": "Path",
    "args": {"doge":"/throwable"}
  }],
  "filters": [],
  "uri": "https://www.throwable.club",
  "order": 0
}
複製代碼

小結

筆者雖然是一個底層的碼畜,可是好久以前就向身邊的朋友說:

反應式編程結合同步非阻塞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不只僅是爲了使用新的技術,更重要的是它的性能有了不俗的提高,基準測試項目spring-cloud-gateway-bench的結果以下:

代理組件(Proxy) 平均交互延遲(Avg Latency) 平均每秒處理的請求數(Avg Requests/Sec)
Spring Cloud Gateway 6.61ms 32213.38
Linkered 7.62ms 28050.76
Zuul(1.x) 12.56ms 20800.13
None(直接調用) 2.09ms 116841.15

原文連接

(本文完 c-3-d e-a-20190504)

相關文章
相關標籤/搜索