構建閘道器

本指南將引導您如何使用 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. 單擊生成

  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。在此路由的配置中,我們添加了一個過濾器,在路由之前將 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();
}

為了測試我們的簡單閘道器,我們可以在埠 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

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

在下一個示例中,我們使用 HTTPBin 的延遲 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 發出請求。同樣重要的是,我們要包含一個 Host 頭,其主機為 circuitbreaker.com。否則,請求將不會被路由。我們可以使用以下 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 響應時超時。當斷路器超時時,我們可以選擇提供一個回退,這樣客戶端就不會收到 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

編寫測試

作為一名優秀的開發人員,我們應該編寫一些測試來確保我們的閘道器按預期工作。在大多數情況下,我們希望限制對外部資源的依賴,尤其是在單元測試中,所以我們不應該依賴 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/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 設定了“存根”,並模擬了我們期望的行為。最後,我們使用 WebTestClient 向 Gateway 發出請求並驗證響應。

總結

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

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

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