kagamihogeの日記

kagamihogeの日記です。

Web on Reactive Stack(version 5.0.3.RELEASE) 1. Spring WebFluxをテキトーに訳した(1.5 - 1.9)

1.5. Functional Endpoints

Spring WebFluxには、軽量な関数型プログラミングモデルがあり、関数ではルーティング・リクエスト処理とイミュータブルな設計を行います。アノテーションベースとは別のプログラミングモデルですが、基礎部分で動作するReactive Spring Webは同一です。

1.5.1. HandlerFunction

HTTPリクエストはHandlerFunctionで処理し、これは基本的にはServerRequestを受け取ってMono<ServerResponse>を返す関数です。アノテーションベースを知っているユーザ向けに言えば、ハンドラファクションは@RequestMappingメソッドに相当します。

ServerRequestServerResponseはイミュータブルなインタフェースで、基底のHTTPメッセージにJDK-8らしいやり方でアクセスを提供します。アクセスはReactive Streamsのノンブロッキングバックプレッシャで行います。リクエストはReactorのFluxもしくはMonoでボディを公開し、レスポンスはボディとしてReactive StreamsのPublisherをアクセプトします。これの合理性についてはReactive Librariesで解説します。

ServerRequestはHTTPリクエストの各種要素、メソッド・URI・クエリパラメータ・ヘッダー(ServerRequest.Headers経由)、へのアクセスを提供します。ボディへのアクセスはbodyメソッドで行います。たとえば、以下はリクエストボディをMono<String>に抽出します。

Mono<String> string = request.bodyToMono(String.class);

また、以下はボディをFluxに抽出する方法で、Personはボディのコンテンツからデシリアライズするクラスです(つまりボディがJSON, JAXB(XMLの場合)を含む場合PersonクラスはJacksonが処理する)。

Flux<Person> people = request.bodyToFlux(Person.class);

