1. 序章

Spring WebFluxは、リアクティブな原則を使用して構築された新しい機能的なWebフレームワークです。

このチュートリアルでは、実際にそれを操作する方法を学習します。

これは、既存のガイドからSpring 5WebFluxに基づいています。 そのガイドでは、アノテーションベースのコンポーネントを使用して単純なリアクティブRESTアプリケーションを作成しました。 ここでは、代わりに機能フレームワークを使用します。

2. Mavenの依存関係

前の記事で定義したものと同じspring-boot-starter-webflux依存関係が必要です。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>2.6.4</version>
</dependency>

3. 機能的なWebフレームワーク

機能的なWebフレームワークは、関数を使用してリクエストをルーティングおよび処理する新しいプログラミングモデルを導入します。

注釈マッピングを使用する注釈ベースのモデルとは対照的に、ここではHandlerFunctionおよびRouterFunction sを使用します。

同様に、注釈付きコントローラーの場合と同様に、機能エンドポイントアプローチは同じリアクティブスタック上に構築されます。

3.1. HandlerFunction

HandlerFunction は、それらにルーティングされた要求に対する応答を生成する関数を表します。

@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
    Mono<T> handle(ServerRequest request);
}

このインターフェースは主に関数 >> 、サーブレットのように動作します。

ただし、標準の Servlet#service(ServletRequest req、ServletResponse res)と比較すると、HandlerFunctionは入力パラメーターとして応答を受け取りません。

3.2. RouterFunction

RouterFunction は、@RequestMappingアノテーションの代替として機能します。 これを使用して、リクエストをハンドラー関数にルーティングできます。

@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
    Mono<HandlerFunction<T>> route(ServerRequest request);
    // ...
}

通常、完全なルーター関数を作成する代わりに、ヘルパー関数 RouterFunctions.route()をインポートしてルートを作成できます。

それは私達が適用することによって要求をルーティングすることを可能にします RequestPredicate。 述語が一致すると、2番目の引数であるハンドラー関数が返されます。

public static <T extends ServerResponse> RouterFunction<T> route(
  RequestPredicate predicate,
  HandlerFunction<T> handlerFunction)

route()メソッドは RouterFunction を返すため、これをチェーンして強力で複雑なルーティングスキームを構築できます。

4. 機能的なWebを使用したリアクティブRESTアプリケーション

以前のガイドでは、簡単なものを作成しました従業員管理を使用したRESTアプリケーション @RestController WebClient。

それでは、ルーター関数とハンドラー関数を使用して同じロジックを実装しましょう。

まず、 RouterFunctionを使用してルートを作成し、従業員のリアクティブストリームを公開および消費する必要があります。

ルートはSpringBeanとして登録され、任意の構成クラス内に作成できます。

4.1. 単一のリソース

単一のEmployeeリソースを公開するRouterFunctionを使用して最初のルートを作成しましょう。

@Bean
RouterFunction<ServerResponse> getEmployeeByIdRoute() {
  return route(GET("/employees/{id}"), 
    req -> ok().body(
      employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class));
}

最初の引数は要求述語です。 ここで、静的にインポートされたRequestPredicates.GETメソッドをどのように使用したかに注目してください。 2番目のパラメーターは、述語が適用される場合に使用されるハンドラー関数を定義します。

つまり、上記の例では、 / employees / {id}のすべてのGETリクエストをEmployeeRepository#findEmployeeById(String id)メソッドにルーティングします。

4.2. コレクションリソース

次に、コレクションリソースを公開するために、別のルートを追加しましょう。

@Bean
RouterFunction<ServerResponse> getAllEmployeesRoute() {
  return route(GET("/employees"), 
    req -> ok().body(
      employeeRepository().findAllEmployees(), Employee.class));
}

4.3. 単一リソースの更新

最後に、従業員リソースを更新するためのルートを追加しましょう。

@Bean
RouterFunction<ServerResponse> updateEmployeeRoute() {
  return route(POST("/employees/update"), 
    req -> req.body(toMono(Employee.class))
      .doOnNext(employeeRepository()::updateEmployee)
      .then(ok().build()));
}

5. ルートの構成

1つのルーター機能でルートをまとめることもできます。

上で作成したルートを組み合わせる方法を見てみましょう。

@Bean
RouterFunction<ServerResponse> composedRoutes() {
  return 
    route(GET("/employees"), 
      req -> ok().body(
        employeeRepository().findAllEmployees(), Employee.class))
        
    .and(route(GET("/employees/{id}"), 
      req -> ok().body(
        employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class)))
        
    .and(route(POST("/employees/update"), 
      req -> req.body(toMono(Employee.class))
        .doOnNext(employeeRepository()::updateEmployee)
        .then(ok().build())));
}

ここでは、 RouterFunction.and()を使用してルートを結合しました。

最後に、ルーターとハンドラーを使用して、EmployeeManagementアプリケーションに必要な完全なRESTAPIを実装しました。

アプリケーションを実行するには、個別のルートを使用するか、上記で作成した単一の構成されたルートを使用できます。

6. ルートのテスト

WebTestClientを使用してルートをテストできます。

そのためには、最初に bindToRouterFunction メソッドを使用してルートをバインドしてから、テストクライアントインスタンスを構築する必要があります。

getEmployeeByIdRouteをテストしてみましょう。

@Test
public void givenEmployeeId_whenGetEmployeeById_thenCorrectEmployee() {
    WebTestClient client = WebTestClient
      .bindToRouterFunction(config.getEmployeeByIdRoute())
      .build();

    Employee employee = new Employee("1", "Employee 1");

    given(employeeRepository.findEmployeeById("1")).willReturn(Mono.just(employee));

    client.get()
      .uri("/employees/1")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(Employee.class)
      .isEqualTo(employee);
}

同様にgetAllEmployeesRoute

@Test
public void whenGetAllEmployees_thenCorrectEmployees() {
    WebTestClient client = WebTestClient
      .bindToRouterFunction(config.getAllEmployeesRoute())
      .build();

    List<Employee> employees = Arrays.asList(
      new Employee("1", "Employee 1"),
      new Employee("2", "Employee 2"));

    Flux<Employee> employeeFlux = Flux.fromIterable(employees);
    given(employeeRepository.findAllEmployees()).willReturn(employeeFlux);

    client.get()
      .uri("/employees")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBodyList(Employee.class)
      .isEqualTo(employees);
}

EmployeeインスタンスがEmployeeRepositoryを介して更新されることを表明することにより、updateEmployeeRouteをテストすることもできます。

@Test
public void whenUpdateEmployee_thenEmployeeUpdated() {
    WebTestClient client = WebTestClient
      .bindToRouterFunction(config.updateEmployeeRoute())
      .build();

    Employee employee = new Employee("1", "Employee 1 Updated");

    client.post()
      .uri("/employees/update")
      .body(Mono.just(employee), Employee.class)
      .exchange()
      .expectStatus()
      .isOk();

    verify(employeeRepository).updateEmployee(employee);
}

WebTestClient を使用したテストの詳細については、WebClientおよびWebTestClientの操作に関するチュートリアルを参照してください。

7. 概要

このチュートリアルでは、Spring 5で新しい機能的なWebフレームワークを紹介し、その2つのコアインターフェイスであるRouterFunctionHandlerFunctionを調べました。リクエストを処理するためのさまざまなルートを作成する方法も学びました。応答を送信します。

さらに、Spring5WebFluxのガイドで紹介したEmployeeManagementアプリケーションを機能エンドポイントモデルで再作成しました。

いつものように、完全なソースコードはGithubにあります。