構建閘道器

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

您將構建什麼

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

您需要什麼

  • 大約 15 分鐘

  • 一個喜歡的文字編輯器或 IDE

  • Java 17+

如何完成本指南

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

從頭開始,請繼續閱讀 從 Spring Initializr 開始

跳過基礎知識,請執行以下操作

完成後,您可以對照 gs-gateway/complete 中的程式碼檢查您的結果。

從 Spring Initializr 開始

您可以使用這個預配置的專案,然後點選 Generate 下載 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 Fork 該專案,並在您的 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。在此路由的配置中,我們添加了一個過濾器,在轉發請求之前向請求新增一個值為 WorldHello 請求頭

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,可以在埠 8080 上執行 Application.java。應用程式執行後,向 https://:8080/get 傳送請求。您可以在終端中使用以下 cURL 命令來完成此操作

$ curl https://: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://:8080/get"
}

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

使用 Spring Cloud CircuitBreaker

現在我們可以做一些更有趣的事情。由於 Gateway 後面的服務可能會出現不良行為並影響我們的客戶端,我們可能希望將建立的路由包裝在斷路器中。您可以透過在 Spring Cloud Gateway 中使用 Resilience4J Spring Cloud CircuitBreaker 實現來做到這一點。這是透過一個簡單的過濾器實現的,您可以將其新增到您的請求中。我們可以建立另一個路由來演示這一點。

在下一個示例中,我們使用 HTTPBin 的 delay 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();
}

這個新的路由配置與我們之前建立的配置之間有一些區別。首先,我們使用 host 謂詞而不是 path 謂詞。這意味著,只要主機是 circuitbreaker.com,我們就將請求路由到 HTTPBin 並將該請求包裝在斷路器中。我們透過向路由應用過濾器來實現這一點。我們可以使用配置物件來配置斷路器過濾器。在此示例中,我們將斷路器命名為 mycmd

現在我們可以測試這個新路由了。為此,我們需要啟動應用程式,但這一次,我們將向 /delay/3 傳送請求。同樣重要的是,我們需要包含一個主機為 circuitbreaker.comHost 頭。否則,請求將不會被路由。我們可以使用以下 cURL 命令

$ curl --dump-header - --header 'Host: www.circuitbreaker.com' https://:8080/delay/3
我們使用 --dump-header 來檢視響應頭。--dump-header 後面的 - 告訴 cURL 將頭資訊列印到標準輸出。

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

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");
}

要測試這個新的回退功能,請重新啟動應用程式,然後再次發出以下 cURL 命令

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

有了回退功能,我們現在可以看到從 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();
}

我們不再硬編碼 HTTPBin 的 URL,而是從新的配置類中獲取 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/test/java/gateway 中建立一個名為 ApplicationTest 的新類。在新類中,我們新增以下內容

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {"httpbin=https://:${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)。這個註解會為我們在一個隨機埠上啟動 WireMock。

接下來,請注意我們利用了 UriConfiguration 類,並在 @SpringBootTest 註解中將 httpbin 屬性設定為本地執行的 WireMock 伺服器。在測試中,我們為透過 Gateway 呼叫的 HTTPBin API 設定“樁(stubs)”並模擬預期的行為。最後,我們使用 WebTestClient 向 Gateway 傳送請求並驗證響應。

總結

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

想編寫新的指南或貢獻現有指南?請檢視我們的貢獻指南

所有指南的程式碼均以 ASLv2 許可證釋出,文字內容以署名-禁止演繹知識共享許可證釋出。