Spring Cloud Gateway の構築

このガイドでは、Spring Cloud Gateway の使い方について説明します。

構築するもの

Spring Cloud Gateway を使用してゲートウェイを構築します。

必要なもの

本ガイドの完成までの流れ

ほとんどの Spring 入門ガイドと同様に、最初から始めて各ステップを完了するか、すでに慣れている場合は基本的なセットアップステップをバイパスできます。いずれにしても、最終的に動作するコードになります。

最初から始めるには、Spring Initializr から開始に進みます。

基本スキップするには、次の手順を実行します。

完了したときは、gs-gateway/complete のコードに対して結果を確認できます。

Spring Initializr から開始

IDE を使用する場合はプロジェクト作成ウィザードを使用します。IDE を使用せずにコマンドラインなどで開発する場合は、この事前に初期化されたプロジェクトからプロジェクトを ZIP ファイルとしてダウンロードできます。このプロジェクトは、このチュートリアルの例に合うように構成されています。

プロジェクトを手動で初期化するには:

  1. IDE のメニューまたはブラウザーから Spring Initializr を開きます。アプリケーションに必要なすべての依存関係を取り込み、ほとんどのセットアップを行います。

  2. Gradle または Maven のいずれかと、使用する言語を選択します。このガイドは、Java を選択したことを前提としています。

  3. 依存関係をクリックし、リアクティブゲートウェイResilience4J を選択します。

  4. 生成をクリックします。

  5. 結果の ZIP ファイルをダウンロードします。これは、選択して構成された Web アプリケーションのアーカイブです。

EclipseIntelliJ のような IDE は新規プロジェクト作成ウィザードから Spring Initializr の機能が使用できるため、手動での ZIP ファイルのダウンロードやインポートは不要です。
プロジェクトを GitHub からフォークして、IDE または他のエディターで開くこともできます。

単純なルートを作成する

Spring Cloud Gateway は、ルートを使用してダウンストリームサービスへのリクエストを処理します。このガイドでは、すべてのリクエストを HTTPBin (英語) にルーティングします。ルートはさまざまな方法で構成できますが、このガイドでは、ゲートウェイが提供する JavaAPI を使用します。

開始するには、Application.java に型 RouteLocator の新しい Bean を作成します。

src/main/java/gateway/Application.java

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

myRoutes メソッドは、ルート作成に使用できる RouteLocatorBuilder を受け取ります。RouteLocatorBuilder では、ルートの作成に加えて、述語とフィルターをルートに追加できるため、特定の条件に基づいてトラフィックをルーティングしたり、必要に応じてリクエスト / レスポンスを変更したりできます。

これで、/get のゲートウェイにリクエストが送信されたときに、リクエストを https://httpbin.org/get (英語)  にルーティングするルートを作成できます。このルートの構成では、World の値を持つ Hello リクエストヘッダーを、ルーティングされる前にリクエストに追加するフィルターを追加します。

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 を実行できます。アプリケーションが実行されたら、http://localhost:8080/get にリクエストを送信します。これを行うには、ターミナルで次の cURL コマンドを使用します。

$ curl http://localhost: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": "http://httpbin.org/get"
}

HTTPBin は、World の値を持つ Hello ヘッダーがリクエストで送信されたことを示していることに注意してください。

Spring Cloud の使用 CircuitBreaker

これで、もう少し面白いことができます。ゲートウェイの背後にあるサービスは、動作が悪く、クライアントに影響を与える可能性があるため、作成したルートをサーキットブレーカーでラップすることをお勧めします。Resilience4J Spring Cloud CircuitBreaker 実装を使用して、Spring Cloud Gateway でこれを行うことができます。これは、リクエストに追加できる単純なフィルターを介して実装されます。これを実証するために別のルートを作成できます。

次の例では、HTTPBin の delay 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();
}

この新しいルート構成と以前に作成したルート構成には、いくつかの違いがあります。1 つは、パス述語の代わりにホスト述語を使用することです。これは、ホストが circuitbreaker.com である限り、リクエストを HTTPBin にルーティングし、そのリクエストをサーキットブレーカーでラップすることを意味します。これを行うには、ルートにフィルターを適用します。構成オブジェクトを使用して、サーキットブレーカーフィルターを構成できます。この例では、サーキットブレーカーに mycmd という名前を付けます。

これで、この新しいルートをテストできます。そのためには、アプリケーションを起動する必要がありますが、今回は /delay/3 にリクエストを送信します。circuitbreaker.com のホストを持つ Host ヘッダーを含めることも重要です。それ以外の場合、リクエストはルーティングされません。次の cURL コマンドを使用できます。

$ curl --dump-header - --header 'Host: www.circuitbreaker.com' http://localhost:8080/delay/3
--dump-header を使用してレスポンスヘッダーを確認します。--dump-header の後の - は、ヘッダーを stdout に出力するように cURL に指示します。

このコマンドを使用すると、ターミナルに次のように表示されます。

HTTP/1.1 504 Gateway Timeout

...

ご覧のとおり、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();
}

これで、サーキットブレーカーでラップされたルートがタイムアウトすると、ゲートウェイアプリケーションで /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' http://localhost:8080/delay/3

フォールバックを設定すると、fallback のレスポンス本文と共に 200 がゲートウェイから返されることがわかります。

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

fallback

テストの作成

優れた開発者として、ゲートウェイが期待どおりに動作していることを確認するためのテストを作成する必要があります。ほとんどの場合、特に単体テストでは、外部リソースへの依存関係を制限したいため、HTTPBin に依存しないでください。この問題の 1 つの解決策は、ルートの 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;
  }
}

次に、テスト依存関係として WireMock を追加し、テスト中に HTTPBin の代わりとして使用できるようにします。

Maven:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux-test</artifactId>
			<scope>test</scope>
		</dependency>	
		<dependency>
			<groupId>org.wiremock.integrations</groupId>
			<artifactId>wiremock-spring-boot</artifactId>
			<version>4.0.9</version>
			<scope>test</scope>
		</dependency>

Gradle:

	testImplementation 'org.springframework.boot:spring-boot-starter-webflux-test'
	testImplementation 'org.wiremock.integrations:wiremock-spring-boot:4.0.9'

これで、src/test/java/gateway に ApplicationTest という新しいクラスを作成できます。新しいクラスでは、次のコンテンツを追加します。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
@EnableWireMock({
  @ConfigureWireMock(name = "httpbin", baseUrlProperties = "httpbin")
})
public class ApplicationTest {

  @InjectWireMock("httpbin")
  private WireMockServer httpbin;

  @LocalServerPort
  private int port;

  @Autowired
  private WebTestClient webClient;

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

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

@EnableWireMock は、Spring コンテキストがロードされる前に WireMock を開始します。@ConfigureWireMock は、モックサーバーのベース URL を httpbin プロパティにマッピングして、UriConfiguration と一致するようにします。@InjectWireMock("httpbin") は、テストメソッドにスタブを登録するためのハンドルを提供します。

このテストでは、@AutoConfigureWebTestClient と注入された WebTestClient を使用して、実行中のゲートウェイを介してトラフィックを送信します。

要約

おめでとう! これで、最初の Spring Cloud Gateway アプリケーションが作成されました。

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください [GitHub] (英語)

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の帰属表示、NoDerivatives クリエイティブコモンズライセンス (英語) でリリースされています。

コードを入手する

プロジェクト