建立閘道

本指南將引導您如何使用 Spring Cloud Gateway

您將建立什麼

您將使用 Spring Cloud Gateway 建立一個閘道。

您需要什麼

  • 大約 15 分鐘

  • 您最喜歡的文字編輯器或 IDE

  • Java 17+

如何完成本指南

與大多數 Spring 入門指南 一樣,您可以從頭開始並完成每個步驟,或者您可以跳過您已經熟悉的基礎設定步驟。 無論哪種方式,您最終都會得到可運作的程式碼。

若要從頭開始,請前往 從 Spring Initializr 開始

若要跳過基礎,請執行以下操作

當您完成時,您可以根據 gs-gateway/complete 中的程式碼檢查您的結果。

從 Spring Initializr 開始

您可以使用這個 預先初始化的專案,然後按一下產生以下載 ZIP 檔案。 此專案已設定為符合本教學課程中的範例。

若要手動初始化專案

  1. 瀏覽至 https://start.spring.io。 此服務會提取應用程式所需的所有相依性,並為您完成大部分的設定。

  2. 選擇 Gradle 或 Maven 以及您想要使用的語言。 本指南假設您選擇了 Java。

  3. 按一下Dependencies,然後選取 Reactive GatewayResilience4JContract Stub Runner

  4. 按一下Generate

  5. 下載產生的 ZIP 檔案,該檔案是使用您的選擇設定的 Web 應用程式的封存檔。

如果您的 IDE 具有 Spring Initializr 整合,您可以從您的 IDE 完成此程序。
您也可以從 Github 分支專案,並在您的 IDE 或其他編輯器中開啟它。

建立簡單路由

Spring Cloud Gateway 使用路由來處理對下游服務的請求。 在本指南中,我們會將所有請求路由到 HTTPBin。 路由可以使用多種方式配置,但是,對於本指南,我們使用 Gateway 提供的 Java API。

若要開始,請在 Application.java 中建立一個類型為 RouteLocator 的新 Bean

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes().build();
}

myRoutes 方法採用一個 RouteLocatorBuilder,可用於建立路由。 除了建立路由之外,RouteLocatorBuilder 還允許您將謂詞和篩選器新增到您的路由,以便您可以根據某些條件路由處理,並根據需要更改請求/回應。

現在我們可以建立一個路由,當向 Gateway 發出 /get 的請求時,該路由會將請求路由到 https://httpbin.org/get。 在此路由的配置中,我們新增一個篩選器,該篩選器將 Hello 請求標頭新增到值為 World 的請求,然後再路由

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .build();
}

若要測試我們的簡單 Gateway,我們可以在 port 8080 上執行 Application.java。 應用程式執行後,向 https://127.0.0.1:8080/get 發出請求。 您可以使用終端機中的以下 cURL 命令來執行此操作

$ curl https://127.0.0.1:8080/get

您應該收到一個看起來類似於以下輸出的回應

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Connection": "close",
    "Forwarded": "proto=http;host=\"localhost:8080\";for=\"0:0:0:0:0:0:0:1:56207\"",
    "Hello": "World",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0",
    "X-Forwarded-Host": "localhost:8080"
  },
  "origin": "0:0:0:0:0:0:0:1, 73.68.251.70",
  "url": "https://127.0.0.1:8080/get"
}

請注意,HTTPBin 顯示在請求中傳送了具有值為 WorldHello 標頭。

使用 Spring Cloud CircuitBreaker

現在我們可以做一些更有趣的事情。 由於 Gateway 背後的服務可能會表現不佳並影響我們的客戶端,因此我們可能希望將我們建立的路由包裝在斷路器中。 您可以使用 Resilience4J Spring Cloud CircuitBreaker 實作在 Spring Cloud Gateway 中執行此操作。 這是通過您可以新增到您的請求的簡單篩選器來實作的。 我們可以建立另一個路由來演示這一點。

