1. 序章

Spring5WebFluxを導入しました。これは、reactiveプログラミングモデルを使用してWebアプリケーションを構築できる新しいフレームワークです。

このチュートリアルでは、このプログラミングモデルをSpringMVCの機能コントローラーに適用する方法を説明します。

2. Mavenのセットアップ

Spring Boot を使用して、新しいAPIのデモを行います。

このフレームワークは、コントローラーを定義するおなじみの注釈ベースのアプローチをサポートします。 しかし、それはまた、コントローラーを定義する機能的な方法を提供する新しいドメイン固有言語を追加します。

Spring 5.2以降、機能的アプローチはSpringWebMVCフレームワークでも利用可能になります。WebFlux モジュールと同様に、RouterFunctionsおよびRouterFunction[ X198X]は、このAPIの主な抽象概念です。

それでは、spring-boot-starter-web依存関係をインポートすることから始めましょう。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3. RouterFunction vs @ Controller

機能領域では、Webサービスはルートと呼ばれ、@Controllerおよび@RequestMappingの従来の概念はRouterFunctionに置き換えられます

最初のサービスを作成するために、アノテーションベースのサービスを利用して、それを同等の機能に変換する方法を見てみましょう。

製品カタログ内のすべての製品を返すサービスの例を使用します。

@RestController
public class ProductController {

    @RequestMapping("/product")
    public List<Product> productListing() {
        return ps.findAll();
    }
}

それでは、同等の機能を見てみましょう。

@Bean
public RouterFunction<ServerResponse> productListing(ProductService ps) {
    return route().GET("/product", req -> ok().body(ps.findAll()))
      .build();
}

3.1. ルート定義

機能的アプローチでは、 productListing()メソッドが応答本文の代わりにRouterFunctionを返すことに注意してください。 これはルートの定義であり、リクエストの実行ではありません。

RouterFunction には、パス、要求ヘッダー、ハンドラー関数が含まれています。これらの関数は、応答本文と応答ヘッダーを生成するために使用されます。 これには、単一またはグループのWebサービスを含めることができます。

ネストされたルートについては、Webサービスのグループについて詳しく説明します。

この例では、 RouterFunctionsの静的route()メソッドを使用してRouterFunctionを作成しました。ルートのすべての要求および応答属性は、このメソッドを使用して提供できます。

3.2. 述語の要求

この例では、route()でGET()メソッドを使用して、これが GET リクエストであり、パスがString。として提供されていることを指定します。

リクエストの詳細を指定する場合は、RequestPredicateを使用することもできます。

たとえば、前の例のパスは、RequestPredicateを使用して次のように指定することもできます。

RequestPredicates.path("/product")

ここでは、静的ユーティリティRequestPredicatesを使用してRequestPredicateのオブジェクトを作成しました。

3.3. 応答

同様に、 ServerResponseには、応答オブジェクトの作成に使用される静的ユーティリティメソッドが含まれています。

この例では、 ok()を使用してHTTPステータス200を応答ヘッダーに追加してから、 body()を使用して応答本文を指定します。

さらに、 ServerResponse を使用してカスタムデータ型からの応答の構築をサポートします EntityResponse。 SpringMVCも使用できます ModelAndView 経由 RenderingResponse。

3.4. ルートの登録

次に、 @ Bean アノテーションを使用してこのルートを登録し、アプリケーションコンテキストに追加します。

@SpringBootApplication
public class SpringBootMvcFnApplication {

    @Bean
    RouterFunction<ServerResponse> productListing(ProductController pc, ProductService ps) {
        return pc.productListing(ps);
    }
}

それでは、機能的アプローチを使用してWebサービスを開発するときに遭遇するいくつかの一般的なユースケースを実装しましょう。

4. ネストされたルート

アプリケーションに多数のWebサービスがあり、それらを機能またはエンティティに基づいて論理グループに分割することは非常に一般的です。 たとえば、製品に関連するすべてのサービスを /productで始めたい場合があります。

