Spring MVCの機能コントローラー

1. 前書き

link:/spring-5[Spring 5]は、https://www.baeldung.com/spring-5-functional-web [WebFlux]を導入しました。これは、 link:/spring-reactor[reactive]プログラミングモデル。
このチュートリアルでは、このプログラミングモデルをSpring MVCの機能コントローラーに適用する方法を説明します。

2. Mavenセットアップ

link:/spring-boot[Spring Boot]を使用して、新しいAPIのデモを行います。
このフレームワークは、コントローラを定義するおなじみの注釈ベースのアプローチをサポートします。 しかし、コントローラーを定義する機能的な方法を提供する新しいドメイン固有の言語も追加します。
Spring 5.2以降、機能的なアプローチは* https://www.baeldung.com/spring-mvc-tutorial [Spring Web MVC]フレームワークでも利用可能になります。* _WebFlux_モジュールと同様に、_RouterFunctions_と_RouterFunction_がメインですこのAPIの抽象化。
それでは、必要な依存関係をインポートすることから始めましょう。
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.0.BUILD-SNAPSHOT</version>
    <relativePath />
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
Spring 5.2はまだ一般公開されていないため、* Springリポジトリを介してこれらの依存関係をインポートする必要があります:*
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        </repository>
</repositories>
<pluginRepositories>
    <pluginRepository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </pluginRepository>
    <pluginRepository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </pluginRepository>
</pluginRepositories>

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サービスの単一またはグループを含めることができます。
Nested Routesを見るときに、Webサービスのグループについて詳しく説明します。
この例では、* _ RouterFunctions_のstatic route()メソッドを使用して_RouterFunction_を作成しました。*このメソッドを使用して、ルートのすべての要求および応答属性を提供できます。

3.2. リクエストの述語

この例では、route()でGET()メソッドを使用して、これが_GET_リクエストであり、_String._としてパスが指定されていることを指定します。
*リクエストの詳細を指定する場合は、_RequestPredicate_も使用できます。*
たとえば、前の例のパスは、_RequestPredicate_を使用して次のように指定することもできます。
RequestPredicates.path("/product")
ここでは、*静的ユーティリティ_RequestPredicates_を使用して、_RequestPredicate_のオブジェクトを作成しました。*

3.3. 応答

同様に、* _ ServerResponse_には、応答オブジェクトの作成に使用される静的ユーティリティメソッドが含まれています*。
この例では、_ok()_を使用してHTTPステータス200を応答ヘッダーに追加し、_body()_を使用して応答本文を指定します。
さらに、_ServerResponse_は、_EntityResponseを使用してカスタムデータ型からの応答の構築をサポートします。

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.  エラー処理

別の一般的な使用例は、カスタムエラー処理メカニズムを使用することです。 _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_から応答オブジェクトを作成しました。 次に、ステータスを追加し、_ServerResponse_オブジェクトを返す_EntityResponse.build()_を使用します。

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. 結論

このチュートリアルでは、コントローラーを定義するための機能的アプローチの簡単な紹介から始めました。 次に、Spring MVCアノテーションと機能的に同等のものを比較しました。
次に、機能的なコントローラーを備えた製品のリストを返す単純なWebサービスを実装しました。
次に、ネストされたルート、エラー処理、アクセス制御用のフィルターの追加、ロギングなどの横断的な懸念事項の管理など、Webサービスコントローラーの一般的なユースケースの実装に進みました。
いつものように、サンプルコードはhttps://github.com/eugenp/tutorials/tree/master/spring-boot-mvc-2[GitHub]で見つけることができます。