在下一個範例中,我們使用 HTTPBin 的延遲 API,該 API 在傳送回應之前會等待一定的秒數。 由於此 API 可能需要很長時間才能傳送其回應,因此我們可以將使用此 API 的路由包裝在斷路器中。 以下清單將新路由新增到我們的 RouteLocator 物件

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.circuitbreaker.com")
            .filters(f -> f.circuitBreaker(config -> config.setName("mycmd")))
            .uri("http://httpbin.org:80")).
        build();
}

此新路由配置與我們先前建立的路由配置之間存在一些差異。 首先,我們使用主機謂詞而不是路徑謂詞。 這表示,只要主機是 circuitbreaker.com,我們就會將請求路由到 HTTPBin,並將該請求包裝在斷路器中。 我們通過將篩選器應用於路由來做到這一點。 我們可以使用配置物件來配置斷路器篩選器。 在本範例中,我們為斷路器提供一個名稱 mycmd

現在我們可以測試這個新路由。 若要執行此操作,我們需要啟動應用程式,但是,這次,我們將向 /delay/3 發出請求。 重要的是,我們還包括一個具有 circuitbreaker.com 主機的 Host 標頭。 否則,該請求不會被路由。 我們可以使用以下 cURL 命令

$ curl --dump-header - --header 'Host: www.circuitbreaker.com' https://127.0.0.1:8080/delay/3
我們使用 --dump-header 來查看回應標頭。 --dump-header 後面的 - 告訴 cURL 將標頭列印到 stdout。

在使用此命令後,您應該在終端機中看到以下內容

HTTP/1.1 504 Gateway Timeout
content-length: 0

如您所見,斷路器在等待 HTTPBin 的回應時超時。 當斷路器超時時,我們可以選擇性地提供一個 fallback,以便客戶端不會收到 504,而是收到更有意義的東西。 在生產情境中,您可以從快取中傳回一些資料,例如,但在我們的簡單範例中,我們會傳回一個主體為 fallback 的回應。

若要執行此操作,我們可以修改我們的斷路器篩選器,以提供在超時情況下要呼叫的 URL

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.circuitbreaker.com")
            .filters(f -> f.circuitBreaker(config -> config
                .setName("mycmd")
                .setFallbackUri("forward:/fallback")))
            .uri("http://httpbin.org:80"))
        .build();
}

現在,當斷路器包裝的路由超時時,它會在 Gateway 應用程式中呼叫 /fallback。 現在我們可以將 /fallback 端點新增到我們的應用程式。

Application.java 中,我們新增 @RestController 類別層級註釋,然後將以下 @RequestMapping 新增到類別

src/main/java/gateway/Application.java

@RequestMapping("/fallback")
public Mono<String> fallback() {
  return Mono.just("fallback");
}

若要測試此新的 fallback 功能,請重新啟動應用程式,然後再次發出以下 cURL 命令

$ curl --dump-header - --header 'Host: www.circuitbreaker.com' https://127.0.0.1:8080/delay/3

有了 fallback,我們現在看到我們從 Gateway 收到一個 200,回應主體為 fallback

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: text/plain;charset=UTF-8

fallback

編寫測試

作為一個好的開發人員,我們應該編寫一些測試,以確保我們的 Gateway 正在執行我們期望它應該執行的操作。 在大多數情況下,我們希望限制我們對外部資源的相依性,尤其是在單元測試中,因此我們不應相依於 HTTPBin。 此問題的一種解決方案是使我們路由中的 URI 可配置,因此如果我們需要,我們可以更改 URI。

若要執行此操作,我們可以在 Application.java 中建立一個名為 UriConfiguration 的新類別

@ConfigurationProperties
class UriConfiguration {
  
  private String httpbin = "http://httpbin.org:80";

  public String getHttpbin() {
    return httpbin;
  }

  public void setHttpbin(String httpbin) {
    this.httpbin = httpbin;
  }
}

若要啟用 ConfigurationProperties,我們還需要將類別層級註釋新增到 Application.java

@EnableConfigurationProperties(UriConfiguration.class)