既存のパス/product に別のパスを追加して、その名前で製品を検索してみましょう。

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route().nest(RequestPredicates.path("/product"), builder -> {
        builder.GET("/name/{name}", req -> ok().body(ps.findByName(req.pathVariable("name"))));
    }).build();
}

従来のアプローチでは、@Controllerにパスを渡すことでこれを実現していました。 ただし、 Webサービスをグループ化するための機能的な同等物は、route()のnest()メソッドです。

ここでは、新しいルートをグループ化するパス、つまり /productを指定することから始めます。 次に、前の例と同様に、ビルダーオブジェクトを使用してルートを追加します。

nest()メソッドは、ビルダーオブジェクトに追加されたルートをメインのRouterFunctionとマージします。

5.  エラー処理

もう1つの一般的な使用例は、カスタムエラー処理メカニズムを使用することです。 route()の onError()メソッドを使用して、カスタム例外ハンドラーを定義できます。

これは、アノテーションベースのアプローチで@ExceptionHandlerを使用するのと同じです。 ただし、ルートのグループごとに個別の例外ハンドラーを定義するために使用できるため、はるかに柔軟性があります。

以前に作成した製品検索ルートに例外ハンドラーを追加して、製品が見つからない場合にスローされるカスタム例外を処理してみましょう。

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route()...
      .onError(ProductService.ItemNotFoundException.class,
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.NOT_FOUND)
           .build())
      .build();
}

onError()メソッドは、 Exception クラスオブジェクトを受け入れ、機能実装からServerResponseを期待します。

ここでは、ServerResponseのサブタイプである EntityResponse を使用して、カスタムデータ型Errorから応答オブジェクトを構築しました。 次に、ステータスを追加し、 EntityResponse.build()を使用してServerResponseオブジェクトを返します。

6. フィルタ

認証を実装し、ロギングや監査などの横断的関心事を管理する一般的な方法は、フィルターを使用することです。 フィルターは、リクエストの処理を続行するか中止するかを決定するために使用されます。

カタログに商品を追加する新しいルートが必要な例を見てみましょう。

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
    return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
      .onError(IllegalArgumentException.class, 
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.BAD_REQUEST)
           .build())
        .build();
}

これは管理機能であるため、サービスを呼び出すユーザーも認証する必要があります。

これを行うには、route()に filter()メソッドを追加します。

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
   return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
     .filter((req, next) -> authenticate(req) ? next.handle(req) : 
       status(HttpStatus.UNAUTHORIZED).build())
     ....;
}

ここでは、 filter()メソッドがリクエストと次のハンドラーを提供するため、これを使用して単純な認証を行い、成功した場合に製品を保存したり、UNAUTHORIZEDを返したりします。 ]失敗した場合のクライアントへのエラー。

7. 横断的関心事

場合によっては、リクエストの前、後、または前後にいくつかのアクションを実行したいことがあります。たとえば、着信リクエストと発信応答のいくつかの属性をログに記録したい場合があります。

アプリケーションが着信要求に一致するものを見つけるたびに、ステートメントをログに記録しましょう。 route()のbefore()メソッドを使用してこれを行います:

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .before(req -> {
          LOG.info("Found a route which matches " + req.uri()
            .getPath());
          return req;
      })
      .build();
}

同様に、 route()のafter()メソッドを使用してリクエストが処理された後、簡単なログステートメントを追加できます。

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .after((req, res) -> {
          if (res.statusCode() == HttpStatus.OK) {
              LOG.info("Finished processing request " + req.uri()
                  .getPath());
          } else {
              LOG.info("There was an error while processing request" + req.uri());
          }
          return res;
      })          
      .build();
    }

8. 結論

このチュートリアルでは、コントローラーを定義するための機能的アプローチの簡単な紹介から始めました。 次に、SpringMVCアノテーションを同等の機能と比較しました。

次に、機能コントローラーを備えた製品のリストを返す単純なWebサービスを実装しました。

次に、ルートのネスト、エラー処理、アクセス制御用のフィルターの追加、ロギングなどの横断的関心事の管理など、Webサービスコントローラーの一般的なユースケースのいくつかを実装しました。

いつものように、サンプルコードはGitHubにあります。