Spring 5の機能的Webフレームワークの紹介

Spring 5のFunctional Web Frameworkの概要

1. 前書き

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

このチュートリアルでは、このフレームワークを実際に使用する方法を学習します。

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

2. メーベン依存

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


    org.springframework.boot
    spring-boot-starter-webflux
    2.0.3.RELEASE

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

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

アノテーションマッピングを使用するアノテーションベースのモデルとは対照的に、ここではHandlerFunctionRouterFunctionsを使用します。

また、機能的なWebフレームワークは、注釈ベースのリアクティブフレームワークが構築された同じリアクティブスタック上に構築されます。

3.1. HandlerFunction

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

@FunctionalInterface
public interface HandlerFunction {
    Mono handle(ServerRequest request);
}

このインターフェースは主にFunction<Request, Response<T>>であり、サーブレットのように動作します。

ただし、標準のサーブレットServlet.service(ServletRequest req, ServletResponse res)と比較すると、HandlerFunctionは応答をパラメーターとして受け取るのではなく返します。これにより、副作用がなくなり、テストと再利用が容易になります。

3.2. RouterFunction

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

@FunctionalInterface
public interface RouterFunction {
    Mono> route(ServerRequest request);
    // ...
}

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

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

public static  RouterFunction route(
  RequestPredicate predicate,
  HandlerFunction handlerFunction)

RouterFunctionを返すことにより、route()をチェーンおよびネストして、強力で複雑なルーティングスキームを構築できます。

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

guide to Spring WebFlux tutorialでは、注釈付きの@RestController WebClient. を使用して小さなEmployeeManagement RESTアプリケーションを作成します

それでは、Router関数とHandler関数を使用して同じアプリケーションを作成しましょう。

まず、we’ll create routes using RouterFunction to publish and consume our reactive streams of Employee. RoutesはSpring Beanとして登録され、Spring構成として使用される任意のクラス内に作成できます。

4.1. 単一のリソース

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

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

それを分解するために、最初の引数は、handler.を呼び出すHTTPGETリクエストを定義します。 パスが/employee/{id} で、リクエストのタイプがGETの場合にのみ、一致したEmployeeemployeeRepository から返します。

4.2. 収集リソース

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

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

4.3. 単一リソースの更新

最後に、Employee resourceを更新するためのルートを追加しましょう。

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

5. ルートを作成する

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

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

@Bean
RouterFunction 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 アプリケーションに必要なRouterHandler を使用してすべてのRESTAPIを作成しました。 アプリケーションを実行するには、異なるルートを使用するか、上記で作成した単一のルートを使用します。

6. ルートのテスト

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

WebTestClient を使用してルートをテストするには、bindToRouterFunction を使用してルートをバインドし、テストクライアントインスタンスを構築する必要があります。

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

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

    Employee expected = 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(expected);
}

同様に、getAllEmployeesRoute

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

    List employeeList = new ArrayList<>();

    Employee employee1 = new Employee("1", "Employee 1");
    Employee employee2 = new Employee("2", "Employee 2");

    employeeList.add(employee1);
    employeeList.add(employee2);

    Flux employeeFlux = Flux.fromIterable(employeeList);
    given(employeeRepository.findAllEmployees()).willReturn(employeeFlux);

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

更新された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を使用したテストの詳細については、working with WebClient and WebTestClientに関するチュートリアルを参照してください。

7. 概要

このチュートリアルでは、Spring 5で新しい機能的なウェブフレームワークを紹介し、その2つのコア関数Router Handler を調べ、リクエストを処理してレスポンスを送信するためのさまざまなルートを作成する方法を学びました。

また、Guide to Spring 5 WebFluxで導入されたEmployeeManagement アプリケーションを新しいフレームワークで再作成しました。

Reactorに基盤を置くと、リアクティブフレームワークは、データストアへのリアクティブアクセスで完全に輝きます。 残念ながら、MongoDBなどのいくつかのNoSQLデータベースを除いて、ほとんどのデータストアはまだそのようなリアクティブアクセスを提供していません。

いつものように、完全なソースコードはover on Githubで見つけることができます。