上述のbodyToMonobodyToFluxジェネリックServerRequest.body(BodyExtractor)メソッドを使用する便利なメソッドです。BodyExtractorは関数型のストラテジインタフェースで、自前の抽出ロジックを書くものですが、一般的なBodyExtractorインスタンスは```BodyExtractors````ユーティリティクラスにあります。よって、上記サンプルは以下のようにも書けます。

Mono<String> string = request.body(BodyExtractors.toMono(String.class);
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class);

同様に、ServerResponseはHTTPレスポンスへのアクセスを提供します。これはイミュータブルなので、ビルダーでServerResponseを生成します。ビルダーで、レスポンスステータス・レスポンスヘッダーの追加・ボディの指定、が出来ます。たとえば、レスポンスを、200 OKステータス・JSONコンテンツタイプ・ボディ指定、で作るには以下のようにします。

Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

また、以下は、201 CREATEDステータス・"Location"ヘッダー・空のボディ、のレスポンスを作ります。

URI location = ...
ServerResponse.created(location).build();

HandlerFunctionをこれらを使用して作れます。たとえば、以下はごく単純な"Hello World"なラムダのハンドラーで、200 ステータス・ボディにString、のレスポンスを返します。

HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().body(fromObject("Hello World"));

上述のように、ラムダでハンドラ関数を簡単に書けますが、場合によっては可読性を損なうので、複数の関数がある場合はメンテナンス性が悪くなります。よって、単一のハンドラかコントローラに、関連するハンドラ関数をグループ化することを推奨します。たとえば、以下はreactiveのPersonリポジトリを公開するクラスです。

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    public Mono<ServerResponse> listPeople(ServerRequest request) { // 1.
        Flux<Person> people = repository.allPeople();
        return ServerResponse.ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) { // 2.
        Mono<Person> person = request.bodyToMono(Person.class);
        return ServerResponse.ok().build(repository.savePerson(person));
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) { // 3.
        int personId = Integer.valueOf(request.pathVariable("id"));
        Mono<ServerResponse> notFound = ServerResponse.notFound().build();
        Mono<Person> personMono = repository.getPerson(personId);
        return personMono
                .flatMap(person -> ServerResponse.ok().contentType(APPLICATION_JSON).body(fromObject(person)))
                .switchIfEmpty(notFound);
    }
}
  1. listPeopleJSONリポジトリのすべてのPersionオブジェクトを返します。
  2. createPersonはリクエストボディ内のPersionを新規作成します。PersonRepository.savePerson(Person)Mono<Void>を返す点に注意してください。personがリクエストから読み込まれて保存されると、空のMonoは完了シグナルを出します。完了シグナル受信時、つまりPerson保存完了時、にレスポンスを送信するにはbuild(Publisher<Void>)を使うことになります。
  3. getPersonはパス変数にIDを指定して単一のpersonを返すハンドラー関数です。リポジトリからPersonを取得し、もし存在すればJSONレスポンスを生成します。無ければ、switchIfEmpty(Mono<T>)で404 Not Foundレスポンスを返しています。

1.5.2. RouterFunction

リクエストはRouterFunctionがハンドラー関数にルーティングします。RouterFunctionの関数はServerRequestを受け取ってMono<HandlerFunction>を返します。リクエストがある特定のルーティングにマッチするとハンドラ関数が返され、マッチしない場合は空のMonoが返されます。アノテーションベースを知っているユーザ向けに言えば、RouterFunction@RequestMappingアノテーションに相当します。

基本的には、ルータ―関数を自身で書くことはありませんが、リクエスト述語とハンドラ関数でルータ関数を生成するにはRouterFunctions.route(RequestPredicate, HandlerFunction)を使います。述語が適用可能な場合、そのリクエストはハンドラー関数にルーティングが行われ、そうでない場合ルーティングは実行されず、404 Not Foundレスポンスが返されます。自前のRequestPredicateを書くことは可能ですが、そうする必要は無く、良くある一般的な述語はRequestPredicatesユーティリティクラスにあり、例えば、パスペース・HTTPメソッド・コンテンツタイプなどでのマッチングです。routeを使用する、"Hello World"なハンドラ―関数へのルーティングの記述は以下のようになります。

RouterFunction<ServerResponse> helloWorldRoute =
    RouterFunctions.route(RequestPredicates.path("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")));

二つのルータ関数を一つの複合ルータ関数にまとめることが出来ます。最初のルーティングの述語にマッチしない場合、二つ目が評価されます。複合ルータ関数はそれぞれ順に評価されるので、狭い述語を先に置き、より汎用の述語を後に置きます。RouterFunction.and(RouterFunction)RouterFunction.andRoute(RequestPredicate, HandlerFunction)で二つのルータ関数を複合化します。後者はRouterFunction.and()RouterFunctions.route()を組み合わせて一度にやるものです。

上述したPersonHandlerを例にとると、個々のハンドラへのルータ関数は以下のように定義します。ハンドラ関数の参照はメソッド参照を使用しています。

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> personRoute =
    route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson)
        .andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople)
        .andRoute(POST("/person").and(contentType(APPLICATION_JSON)), handler::createPerson);

ルータ関数では、RequestPredicate.and(RequestPredicate)RequestPredicate.or(RequestPredicate)で、リクエスト述語の組み合わせを指定しています。これらの動作は見た目ままで、andは与えられた述語が両方ともマッチした場合で、orどちらかです。RequestPredicatesの述語の多くが複合になっています。たとえば、RequestPredicates.GET(String)RequestPredicates.method(HttpMethod)RequestPredicates.path(String)の複合です。

1.5.3. Running a server

HTTPサーバでのルータ関数の動かし方について。最もシンプルなやり方はルータ関数を以下のいずれかの手段でHttpHandlerに変換します。

  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

戻り値のHttpHandlerは各種サーバ固有のアダプタで使用可能で、以下のHttpHandlerに示す通りです。

別の方法として、WebFlux Configを使用するDispatcherHandlerベースのセットアップで動かす方法があります。WebFlux Configはリクエスト処理に必要となるコンポーネントを宣言するのにSpring configurationを使用します。WebFlux Java configは以下のファンクショナルエンドポイント用の基盤コンポーネントを宣言します。

  • RouterFunctionMapping - Spring configurationで一つ以上のRouterFunction<?>を検出すると、それらをRouterFunction.andOtherで複合化し、出来たRouterFunctionにリクエストをルーティングする。
  • HandlerFunctionAdapter - リクエストにマッピングされているHandlerFunctionDispatcherHandlerが呼び出すためのシンプルなアダプタ。
  • ServerResponseResultHandler - HandlerFunctionの呼び出し結果をServerResponsewriteToを呼び出すことで処理する。

上述のコンポーネントDispatcherHandlerのリクエスト処理ライフサイクルにファンクショナルエンドポイントをフィットさせるためのもので、同時に、アノテーションのコントローラがもし存在すればこれと並存させるためのものでもあります。また、Spring Boot WebFlux starterがファンクショナルエンドポイントを有効化する方法でもあります。

以下はWebFlux Java configの例です。(動かし方はDispatcherHandler参照)

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }

    // ...

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // configure message conversion...
    }

    @Override
    default void addCorsMappings(CorsRegistry registry) {
        // configure CORS...
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // configure view resolution for HTML rendering...
    }
}

1.5.4. HandlerFilterFunction

ルータ関数でマッピングしたルーティングはRouterFunction.filter(HandlerFilterFunction)でフィルタをかけられます。HandlerFilterFunctionServerRequestHandlerFunctionを受け取りServerResponseを返す関数です。このハンドラ関数の引数はチェーンの次の要素で、基本的にはルーティング先のHandlerFunctionですが、複数のフィルタが適用される場合は別のFilterFunctionになる場合があります。アノテーションベースの類似機能は@ControllerAdvice and/or ServletFilterで実現できます。いま、ルーティングにセキュリティフィルタを追加するとして、特定のパスが許可されるかどうかを判定するSecurityManagerが既にあるとします。

import static org.springframework.http.HttpStatus.UNAUTHORIZED;

SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = ...

RouterFunction<ServerResponse> filteredRoute =
    route.filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
  });

上記の例にあるようにnext.handle(ServerRequest)は必ずしも呼ぶ必要はありません。上記ではアクセスを許可する場合にだけハンドラ関数を呼び出しています。

ファンクショナルエンドポイントでのCORSはCorsWebFilterで行っています。

1.6. CORS

Spring MVCと同等

1.6.1. Introduction

Spring MVCと同等

セキュリティ上の理由によりブラウザはオリジンサーバ外のリソースへのAJAX呼び出しを禁止しています。あるタブで銀行アカウントを取得すると別タブのevil.comでも取得出来てしまいます。最初のタブで取得したクレデンシャルを使用するAJAXリクエストを、二つ目のタブのevil.comのスクリプトで利用可能にさせるできではありません。

Cross-Origin Resource Sharing (CORS)はたいていのブラウザが実装しているW3C specificationで、IFRAMEやJSOPなどのセキュリティが低く強力というわけでもない回避策よりも、許可するクロスドメインリクエストの種類を指定可能になります。

1.6.2. Processing

Spring MVCと同等

CORS仕様ではpreflight, simple, actual requestsを区別しています。CORSの動作を知るにはこの記事や、他にもいろいろありますが、詳細は仕様を参照してください。

Spring WebFluxのHandlerMappingはCORSのビルトイン機能です。リクエストからハンドラのマッピング正常終了後、HandlerMappingはリクエスト・ハンドラ・以降のアクションにおけるCORS configurationをチェックします。Preflight requestsは直接処理されますが、simpleとactual CORSリクエストは intercepted, validated, and have required CORS response headers set.

クロスオリジンリクエスト(Originヘッダが存在してリクエストのホストが異なる)の有効化には、明示的なCORS configurations宣言が必要です。CORS configurationが見つからない場合、preflight requestsを拒否します。simpleおよびactual CORSリクエストのレスポンスにCORSヘッダーを追加しないと、以降のブラウザはそれらを拒否します。

HandlerMappingCorsConfigurationマッピングベースのURLパターンで個々に設定できます。たいていの場合はアプリケーションはそうしたマッピングを宣言するのにWebFlux Java configを使います。この場合、すべてのHadlerMapppingに渡される単一のグローバルマッピングになります。

HandlerMappingのグローバルなCORS configurationはより細かい、ハンドラレベルのCORS configurationと組み合わせることが出来ます。たとえば、アノテーションのコントローラはクラスかメソッドレベルの@CrossOriginを使えます(その他のハンドラではCorsConfigurationSourceを実装する)。

グローバルとローカルのconfigurationの組み合わせの際のルールは基本的にはadditiveです。allowCredentialsmaxAgeなど単一値のみ取る属性では、ローカルがグローバルをオーバーライドします。詳細はCorsConfiguration#combine(CorsConfiguration)を参照。

各機能の詳細やより細かいカスタマイズについては以下を参照してください。

  • CorsConfiguration
  • CorsProcessor, DefaultCorsProcessor
  • AbstractHandlerMapping

1.6.3. @CrossOrigin

Spring MVCと同等

@CrossOriginによりコントローラーのメソッドでクロスオリジンリクエストを有効化します。

@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}

デフォルトの@CrossOriginは以下を許可します。

  • すべてのオリジン。
  • すべてのヘッダー。
  • コントローラーメソッドにマッピングされているすべてのHTTPメソッド
  • allowedCredentialsはデフォルトでは無効化で、これはクッキーとCSRFトークンなど重要なユーザ固有情報を公開するtrust levelを確立するためです。よって必要な場合にだけ使用すべきです。
  • maxAge30分。

クラスレベルの@CrossOriginはすべてのメソッドに引き継がれます。

@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}

クラスとメソッド両方でCrossOriginを使用可能です。

@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("http://domain2.com")
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}

1.6.4. Global Config

Spring MVCと同等

コントローラのメソッドレベルに加えて、グローバルのCORS configurationを定義したい場合もあります。HandlerMappingにURLベースのCorsConfigurationマッピングを設定できます。ただし、たいていのアプリケーションではWebFlux Java configを使うことになると思われます。

デフォルトのグローバルconfigurationは以下を有効化します。

  • すべてのオリジン。
  • すべてのヘッダー。
  • GET, HEAD, POSTのメソッド。
  • allowedCredentialsはデフォルトでは無効化で、これはクッキーとCSRFトークンなど重要なユーザ固有情報を公開するtrust levelを確立するためです。よって必要な場合にだけ使用すべきです。
  • maxAge30分。

WebFlux Java configでCORSを有効化するにはCorsRegistryコールバックを使います。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("http://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}

1.6.5. CORS WebFilter

Spring MVCと同等

ビルトインのCorsWebFilterでCORSを使用可能で、ファンクショナルエンドポイントにはこちらがよりフィットします。

フィルタの設定にはCorsWebFilter beanを宣言してそのコンストラクタにCorsConfigurationSourceを渡します。

@Bean
CorsWebFilter corsFilter() {

    CorsConfiguration config = new CorsConfiguration();

    // Possibly...
    // config.applyPermitDefaultValues()

    config.setAllowCredentials(true);
    config.addAllowedOrigin("http://domain1.com");
    config.addAllowedHeader("");
    config.addAllowedMethod("");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsWebFilter(source);
}

1.7. Web Security

Spring MVCと同等

Spring Securityは悪意のある攻撃からwebアプリケーションを保護する機能を提供します。詳細はSpring Security reference documentationを参照してください。

1.8. WebFlux Config

Spring MVCと同等

WebFlux Java configはコントローラやファンクショナルエンドポイントでのリクエスト処理に必要となるコンポーネントを宣言しており、カスタマイズのAPIを提供します。Java configが生成するbeanを理解する必要は必ずしもありませんが、必要になった場合は、WebFluxConfigurationSupportを見たりSpecial bean typesを参照してください。

configuration APIでは利用できない、より高度なカスタマイズについては、Advanced config modeでフルコントロールを得られます。

1.8.1. Enable WebFlux config

Spring MVCと同等

Java configで@EnableWebFluxを使用します。

@Configuration
@EnableWebFlux
public class WebConfig {
}

上記により多数のSpring WebFlux infrastructure beansを登録します。また、クラスパス上で利用可能な(JSONXML用などの)依存性に適したbeanも登録します。

1.8.2. WebFlux config API

Spring MVCと同等

Java configでWebFluxConfigurerインタフェースを実装します。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // Implement configuration methods...

}

1.8.3. Conversion, formatting

Spring MVCと同等

デフォルトではNumberDate用のフォーマッターがあり、これらでは@NumberFormat@DateTimeFormatが使えます。また、Joda Timeがクラスパス上にある場合、Joda Timeも使えるようになります。

カスタムのフォーマッターとコンバータを登録するには以下のようにします。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // ...
    }

}

FormatterRegistrarsを使うにはFormatterRegistrar SPIFormattingConversionServiceFactoryBeanを参照してください。

1.8.4. Validation

Spring MVCと同等

デフォルトでは、Bean ValidationHibernate Validatorなど、がクラスパス上にあればグローバルのValidatorLocalValidatorFactoryBeanを登録し、@Controllerメソッド引数の@ValidValidatedで使われます。

Java configで、グローバルのValidatorインスタンスをカスタマイズできます。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public Validator getValidator(); {
        // ...
    }

}

なお、ローカルにValidatorを登録することもできます。

@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }

}

どこかでLocalValidatorFactoryBeanをインジェクションする必要が生じる場合、beanを生成して@Primaryを付与し、MVC configで宣言しているものとのコンフリクトを回避してください。

1.8.5. Content type resolvers

Spring MVCと同等

Spring WebFluxでは@Controllerが要求するメディアタイプの決定方法を設定可能です。デフォルトでは"Accept"ヘッダーのみチェックしますが、クエリ―パラメータベースに変更することも可能です。

コンテンツタイプのリゾルバをカスタマイズするには以下のようにします。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
        // ...
    }
}

1.8.6. HTTP message codecs

Spring MVCと同等

リクエストとレスポンスの読み書きをカスタマイズできます。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // ...
    }
}

ServerCodecConfigurerがデフォルトのリーダーとライターを提供します。これを使用して、リーダー・ライターを追加したり、デフォルトをカスタマイズしたり、あるいは、デフォルトを完全に置き換えたりします。

Jackson JSONXMLでは、JacksonのデフォルトプロパティをカスタマイズするJackson2ObjectMapperBuilderの使用を検討してください。

  1. DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES をdisabledにする。
  2. MapperFeature.DEFAULT_VIEW_INCLUSIONをdisabledにする。

また、以下がクラスパス上にある場合、自動的に登録します。

  1. jackson-datatype-jdk7: java.nio.file.PathなどJava 7サポート。
  2. jackson-datatype-joda: Joda-Timeサポート。
  3. jackson-datatype-jsr310: Java 8 Date & Time APIサポート
  4. jackson-datatype-jdk8: OptionalなどJava 8サポート

1.8.7. View resolvers

Spring MVCと同等

viewのリゾルバを設定するには以下のようにします。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // ...
    }
}

なお、FreeMarkerではそのライブラリ用の設定が必要です。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // ...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        return configurer;
    }

}

1.8.8. Static resources

Spring MVCと同等

Resourceのリストで静的リソースを記述します。

以下の例では、"/resources"で始まるリクエストの場合、クラスパス上の静的リソース"/static"を参照する相対パスになります。リソースは、HTTPリクエスト削減およびブラウザキャッシュを確保するため、1年の有効期限になります。Last-Modifiedヘッダを評価し、もし存在すれば304ステータスコードを返します。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
            .addResourceLocations("/public", "classpath:/static/")
            .setCachePeriod(31556926);
    }

}

リソースハンドラはResourceResolverResourceTransformerのチェーンが出来ます。which can be used to create a toolchain for working with optimized resources.

VersionResourceResolverは、コンテンツから算出するMD5・固定アプリケーションバージョンなど、によるバージョン付きリソースURLを使えます。ContentVersionStrategyMD5ハッシュ)はモジュールローダーを使うJavaScriptリソースなどで有用な選択肢となります。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }

}

ResourceUrlProviderでURLリライトを行い、たとえばバージョンを追加するのに、リゾルバとtransformersのチェーンを適用できます。WebFlux configはResourceUrlProviderを提供するのにでこれを他で使います。

Spring MVCと異なり、今のところのWebFluxでは透過的な静的リソースリライトを行う方法が無く、これはリゾルバとtransformersのノンブロッキングチェーンを利用可能なビュー技術が存在しないためです(Amazon S3上のリソースなど)。ローカルのリソースを提供する場合のみ、直接ResourceUrlProviderを使います(例えばカスタムタグを経由するなどして)。この場合ブロックは0秒です。

WebJarsWebJarsResourceResolverで使用可能で、クラスパス上に"org.webjars:webjars-locator"がある場合自動的に登録します。このリゾルバはjarのバージョンを含むようにURLをリライトするので、バージョンの無いURLにマッチングします。たとえば、"/jquery/jquery.min.js"から"/jquery/1.2.0/jquery.min.js"など。

1.8.9. Path Matching

Spring MVCと同等

Spring WebFluxはパスパターンのパース済み表現、```PathPattern、を使用します。また、リクエストパスはRequestPathです。これらにより、リクエストパスのデコードやセミコロン削除をするかどうかを指定する必要を省きます。PathPatternはデコード済みのパスセグメントにアクセスして安全にマッチングが出来ます。*1

