WebFlux REST API と WebClient

このガイドでは、Spring、WebFlux を使用して "Hello, Spring!" RESTful Web サービスを作成し、そのサービスを WebClient で使用するプロセスについて説明します。

このガイドでは、Spring WebFlux の関数使用方法を示します。WebFlux でアノテーションを使用することもできます。

構築するもの

Spring Webflux とそのサービスの WebClient コンシューマーを使用して RESTfulWeb サービスを構築します。System.out と次の両方で出力を確認できます。

http://localhost:8080/hello

必要なもの

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

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

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

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

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

Spring Initializr から開始

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

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

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

  2. Gradle または Maven のいずれかと、使用する言語を選択してください。

  3. "Artifact" フォームフィールドに "reactive-web-service" と入力します。

  4. 依存関係をクリックし、Spring リアクティブ Web とリアクティブ HTTP クライアントを選択します。

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

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

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

WebFlux ハンドラーを作成する

まず、RESTful サービスによって JSON として直列化される Greeting 型から始めます。

Java
package com.example.reactivewebservice;

public record Greeting(String message) {
}
Kotlin
package com.example.reactivewebservice

data class Greeting(val message: String)

Spring のリアクティブアプローチでは、次の例に示すように、ハンドラーを使用してリクエストを処理し、レスポンスを作成します。

Java
package com.example.reactivewebservice;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Mono;

@Component
public class GreetingHandler {

  public Mono<ServerResponse> hello(ServerRequest request) {
    return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
      .body(BodyInserters.fromValue(new Greeting("Hello, Spring!")));
  }
}
Kotlin
package com.example.reactivewebservice

import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono

@Component
class GreetingHandler {

  fun hello(request: ServerRequest): Mono<ServerResponse> =
    ServerResponse.ok()
      .contentType(MediaType.APPLICATION_JSON)
      .bodyValue(Greeting("Hello, Spring!"))
}

この単純なリアクティブクラスは、常に「Hello、Spring!」の JSON 本文を返します。挨拶。データベースからのアイテムのストリーム、計算によって生成されたアイテムのストリームなど、他の多くのものを返す可能性があります。リアクティブコードに注意してください。ServerResponse 本体を保持する Mono オブジェクトです。

ルーターを作成する

このアプリケーションでは、次の例に示すように、ルーターを使用して、公開する唯一のルート(/hello)を処理します。

Java
package com.example.reactivewebservice;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;

@Configuration(proxyBeanMethods = false)
public class GreetingRouter {

  @Bean
  public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {

    return RouterFunctions.route(GET("/hello")
        .and(accept(MediaType.APPLICATION_JSON)), greetingHandler::hello);
  }
}
Kotlin
package com.example.reactivewebservice

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.router

@Configuration(proxyBeanMethods = false)
class GreetingRouter {

  @Bean
  fun route(greetingHandler: GreetingHandler): RouterFunction<ServerResponse> = router {
    (accept(MediaType.APPLICATION_JSON) and GET("/hello")).invoke(greetingHandler::hello)
  }
}

ルーターは /hello パスでトラフィックをリッスンし、リアクティブハンドラークラスによって提供された値を返します。

WebClient を作成する

Spring RestTemplate クラスは、本質的にブロッキングです。リアクティブアプリケーションでは使用したくありません。リアクティブアプリケーションの場合、Spring はノンブロッキングである WebClient クラスを提供します。WebClient ベースの実装を使用して、RESTful サービスを利用します。

Java
package com.example.reactivewebservice;

import reactor.core.publisher.Mono;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
public class GreetingClient {

  private final WebClient client;

  // Spring Boot auto-configures a WebClient.Builder instance with nice defaults and customizations.
  // We can use it to create a dedicated WebClient for our component.
  public GreetingClient(WebClient.Builder builder) {
    this.client = builder.baseUrl("http://localhost:8080").build();
  }

  public Mono<String> getMessage() {
    return this.client.get().uri("/hello").accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(Greeting.class)
        .map(Greeting::message);
  }

}
Kotlin
package com.example.reactivewebservice

import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.bodyToMono
import reactor.core.publisher.Mono

@Component
class GreetingClient(builder: WebClient.Builder) {

  // Spring Boot auto-configures a WebClient.Builder instance with nice defaults and customizations.
  // We can use it to create a dedicated WebClient for our component.
  private val client: WebClient = builder.baseUrl("http://localhost:8080").build()

  fun getMessage(): Mono<String> = client
    .get()
    .uri("/hello")
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .bodyToMono<Greeting>()
    .map { it.message }
}

WebClient クラスは、Mono の形式でリアクティブ機能を使用して、メッセージのコンテンツを保持します(getMessage メソッドによって返されます)。これは、チェーンリアクティブ演算子に対して、命令型ではなく関数 API を使用しています。

リアクティブ API に慣れるまでには時間がかかる場合がありますが、WebClient には興味深い機能があり、従来の Spring MVC アプリケーションでも使用できます。

WebClient を使用して、非リアクティブのブロッキングサービスと通信することもできます。

アプリケーションを実行可能にする