有了新的配置類別,我們可以在 myRoutes 方法中使用它

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder, UriConfiguration uriConfiguration) {
  String httpUri = uriConfiguration.getHttpbin();
  return builder.routes()
    .route(p -> p
      .path("/get")
      .filters(f -> f.addRequestHeader("Hello", "World"))
      .uri(httpUri))
    .route(p -> p
      .host("*.circuitbreaker.com")
      .filters(f -> f
        .circuitBreaker(config -> config
          .setName("mycmd")
          .setFallbackUri("forward:/fallback")))
      .uri(httpUri))
    .build();
}

我們不是將 URL 硬編碼到 HTTPBin,而是從我們新的配置類別取得 URL。

以下清單顯示了 Application.java 的完整內容

src/main/java/gateway/Application.java

@SpringBootApplication
@EnableConfigurationProperties(UriConfiguration.class)
@RestController
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  @Bean
  public RouteLocator myRoutes(RouteLocatorBuilder builder, UriConfiguration uriConfiguration) {
    String httpUri = uriConfiguration.getHttpbin();
    return builder.routes()
      .route(p -> p
        .path("/get")
        .filters(f -> f.addRequestHeader("Hello", "World"))
        .uri(httpUri))
      .route(p -> p
        .host("*.circuitbreaker.com")
        .filters(f -> f
          .circuitBreaker(config -> config
            .setName("mycmd")
            .setFallbackUri("forward:/fallback")))
        .uri(httpUri))
      .build();
  }

  @RequestMapping("/fallback")
  public Mono<String> fallback() {
    return Mono.just("fallback");
  }
}

@ConfigurationProperties
class UriConfiguration {
  
  private String httpbin = "http://httpbin.org:80";

  public String getHttpbin() {
    return httpbin;
  }

  public void setHttpbin(String httpbin) {
    this.httpbin = httpbin;
  }
}

現在我們可以在 src/main/test/java/gateway 中建立一個名為 ApplicationTest 的新類別。 在新類別中,我們新增以下內容

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {"httpbin=https://127.0.0.1:${wiremock.server.port}"})
@AutoConfigureWireMock(port = 0)
public class ApplicationTest {

  @Autowired
  private WebTestClient webClient;

  @Test
  public void contextLoads() throws Exception {
    //Stubs
    stubFor(get(urlEqualTo("/get"))
        .willReturn(aResponse()
          .withBody("{\"headers\":{\"Hello\":\"World\"}}")
          .withHeader("Content-Type", "application/json")));
    stubFor(get(urlEqualTo("/delay/3"))
      .willReturn(aResponse()
        .withBody("no fallback")
        .withFixedDelay(3000)));

    webClient
      .get().uri("/get")
      .exchange()
      .expectStatus().isOk()
      .expectBody()
      .jsonPath("$.headers.Hello").isEqualTo("World");

    webClient
      .get().uri("/delay/3")
      .header("Host", "www.circuitbreaker.com")
      .exchange()
      .expectStatus().isOk()
      .expectBody()
      .consumeWith(
        response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
  }
}

我們的測試利用 Spring Cloud Contract 中的 WireMock 來啟動一個伺服器,該伺服器可以模擬 HTTPBin 中的 API。 首先要注意的是 @AutoConfigureWireMock(port = 0) 的使用。 此註釋在一個隨機 port 上為我們啟動 WireMock。

接下來,請注意,我們利用了我們的 UriConfiguration 類別,並在 @SpringBootTest 註釋中將 httpbin 屬性設定為在本機執行的 WireMock 伺服器。 在測試中,我們然後為我們通過 Gateway 呼叫的 HTTPBin API 設定「存根」,並模擬我們期望的行為。 最後,我們使用 WebTestClient 向 Gateway 發出請求並驗證回應。

摘要

恭喜! 您剛剛建立了您的第一個 Spring Cloud Gateway 應用程式!

想要編寫新的指南或貢獻現有的指南嗎? 請查看我們的 貢獻指南

所有指南都以程式碼的 ASLv2 授權發佈,並以寫作的 署名、禁止衍生創意共享授權 發佈。