Spring WebFluxはサフィックスパターンマッチングをサポートせず、パスマッチングに関するカスタマイズのオプションは二つだけ存在します。trailing slashesとマッチングするかどうか(デフォルトtrue)、case-sensitiveとマッチングするかどうか(デフォルトfalse)。

これらオプションをカスタマイズするには以下のようにします。

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        // ...
    }

}

1.8.10. Advanced config mode

Spring MVCと同等

@EnableWebFluxDelegatingWebFluxConfigurationをインポートします。これは、(1)WebFluxアプリケーション用のデフォルトのSpring configuration、(2)configurationをカスタマイズするためのWebFluxConfigurerへのデリゲートの検出、を行います。

advanced modeでは、@EnableWebFluxを削除し、WebFluxConfigurerを実装する代わりにDelegatingWebFluxConfigurationを直接拡張します。

@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    // ...

}

WebConfigの既存メソッドはそのままで、ベースクラスのbean宣言をオーバーライドします。また、クラスパス上のその他のWebMvcConfigurerも使えます。

1.9. HTTP/2

Spring MVCと同等

Servlet 4コンテナはHTTP/2サポートが要求され、Spring Framework 5はServlet API 4と互換性があります。プログラミングモデルの観点からは特にこれといってアプリケーションが何かする必要はありません。サーバ設定に関する考慮事項があります。HTTP/2 wiki pageを参照してください。

現行のSpring WebFluxはNettyのHTTP/2をサポートしません。また、クライアントへのpushing resources programmaticallyもサポートしません。

*1:Spring WebFlux uses parsed representation of path patterns i.e. PathPattern, and also the incoming request path???i.e. RequestPath, which eliminates the need to indicate whether to decode the request path, or remove semicolon content, since PathPattern can now access decoded path segment values and match safely.が原文。カンマで区切られた文がズラッとならぶと訳するのがきつい…