main() メソッドを使用してアプリケーションを駆動し、エンドポイントから Greeting メッセージを取得します。

Java
package com.example.reactivewebservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class ReactiveWebServiceApplication {

  public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(ReactiveWebServiceApplication.class, args);
    GreetingClient greetingClient = context.getBean(GreetingClient.class);
    // We need to block for the content here or the JVM might exit before the message is logged
    System.out.println(">> message = " + greetingClient.getMessage().block());
  }
}
Kotlin
package com.example.reactivewebservice

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class ReactiveWebServiceApplication

fun main(args: Array<String>) {
  val context = runApplication<ReactiveWebServiceApplication>(*args)
  val greetingClient = context.getBean(GreetingClient::class.java)
  // Block here to wait for the response, otherwise the JVM might exit before the message is logged
  println(">> message = ${greetingClient.getMessage().block()}")
}

@SpringBootApplication は、次のすべてを追加する便利なアノテーションです。

  • @Configuration: アプリケーションコンテキストの Bean 定義のソースとしてクラスにタグを付けます。

  • @EnableAutoConfiguration: クラスパス設定、他の Bean、さまざまなプロパティ設定に基づいて Bean の追加を開始するよう Spring Boot に指示します。例: spring-webmvc がクラスパスにある場合、このアノテーションはアプリケーションに Web アプリケーションとしてフラグを立て、DispatcherServlet のセットアップなどの主要な動作をアクティブにします。

  • @ComponentScan: Spring に、hello パッケージ内の他のコンポーネント、構成、サービスを探して、コントローラーを検出させるように指示します。

main() メソッドは、Spring Boot の SpringApplication.run() メソッドを使用してアプリケーションを起動します。XML が 1 行もないことに気付きましたか? web.xml ファイルもありません。この Web アプリケーションは 100% 純粋な Java であり、接続機能やインフラストラクチャの構成に対処する必要はありませんでした。

実行可能 JAR を構築する

コマンドラインから Gradle または Maven を使用してアプリケーションを実行できます。必要なすべての依存関係、クラス、リソースを含む単一の実行可能 JAR ファイルを構築して実行することもできます。実行可能な jar を構築すると、開発ライフサイクル全体、さまざまな環境などで、アプリケーションとしてサービスを簡単に提供、バージョン管理、デプロイできます。

Gradle を使用する場合、./gradlew bootRun を使用してアプリケーションを実行できます。または、次のように、./gradlew build を使用して JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar build/libs/gs-reactive-rest-service-0.0.1-SNAPSHOT.jar

Maven を使用する場合、./mvnw spring-boot:run を使用してアプリケーションを実行できます。または、次のように、./mvnw clean package で JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar target/gs-reactive-rest-service-0.0.1-SNAPSHOT.jar

ロギング出力が表示されます。サービスは数秒以内に起動して実行されるはずです。

サービスが開始されると、次のような行が表示されます。

>> message = Hello, Spring!

その行は、WebClient によって消費されるリアクティブコンテンツに由来しています。当然、System.out に出力するよりも、出力で行う方が面白いものを見つけることができます。

アプリケーションをテストする

アプリケーションが実行されているため、テストできます。まず、ブラウザーを開いて http://localhost:8080/hello に移動し、「こんにちは、Spring!」と表示します。このガイドでは、WebTestClient クラスのテストを開始するためのテストクラスも作成しました。

Java
package com.example.reactivewebservice;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

import static org.assertj.core.api.Assertions.assertThat;

//  We create a @SpringBootTest, starting an actual server on a RANDOM_PORT
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
public class GreetingRouterTest {

  // Spring Boot will create a WebTestClient for you,
  // already configure and ready to issue requests against "localhost:RANDOM_PORT"
  @Autowired
  private WebTestClient webTestClient;

  @Test
  public void testHello() {
    webTestClient
      // Create a GET request to test an endpoint
      .get().uri("/hello")
      .accept(MediaType.APPLICATION_JSON)
      .exchange()
      // and use the dedicated DSL to test assertions against the response
      .expectStatus().isOk()
      .expectBody(Greeting.class).value(greeting -> {
        assertThat(greeting.message()).isEqualTo("Hello, Spring!");
    });
  }
}
Kotlin
package com.example.reactivewebservice

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient
import org.springframework.http.MediaType
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody

// We create a @SpringBootTest, starting an actual server on a RANDOM_PORT
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class GreetingRouterTest(@Autowired private val webTestClient: WebTestClient) {

  // Spring Boot will create a WebTestClient for you,
  // already configured and ready to issue requests against "localhost:RANDOM_PORT"
  @Test
  fun testHello() {
    webTestClient
      // Create a GET request to test an endpoint
      .get().uri("/hello")
      .accept(MediaType.APPLICATION_JSON)
      .exchange()
      // and use the dedicated DSL to test assertions against the response
      .expectStatus().isOk
      .expectBody<Greeting>()
      .isEqualTo(Greeting("Hello, Spring!"))
  }
}

要約

おめでとう! RESTful サービスを使用する WebClient を含む Reactive Spring アプリケーションを開発しました!

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

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

コードを入手する