使用 Spring Cloud Gateway


在閘道方案上,Spring 5 自己推出了 Spring Cloud Gateway,支援 Java 8、Reactor API,可在 Spring Boot 2 使用,看到了 Reactor,可以理解到這個閘道方案目標之一,是能夠採用 Reactive 來實現高效率的閘道。

想要建立一個 Spring Cloud Gateway 的話,在 Spring Tool Suite 上可以選擇「Gateway」這個 Starter,為了能註冊到服務發現伺服器,也為了能開放 gateway/routes 端點,以便觀察路由資訊,就順便加入 Eureka 與 Actuator 的 Starter,也就是 build.gradle 中可以包含:

implementation('org.springframework.boot:spring-boot-starter-actuator')  
implementation('org.springframework.cloud:spring-cloud-starter-gateway')
implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')

Spring Cloud Gateway 可以從服務發現伺服器上註冊的服務 ID,自動建立路由資訊,為此,可以如下設定 bootstrap.properties:

server.port=5555

spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true

eureka.instance.preferIpAddress=true
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

management.endpoints.web.exposure.include: gateway

spring.cloud.gateway.discovery.locator.enabled 啟用了自動從服務 ID 建立路由,然而,路由的路徑對應,預設會使用大寫 ID,若想要使用小寫 ID,可將 spring.cloud.gateway.discovery.locator.lowerCaseServiceId 設為 true;在設定中也開放了 gateway 端點。

必要時,可以自行實作 RouteLocator 來自定義路由方式。

接下來啟動相關服務,並且啟動 Spring Cloud Gateway 專案,預設會跑在 Netty 上,請求 http://localhost:5555/actuator/gateway/routes 的話,就可以看到以下資訊:

[
    {
        "route_id": "CompositeDiscoveryClient_ACCTSVI",
        "route_definition": {
            "id": "CompositeDiscoveryClient_ACCTSVI",
            "predicates": [
                {
                    "name": "Path",
                    "args": {
                        "pattern": "/acctsvi/**"
                    }
                }
            ],
            "filters": [
                {
                    "name": "RewritePath",
                    "args": {
                        "regexp": "/acctsvi/(?<remaining>.*)",
                        "replacement": "/${remaining}"
                    }
                }
            ],
            "uri": "lb://ACCTSVI",
            "order": 0
        },
        "order": 0
    },
    ...
]

每個路由設定會有個 route_id 作為識別,在路由定義的 predicates 中,可以看到設定了 Path,這是 Spring Cloud Gateway 內建的斷言器工廠 Bean 名稱,pattern,這表示對於 http://localhost:5555/acctsvi/xxxx 的請求,會轉給 uri 設定的對象,lb://ACCTSVI 表示轉給服務 ID 為 ACCTSVI 的服務。

filters 中設定了 RewritePath,這是個過濾器工廠 Bean 名稱,依照 regexp 的規則,會捕捉請求中的 /acctsvi/ 之後的部份,套用至服務的 URI 上,也就是 http://localhost:5555/acctsvi/xxxx 的請求,將會轉發至 http://acctsvi-uri/xxxx

predicatesfilters 是 Spring Cloud Gateway 的重要特性,predicates 斷言哪些路徑符合路由定義,filters 設定哪些過濾器要套用在上頭,除了透過設定檔之外,必要時,都可以透過程式碼來自訂。

Spring Cloud Gateway 也內建了一些斷言器工廠過濾器工廠,這些工廠類別,是可以透過屬性檔來定義的,必要時,也可以自訂工廠類別

就以上的設定來說,請求 http://localhost:5555/acctsvi/accountByName?username=caterpillar 就可以得到以下回應:

{
    "name": "caterpillar",
    "email": "caterpillar@openhome.cc",
    "password": "$2a$10$CEkPOmd.Uid2FpIOHA6Cme1G.mvhWfelv2hPu7cxZ/vq2drnXaVo.",
    "_links": {
        "self": {
            "href": "http://Justin-2017:8084/accountByNameEmail?username=caterpillar"
        }
    }
}

如果想要自訂路由,可以寫個 application.yml(若不想自動建立路由,可以將 spring.cloud.gateway.discovery.locator.enabledspring.cloud.gateway.discovery.locator.lowerCaseServiceId 註解掉):

spring:
    application:
            name: gateway
    cloud:
        gateway:
            routes: 
                - predicates:
                    - Path=/acct/**
                  filters:
                      - StripPrefix=1
                  uri: lb://acctsvi
                - predicates:
                    - Path=/msg/**
                  filters:
                      - StripPrefix=1
                  uri: lb://msgsvi     
                - predicates:
                    - Path=/email/**
                  filters:
                      - StripPrefix=1
                  uri: lb://email                                                                                                           

StripPrefix 也是內建的過濾器工廠 Bean 名稱,設定值為 1 表示將路徑中的第一個階層去除,其餘保留用來轉發請求,請求 http://localhost:5555/actuator/gateway/routes 的話,就可以看到以下資訊:

[
    {
        "route_id": "545d278b-192b-4370-8156-161815957f91",
        "route_definition": {
            "id": "545d278b-192b-4370-8156-161815957f91",
            "predicates": [
                {
                    "name": "Path",
                    "args": {
                        "_genkey_0": "/acct/**"
                    }
                }
            ],
            "filters": [
                {
                    "name": "StripPrefix",
                    "args": {
                        "_genkey_0": "1"
                    }
                }
            ],
            "uri": "lb://acctsvi",
            "order": 0
        },
        "order": 0
    },
    ...
]

也就是對 http://localhost:5555/acct/accountByName?username=caterpillar 的請求,會轉給 http://acctsvi-url/accountByName?username=caterpillar

如果想要設定 api 前置路徑,就是修改一下 StripPrefix=1StripPrefix=2

spring:
    application:
            name: gateway
    cloud:
        gateway:
            default-filters:
                - StripPrefix=2
            routes: 
                - predicates:
                    - Path=/api/acct/**
                  uri: lb://acctsvi
                - predicates:
                    - Path=/api/msg/**
                  uri: lb://msgsvi     
                - predicates:
                    - Path=/api/email/**
                  uri: lb://email               

對於每個路由都要套用的過濾器,可以使用 default-filters 來設置,就以上設定來說,可以請求 http://localhost:5555/api/acct/accountByName?username=caterpillar 來取得使用者資訊。

一開始自動根據服務 ID 建立路由時,可以看到 RewritePath,它也是內建的過濾器工廠,可以運用規則表示式來進行路徑重寫,因此,也可以這麼設置 api 前置:

spring:
    application:
            name: gateway
    cloud:
        gateway:
            default-filters:
                - RewritePath=/api/.*?/(?<remaining>.*), /$\{remaining}
            routes: 
                - predicates:
                    - Path=/api/acct/**
                  uri: lb://acctsvi
                - predicates:
                    - Path=/api/msg/**
                  uri: lb://msgsvi     
                - predicates:
                    - Path=/api/email/**
                  uri: lb://email                                 

就目前的設定來說,在客戶端的部份,〈使用 Zuul〉中的 gossip 就可以了,畢竟溝通的介面沒有改變,因為 spring.application.gateway 設為 gateway,記得改一下 @FeignClient 中設定的服務 ID 為 gateway

你可以在 Gateway 中找到以上的範例專案。