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.が原文。カンマで区切られた文がズラッとならぶと訳するのがきつい…

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

1.4. Annotated Controllers

Spring MVCと同等

Spring WebFluxはアノテーションベースのプログラミングモデルを提供しており、@Controller@RestControllerコンポーネントは、リクエスマッピング・リクエスト入力・例外ハンドリングなど、を表現するのにアノテーションを用います。コントローラには適当なメソッドを作りますが、ベースクラスの拡張や特定インタフェースの実装は不要です。

以下が例です。

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String handle() {
        return "Hello WebFlux";
    }
}

この例ではメソッドはレスポンスボディに書き込むStringを返しています。

1.4.1. @Controller

Spring MVCと同等

一般的なSpringのbeanでコントローラーを定義します。ステレオタイプ@Controllerは自動検出を行うものです。これはクラスパスの@Componentクラスを検出するSpringの汎用サポート機能で、該当のbean定義を自動登録します。また、コントローラのステレオタイプとしても振る舞うので、これはそのクラスがwebコンポーネントの役割を担うことを指します。

@Controllerの自動検出を有効化するには、Java configurationにコンポーネントスキャンを追加します。

@Configuration
@ComponentScan("org.example.web")
public class WebConfig {

    // ...
}

@RestController@Controller@ResponseBodyから成る複合アノテーションです。これはコントローラのすべてのメソッドは型レベルの@ResponseBodyアノテーションを引き継ぎ、それらのメソッドはレスポンスボディに書き込む、という意味になります(対してmodel-and-viewレンダリングもある)。

1.4.2. Request Mapping

Spring MVCと同等

@RequestMappingはリクエストをコントローラのメソッドにマッピングするのに使います。URL・HTTPのメソッド・リクエストパラメータ・ヘッダ・メディアタイプなどでマッチする様々な属性があります。共通のマッピングを表現するにはクラスレベルで使用し、特定のエンドポイントマッピングに落とし込むにはメソッドレベルで使用します。

また、@RequestMappingには特定のHTTPメソッド用のショートカットがあります。

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

これらは複合アノテーションで、アノテーション自身に@RequestMappingが付けられています。これらは通常メソッドレベルで使います。クラスレベルには@RequestMappingが共通のマッピングを表すのに便利です。

@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}

URI Patterns

Spring MVCと同等

リクエストのマッピングにはワイルドカードパターンを使います。

  • ?は1文字とマッチ
  • *はパスセグメント内で0文字以上とマッチ
  • **は0個以上のパスセグメントにマッチ

またURL変数とその値にアクセスする@PathVariableを宣言できます。

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}

URL変数はクラスとメソッドレベルで使えます。

@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

    @GetMapping("/pets/{petId}")
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}

URI変数は自動的に適当な型に変換され、出来ない場合はTypeMismatchExceptionをスローします。単純な型のint, long, Dateはデフォルトでサポートしており、その他のデータ型は自前で登録します。型変換Binder Methodsを参照してください。

URI変数には明示的な名前、例えば@PathVariable("customId")、を付けられますが、名前と同じ場合には省略可能です。省略するには、デバッグ情報を付けるか、Java 8では-parametersフラグを付けてコンパイルします。

URI変数宣言{*varName}は、ゼロ個以上のパスセグメントにマッチします。例えば、/resources/{*path}/resources/のすべてのファイルにマッチし、"path"変数は完全な相対パスになります*1

URI変数宣言{varName:regex}正規表現付きのもので、例えばURL /spring-web-3.0.5 .jar"の場合、以下のメソッドはname, version, extentionを抽出します。

@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
    // ...
}

URIパスパターンにはプレースホルダ${...}を入れることも可能で、これは開始時にPropertyPlaceHolderConfigurerを介して、ローカル・システム・環境などのプロパティソースで解決されます。使用例としては、何らかの外部設定に基づくベースURLをパラメータ化するなどです。

Spring WebFluxはURIパスマッチングにPathPatternPathPatternParserを使用します。これらはどちらもspring-webのもので、実行時に大量のURIパスパターンマッチングを行うwebアプリケーションのHTTP URLパスに使うために作られたものです。

Spring WebFluxはサフィックスパターンマッチをサポートせず、対照的に、Spring MVC/person/person.*にマッチするなどのマッピングをします。

URLベースのコンテンツネゴシエーションがもし必要であれば、クエリパラメータを推奨します。クエリパラメータは、単純かつ明示的で、URIパスベースで脆弱性を作る危険性を減らせます。

Pattern Comparison

Spring MVCと同等

URLに複数のパターンがマッチする場合、最もマッチするものを決定する必要があります。

PathPattern.SPECIFICITY_COMPARATOR

すべてのパターンについて、URI変数とワイルドカードの個数に基づくスコア算出を行い、URI変数のスコアはワイルドカードより低いです。トータルスコアがより低いパターンが選ばれます。もし二つのパターンが同一スコアの場合、最も長い方が選ばれます。

**, {*varName}などcatch-allのパターンはスコア計算から除外されて常にソート順が末尾になります。もし二つのパターンともcatch-allの場合、最も長い方が選ばれます。

Consumable Media Types

Spring MVCと同等

リクエストのContent-Typeを用いてリクエスマッピングを絞り込むことが出来ます。

@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
    // ...
}

consumes属性は否定表現もサポートしていおり、!text/plainは"text/plain"以外のコンテンツタイプという意味になります。

クラスレベルで共通のconsumes属性を宣言できます。他のリクエスマッピングの属性とは異なり、これをクラスレベルで使う場合、メソッドレベルのconsumes属性はクラスレベルの宣言の拡張ではなくオーバライドします。

MediaTypeAPPLICATION_JSON_VALUE, APPLICATION_JSON_UTF8_VALUEなど一般的なメディアタイプの定数があります。

Producible Media Types

Spring MVCと同等

Acceptリクエストヘッダーでリクエスマッピングや、コントローラーのメソッドがプロデュースするコンテンツタイプのリストを、絞り込めます。

@GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}

メディアタイプには文字列を指定します。否定表現、例えば!text/plainを使用可能で、これは"text/plain"以外の意味になります。

クラスレベルで共通のproduces属性を宣言できます。他のリクエスマッピングの属性とは異なり、これをクラスレベルで使う場合、メソッドレベルのproduces属性はクラスレベルの宣言の拡張ではなくオーバライドします。

MediaTypeAPPLICATION_JSON_VALUE, APPLICATION_JSON_UTF8_VALUEなど一般的なメディアタイプの定数があります。

Parameters and Headers

Spring MVCと同等

クエリパラメータの条件によりリクエスマッピングを絞り込めます。クエリパラメータが存在するかどうか("myParam")、存在しないかどうか("!myParam")、特定の値かどうか("myParam=myValue")、が使えます。

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
    // ...
}

リクエストヘッダ―でも同じような条件が使えます。

@GetMapping(path = "/pets", headers = "myHeader=myValue")
public void findPet(@PathVariable String petId) {
    // ...
}

HTTP HEAD, OPTIONS

Spring MVCと同等

@GetMapping - と@RequestMapping(method=HttpMethod.GET)はリクエスマッピングでは透過的にHTTP HEADをサポートします*2。コントローラーのメソッドには特に手を入れる必要はありません。HttpHandlerのサーバアダプタが適用されるレスポンスラッパは、"Content-Length"ヘッダーに書き込みバイト数をセットし、レスポンスには何も書き込みません。

デフォルトではHTTP OPTIONSは、マッチするURLパターンのすべての@RequestMapping methodsに"Allow"レスポンスヘッダを指定することで処理されるようになります。

HTTPメソッド宣言の無い@RequestMappingでは、"Allow"ヘッダーは"GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"がセットされます。コントローラのメソッドは、特定のHTTPメソッド用のアノテーション@GetMapping, @PostMappingなど、使用するHTTPメソッドを常に宣言するようにしてください。

@RequestMappingのメソッドは明示的にHTTP HEADとHTTP OPTIONSにマッピングされますが、一般的な使用法ではありません。

1.4.3. Handler methods

Spring MVCと同等

@RequestMappingハンドラーメソッドのシグネチャは柔軟に決められ、コントローラメソッドの引数と戻り値にはいくつかの選択肢があります。

Method arguments

Spring MVCと同等

以下のテーブルはコントローラーメソッド引数で使用可能な一覧です。

Reactive型(Reactor, RxJava, その他)をブロッキングI/Oを必要とする引数、例えばリクエストボディの読み込み、で使用できます。この点はdescription列に記しています。ブロッキングを必要としない引数ではReactive型は使用できません。

JDK 1.8のjava.util.Optinalrequired属性、たとえば@RequestParam, @RequestHeader、を持つアノテーションのメソッド引数と組み合わせて使用できます。また、required=falseと同等になります。

Controller method argument Description
ServerWebExchange ServerWebExchangeのフルアクセス。このクラスは、HTTPリクエスト・レスポンス・リクエストとセッション属性・checkNotModifiedメソッド、などのコンテナ
ServerHttpRequest, ServerHttpResponse HTTPリクエストやレスポンスへのアクセス用
WebSession セッションへのアクセス用。属性が追加されない限り、新規セッションの開始を強制しない。reactive型サポート対象。
java.security.Principal 現在の認証済みユーザ。基本的には既知のPrincipal実装になる。reactive型サポート対象。
org.springframework.http.HttpMethod リクエストのHTTPメソッド
java.util.Locale 使用可能な中で最も特定的なLocaleResolverで決定される現在のリクエストのロケール
Java 6+: java.util.TimeZone
Java 8+: java.time.ZoneId
LocaleContextResolverが決定する現在のリクエストに関連付けられたタイムゾーン
@PathVariable URIテンプレート変数へのアクセス用。URI Patterns参照。
@MatrixVariable URIパスセグメント内のname-valueペアへのアクセス用。Matrix variables
@RequestParam Servletリクエストパラメータへのアクセス用。パラメータ値はメソッド引数の型に変換される。@RequestParamを参照。
属性の設定には@RequestParam以外の方法もあります。この表の"Any other argument"も参照。
@RequestHeader リクエストヘッダ―へのアクセス用。ヘッダー値はメソッド引数の型に変換される。@RequestHeader
@CookieValue クッキーへのアクセス用。クッキーの値はメソッド引数の型に変換される。@CookieValue
@RequestBody HTTPリクエストボディへのアクセス用。ボディコンテンツはHttpMessageReaderを使用するメソッド引数の型に変換される。reactive型サポート対象。
HttpEntity<B> リクエストヘッダ―とボディへのアクセス用。ボディはHttpMessageReaderに変換される。reactive型サポート対象。
@RequestPart "multipart/form-data"リクエストのpart部分へのアクセス用。reactive型サポート対象。MultipartMultipart Readerを参照。
java.util.Map,
org.springframework.ui.Model,
org.springframework.ui.ModelMap
HTMLコントローラーおよびビューのテンプレートで使うモデルへのアクセス用。
@ModelAttribute モデル(無ければインスタンス化される)の属性へのアクセス用。データバインディングとvalidationが適用される。@ModelAttribute, Model Methods, Binder Methodsを参照。
属性の設定には@ModelAttribute以外の方法もあります。この表の"Any other argument"も参照。
Errors, BindingResult command object(@ModelAttribute引数のこと)のvalidationとデータバインディングのエラー、もしくは、@RequestBodyもしくは@RequestPartのvalidationエラー。ErrorsBindingResultはvalidation対象メソッド引数の直後に宣言する必要がある。
SessionStatus +
クラスレベル@SessionAttributes
あるフォーム処理の完了時に、クラスレベルの@SessionAttributesで宣言したセッション属性のクリーンアップを指示する。@SessionAttributesを参照。
UriComponentsBuilder 相対URLを現在のリクエストのホスト・ポート・スキーマ・コンテキストパスを考慮したものにする。サーブレットマッピングリテラルパートはForwardedX-Forwarded-*ヘッダーに入る。
@SessionAttribute 任意のセッション属性へのアクセス用。クラスレベルの@SessionAttributes宣言でセッションに持たせるモデル属性以外のセッション属性にもアクセスできます。
@RequestAttribute リクエスト属性へのアクセス用。@RequestAttributeを参照。
その他 メソッド引数が上記いずれにもマッチしない場合、デフォルトでは、BeanUtils#isSimplePropertyに該当する単純な型の場合は@RequestParamに解決される。そうでない場合は@ModelAttributeになる。
Return values

Spring MVCと同等

以下の表はコントローラのメソッドで使用可能な戻り値です。すべての戻り値でReactive型、Reactor, RxJava, その他、が使用可能です。

Controller method return value Description
@ResponseBody HttpMessageWriterエンコードしてレスポンスに書き込まれる。@ResponseBody参照。
HttpEntity<B>, ResponseEntity<B> HttpMessageWriterエンコードするボディとHTTPヘッダーを含む完全なレスポンス。ResponseEntity参照。
HttpHeaders ボディ無しでヘッダーを持つレスポンスを返す。
String ViewResolverで解決するビュー名で、command objectsと@ModelAttributeのメソッドが決定する暗黙モデルで使用される*3。ハンドラメソッドではModel引数を宣言してモデルを使用します(上記参照)。
View 暗黙モデルと一緒にレンダリングに使うViewインスタンス。ハンドラメソッドではModel引数を宣言してモデルを使用します(上記参照)。
java.util.Map, org.springframework.ui.Model 暗黙モデルに追加される属性。ビュー名はリクエストパスに基づいて暗黙的に決定される。
@ModelAttribute モデルに追加される属性。ビュー名はリクエストパスに基づいて暗黙的に決定される。
@ModelAttributeの使用は任意です。この表の「それ以外の戻り値」を参照。
Rendering モデルとビューをレンダリングするシナリオ用のAPI
void voidのメソッドは、おそらく非同期(Mono<Void>など)で、ServerHttpResponseServerWebExchange引数・@ResponseStatusのいずれかの場合、戻り値型(もしくはnull)はレスポンスを完全に処理したと見なします。コントローラーがpositive ETagかlastModifiedタイムスタンプチェックを行う場合も同様に扱われます。上記いずれにも当てはまらない場合、voidはRESTコントローラーでは"no response body"を意味し、HTMLコントローラーではデフォルトのビュー名になります。
Flux<ServerSentEvent>,
Observable<ServerSentEvent>,
その他のReactive型
server-sentイベントの送出。データを書き込む必要がある場合のみ、SeverSentEventラッパーは省略可能です。(ただし、text/event-streamは、リクエストするか、属性のプロデュース経由でマッピングに宣言する必要があります。*4
それ以外の戻り値 上記いずれにも該当しない場合、デフォルトでは、Stringvoidの場合ビュー名として扱われます(voidはデフォルトビュー名が適用)。また、BeanUtils#isSimplePropertyに該当する単純な型でない場合はモデルに追加される属性になります。それ以外はunresolvedのままです。

Type Conversion

Spring MVCと同等

コントローラメソッドの引数でSpringの入力リクエストを表現するアノテーション、例えば@RequestParam, @RequestHeader, @PathVariable, @MatrixVariable, @CookieValue、は引数をString以外で宣言する場合は型変換を基本的には必要とします。

このような場合は型変換は設定されているコンバーターを自動的に適用します。デフォルトでは、単純な型、int, long, Date、などです。型変換はWebDataBinderでカスタマイズするか、FormattingConversionServiceFormattersを登録します。Spring Field Formatting参照。

Matrix variables

Spring MVCと同等

RFC 3986ではパスセグメント内のname-valueペアについての提案です。Spring WebFluxではTim Berners-Leeの過去ポストに基づきこれを"matrix variables"と呼称していますが、URIパスパラメータ(URI path parameters)とも呼称します。

Matrix variablesはパスセグメント内に配置可能で、各変数はセミコロンで区切り、複数の値はカンマで区切ります。例:"/cars;color=red,green;year=2012"。複数の値は変数名を繰り返すことでも指定可能です。例:"color=red;color=green;color=blue"

Spring MVCとは異なり、WebFluxではURLのmatrix variablesの有無はリクエスマッピングに影響を与えません。言い換えると、可変要素をマスクするためのURI変数を用意する必要はありません。コントローラーメソッドでmatrix variablesにアクセスする場合、matrix variablesを含むパスセグメントにURI変数を追加する必要があります。以下がその例です。

// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11
}

すべてのパスセグメントがmatrix variablesを含む場合、matrix variableを含みうるパス変数のあいまいさを無くす必要があります。

// GET /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {

    // q1 == 11
    // q2 == 22
}

matrix variableにはデフォルト値を指定可能です。

// GET /pets/42

@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

    // q == 1
}

すべてのmatrix variablesを取得するにはMultiValueMapを使います。


// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable MultiValueMap<String, String> matrixVars,
        @MatrixVariable(pathVar="petId"") MultiValueMap<String, String> petMatrixVars) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}

@RequestParam

Spring MVCと同等

コントローラでクエリパラメータをメソッド引数にバインドするには@RequestParamを使います。以下が使用例です。

@Controller
@RequestMapping("/pets")
public class EditPetForm {

    // ...

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) {
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    // ...

}

Servlet APIの"request paramater"の概念は、クエリパラメータ・フォームデータ・マルチパートをを一つにまとめていますが、これと異なりWebFluxでは、ServerWebExchangeを介してそれぞれにアクセスします。@RequestParamはクエリパラメータにだけバインドしますが、command objectにクエリパラメータ・フォームデータ・マルチパートをデータバインディングできます。

@RequestParaのメソッド引数のデフォルトは必須ですが、@RequestParamrequiredfalseにするかjava.util.Optionalでラップできます。

ターゲットメソッドの引数型がStringでは無い場合、型変換を自動的に適用します。mvc-ann-typeconversion

@RequestParamMap<String, String>MultiValueMap<String, String>で宣言する場合、そのマップにはすべてのクエリパラメータが入ります。

属性を設定するのに@RequestParamは必ずしも必須ではない点に注意してください。デフォルトでは、引数は、BeanUtils#isSimplePropertyが単純な値型と判定し、かつ、その他の引数リゾルバで解決しない場合、@RequestParamを付与したと見なします。

@RequestHeader

Spring MVCと同等

コントローラーでリクエストヘッダをメソッド引数にバインドするには@RequestHeaderを使います。

以下のヘッダーがあると仮定します。

Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300

以下によりAccept-EncodingKeep-Aliveの値を取得します。

@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding,
        @RequestHeader("Keep-Alive") long keepAlive) {
    //...
}

ターゲットメソッドの引数型がStringでは無い場合、型変換を自動的に適用します。mvc-ann-typeconversion

@RequestHeaderMap<String, String>MultiValueMap<String, String>HttpHeadersで宣言する場合、そのマップにはすべてのヘッダー値が入ります。

カンマ区切り文字列をStringもしくは型変換システムが利用可能な型のarray/collectionへの変換をビルトインで利用可能です。例えば、@RequestHeader("Accept")のメソッド引数はString以外にString[]List<String>も可能です。

@CookieValue

Spring MVCと同等

コントローラーでHTTPクッキーの値をメソッド引数にバインドするには@CookieValueを使います。

以下のクッキーがあるとします。

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下はクッキーの値を取得するコード例です。

@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) {
    //...
}

ターゲットメソッドの引数型がStringでは無い場合、型変換を自動的に適用します。mvc-ann-typeconversion

@ModelAttribute

Spring MVCと同等

メソッド引数でモデルの属性にアクセスには@ModelAttributeを使います。存在しない場合はインスタンス化します。モデル属性はフォームフィールドの名前と一致するクエリパラメータの値を同じに出来ます。これはデータバインディングと呼ばれ、クエリパラメータとフォームフィールド間のパースと変換の手間を省くものです。

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { }

上記Petインスタンスは以下のように解決が行われます。

  • Model Methodsで追加済みの場合、そのモデルから取得。
  • @SessionAttributesのHTTPセッションから取得。
  • デフォルトコンストラクタの呼び出しから取得。
  • クエリパラメータかフォームフィールドにマッチする引数で"primary constructor"を呼び出して取得。引数名はJavaBeansの@ConstructorPropertiesバイトコード内にある実行時保持型のパラメータ名(runtime-retained parameter names )で決定される。

モデル属性のインスタンスを取得後に、データバインディングを適用します。WebExchangeDataBinderクラスは、クエリパラメータ名とフォームフィールドを、ターゲットオブジェクトのフィールド名とマッチングします。マッチするフィールドは適宜型変換後に値が入ります。データバインディング(とvalidation)の詳細はValidationを参照。データバインディングのカスタマイズの詳細はBinder Methodsを参照。

データバインディングはエラーになる可能性があります。デフォルトではWebExchangeBindExceptionになりますが、コントローラーメソッドでそうしたエラーを処理するには@ModelAttributeのすぐ横にBindingResult引数を追加します。

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}

Validationを行うにはデータバインディング後にjavax.validation.ValidかSpringの@Validatedを付与します(詳細はBean validationSpring validationを参照)。

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}

Spring WebFluxは、Spring MVCと異なり、モデルにreactive型(Mono<Account>io.reactivex.Single<Account>など)を使えます。@ModelAttributeにはreactive型ラッパーを使用しても良いし、しなくても良いです。必要に応じて実際の値に解決されます*5。なお、ここでBindingResultを使うには、上で見たように、reactive型ラッパを使わず、BindingResultの前に@ModelAttributeを宣言する必要があります。もしくは、reactive型を通してエラーを処理します。

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
    return petMono
        .flatMap(pet -> {
            // ...
        })
        .onErrorResume(ex -> {
            // ...
        });
}

属性を設定するのに@ModelAttributeは必ずしも必須ではない点に注意してください。

デフォルトでは、引数は、BeanUtils#isSimplePropertyが単純な値型では無いと判定し、かつ、その他の引数リゾルバで解決しない場合、@ModelAttributeを付与したと見なします。

@SessionAttributes

Spring MVCと同等

@SessionAttributesWebSessionにモデル属性を格納するのに使用します。ある特定のコントローラーで使うセッション属性を宣言する型レベルアノテーションです。基本的には、モデル属性の名前かモデル属性の型を列挙し、後続のリクエストでアクセスするためにセッションへ透過的な保存が行われます。

@Controller
@SessionAttributes("pet")
public class EditPetForm {
    // ...
}

初回リクエストで"pet"という名前のモデル属性をモデルに追加し、自動的に昇格してWebSessionに保存が行われます。これを削除するには別のコントローラメソッドの引数でSessionStatusを使用します。

@Controller
@SessionAttributes("pet")
public class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) {
        if (errors.hasErrors) {
            // ...
        }
            status.setComplete();
            // ...
        }
    }
}

@SessionAttribute

Spring MVCと同等

グローバル管理、つまり対象コントローラの外(フィルタなど)、で既に存在するセッション属性にアクセスする場合、存在してもしていなくても、メソッド引数で@SessionAttributeを使います。

@GetMapping("/")
public String handle(@SessionAttribute User user) {
    // ...
}

セッション属性の追加や削除を行う場合はコントローラメソッドにWebSessionをインジェクションします。

コントローラーの処理フローの一部をセッションのモデル属性に一時保存する場合、@SessionAttributesで説明するSessionAttributesを使用してください。

@RequestAttribute

Spring MVCと同等

@SessionAttribute同様に、@RequestAttributeで既に存在する、WebFilterなどが作成する、リクエスト属性にアクセスします。

@GetMapping("/")
public String handle(@RequestAttribute Client client) {
    // ...
}

Multipart

Spring MVCと同等

Multipart Readerの解説の通り、ServerWebExchangeでマルチパートにアクセスします。コントローラーでファイルアップロードのフォームを処理するベストな方法はcommand objectのデータバインディングを使います。

class MyForm {

    private String name;

    private MultipartFile file;

    // ...

}

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(MyForm form, BindingResult errors) {
        // ...
    }

}

RESTfulサービスの非ブラウザクライアントからもマルチパートリクエストをサブミットできます。

POST /someUrl
Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit

{
    "name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...

JSONをデシリアライズする@RequestPartで"meta-data"にアクセスできます。この処理は設定したHTTP Message Codecsを使用します。

@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
        @RequestPart("file-data") FilePart file) {
    // ...
}

ストリーミングでマルチパートをシーケンシャルにアクセスするには、@RequestBodyFlux<Part>を使います。

@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) {
    // ...
}

@RequestPartは、Standard Bean Validationを適用するための、javax.validation.ValidかSpringの@Validatedを一緒に使えます。デフォルトではvalidationエラーはWebExchangeBindExceptionとなり400 (BAD_REQUEST)レスポンスになります。もしくは、ErrorsBindingResult引数によりコントローラ内でvalidationエラーを処理できます。

@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
        BindingResult result) {
    // ...
}

@RequestBody

Spring MVCと同等

リクエストボディを読み込みHttpMessageReader経由でObjectにデシリアライズするには@RequestBodyを使います。以下は@RequestBodyの例です。

@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}

Spring MVCと異なり、WebFluxでは、@RequestBodyメソッド引数はreactive型と完全なノンブロッキング読込およびストリーミングを行えます。

@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
    // ...
}

メッセージリーダのカスタマイズや設定変更はWebFlux ConfigHTTP message codecsを使います。

@RequestBodyは、Standard Bean Validationを適用するための、javax.validation.ValidかSpringの@Validatedを一緒に使えます。デフォルトではvalidationエラーはWebExchangeBindExceptionとなり400 (BAD_REQUEST)レスポンスになります。もしくは、ErrorsBindingResult引数によりコントローラ内でvalidationエラーを処理できます。

@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
    // ...
}

HttpEntity

Spring MVCと同等

HttpEntityは基本的には@RequestBodyの使い方と同じですが、こちらはリクエストヘッダ―とボディを公開するコンテナオブジェクトがベースになっています。

@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}

@ResponseBody

Spring MVCと同等

@ResponseBodyHttpMessageWriterシリアライズしたレスポンスボディを返すメソッドで使います。

@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}

@ResponseBodyはすべてのコントローラーメソッドに引き継ぐためにクラスレベルでも使えます。@RestControllerでは@Controllerおよび@ResponseBodyのメタアノテーションを意味します。

@ResponseBodyはreactive型をサポートし、ReactorないしRxJavaの型を返してこれのクラスがプロデュースする非同期な値をレスポンスにレンダリングできます。JSONレンダリングの詳細はJackson JSONを参照。

@ResponseBodyメソッドはJSON serialization viewsと組み合わせることが出来ます。mvc-ann-jacksonを参照。

メッセージ書き込みもしくは設定変更にはWebFlux ConfigHTTP message codecsを使います。

ResponseEntity

Spring MVCと同等

ResponseEntityは基本的には@ResponseBodyの使い方と同じですが、こちらはリクエストヘッダ―とボディを公開するコンテナオブジェクトがベースになっています。

@PostMapping("/something")
public ResponseEntity<String> handle() {
    // ...
    URI location = ...
    return new ResponseEntity.created(location).build();
}

Jackson JSON

Jackson serialization views

Spring MVCと同等

Spring WebFluxにはJackson's Serialization Views*6用の機能が組み込まれています。これはObjectのフィールドのサブセットのみレンダリングします。これを@ResponseBodyもしくはResponseEntityのコントローラーメソッドで使うには、Jacksonの@JsonViewでserialization view classを有効化します。

@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}

@JsonViewにはview classの配列を指定できますが、一つのコントローラーメソッドには一つのJsonViewのみ指定可能です。複数のviewを有効化する場合はcomposite interfaceを使用してください。

1.4.4. Model Methods

Spring MVCと同等

@ModelAttribute@RequestMappingメソッド引数で、モデルのObjectの生成とアクセスおよびリクエストへのバインディング、を行うために使います。

@ModelAttributeはコントローラーのメソッドレベルでも使えます。

これはリクエストの処理ではなく、リクエストの処理前

コントローラには複数の@ModelAttributeメソッドを作れます。同一コントローラ内の@RequestMappingメソッドより前に、そうしたメソッドをすべて呼び出します。@ModelAttributeメソッドは@ControllerAdviceを介して複数コントローラー間で共有できます。詳細はController Advice参照。

@ModelAttributeメソッドのシグネチャは柔軟に決められます。このメソッドで使用できる引数は@RequestMappingメソッドとほぼ同じですが、@ModelAttribute自身とリクエストボディに関するものは除きます。

以下は@ModelAttributeの例です。

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}

属性を一つ追加するだけの場合は、

@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}

名前を明示的に指定しない場合、デフォルト名はConventionsJavadocの説明にあるとおりObject型に基づいて決定されます。名前指定可能な方のaddAttributeメソッドを使うか、@ModelAttributeのname属性で明示的な名前を割り当てられます。

Spring WebFluxは、Spring MVCと異なり、Mono<Account>io.reactivex.Single<Account>などのreactive型をモデルで使用できます。@RequestMappingの実行時、@ModelAttributeの引数をラッパー無しで宣言すると、こうした非同期モデル属性は実際の値に透過的な解決(とモデル更新)が行われます。

@ModelAttribute
public void addAccount(@RequestParam String number) {
    Mono<Account> accountMono = accountRepository.findAccount(number);
    model.addAttribute("account", accountMono);
}

@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
    // ...
}

ビューレンダリングの前に、reactive型ラッパーのモデル属性はその実際の値に解決(とモデル更新)が行われます。

@ModelAttribute@RequestMappingのメソッドレベルでも使えます。この場合は@RequestMappingメソッドの戻り値はモデル属性と解釈されます。これはHTMLコントローラのデフォルトの振る舞いであって、基本的には使わないもので、戻り値がStringでなければビュー名とは解釈されません。@ModelAttributeはモデル属性の名前をカスタマイズ可能です。

@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}

1.4.5. Binder Methods

Spring MVCと同等

文字列ベースのリクエスト(例:リクエストパラメータ、パス変数、ヘッダー、クッキーなど)を表すメソッド引数の型変換のカスタマイズをするには@Controllerもしくは@ControllerAdviceクラスで@InitBinderのメソッドを作ります。型変換は@ModelAttribute(つまりcommand objects)のリクエストパラメータのデータバインディングにも適用されます。

@InitBinderのメソッドは、コントローラ固有のjava.bean.PropertyEditorかSpringのConverterFormatter、を登録できます。また、WebFlux Java configを使用してグローバルなFormattingConversionServiceConverterFormatterを登録できます。

@InitBinderメソッドは@RequestMappingのメソッドと同じような引数を使用できますが、@ModelAttribute(command object)は使えません。通常、登録を行うためのWebDataBinder引数と一緒に宣言し、voidを返します。

@Controller
public class FormController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}

また、FormattingConversionServiceを介してFormatterベースのクラスを登録する場合、コントローラー固有のFormatterを登録できます。

@Controller
public class FormController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
    }

    // ...
}

1.4.6. Controller Advice

Spring MVCと同等

通常、@ExceptionHandler, @InitBinder, @ModelAttributeのメソッドは、これらを宣言する@Controller(かそのクラス階層)内に適用するものです。これらのメソッドをグローバルに、複数のコントローラに、適用したい場合、@ControllerAdviceもしくは@RestControllerAdviceのクラスで宣言します。

@ControllerAdviceには@Componentがついているので、コンポーネントスキャンによってSpringのbeansとして登録されます。また、@RestControllerAdvice@ControllerAdvice@ResponseBodyのメタアノテーションで、@ExceptionHandlerのメソッドはメッセージ変換を介してレスポンスボディにレンダリングされます。

@RequestMapping@ExceptionHandlerメソッド用のinfrastructure classesが@ControllerAdviceのSpring beanを検出すると、実行時にそれらのメソッドを適用します。グローバルの(@ControllerAdviceの)@ExceptionHandlerメソッドはローカルの(@Controllerあとに適用されます。これと対照的に、グローバルの@ModelAttribute@InitBinderメソッドはローカルのそれのまえに適用されます。

デフォルトでは@ControllerAdviceメソッドはすべてのリクエスト、つまりすべてのコントローラー、に適用されますが、アノテーションの属性を通じてコントローラーのサブセットに絞り込みをかけられます。

// @RestControllerを付与するすべてのコントローラー
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// 特定パッケージ内のすべてのコントローラー
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// 特定のクラスとassignableなすべてのコントローラ
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

なお、上記セレクタは実行時に評価されるので、過度の使用はパフォーマンスに影響を及ぼす可能性があります。詳細は@ControllerAdviceJavadocを参照。

*1:variable captures the complete relative pathが原文

*2:http://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/spring-method-head-options-support/ 実際にコード書いて試してないんで間違ってるかもしれないが、springのGETは暗黙的にHEADをサポートする、とかなんとか

*3:used together with the implicit model - determined through command objects and @ModelAttribute methods.が原文。よくわからん

*4:however text/event-stream must be requested or declared in the mapping through the produces attributeが原文。わからん…

*5:and it will be resolved accordingly, to the actual value if necessary.が原文。訳に自信がない

*6:リンク切れしてるんで適宜ぐぐる

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

Spring WebFluxってなんだ? って感じだったんで https://docs.spring.io/spring/docs/5.0.3.RELEASE/spring-framework-reference/web-reactive.html を読んでテキトーに訳した。なおマッタクといっていいほど推敲してないです。

Web on Reactive Stack

このパートでは、Netty, Undertow, Servlet 3.1+コンテナなどノンブロッキングサーバ上で動作するReactive Streams APIでwebアプリケーションを構築する、リアクティブスタック(reactive stack)について解説します。Spring WebFluxフレームワーク、リアクティブWebClientTesting、についてのチャプターがあります。ServletスタックについてはWeb on Servlet Stackを参照してください。

1. Spring WebFlux

1.1. Introduction

Spring Frameworkに含まれるSpring Web MVCServlet APIServletコンテナ向けに作られました。リアクティブスタックのwebフレームワークWebFluxはversion 5.0で後から追加されました。完全にノンブロッキングで、Reactive Streamsのバックプレッシャをサポートし、Netty, Undertow, Servlet 3.1+コンテナなどのサーバ上で動作します。

spring-webmvcspring-webfluxとソースモジュール名は似たものなっており、Spring Frameworkで共存しています。各モジュールはオプショナルです。アプリケーションはどちらか一方を使ってもよいし、両方使っても構いません。たとえば、Spring MVCのcontrollerをリアクティブなWebClientにするなど。

1.1.1. Why a new web framework?

理由の一つは、少数のスレッドで並行処理し、少ないハードウェアリソースでスケールする、ノンブロッキングなwebスタックに対するニーズです。Servlet 3.1はノンブロッキングI/OのAPIを提供しました。しかし、これを利用すると、同期(Fileter, Servlet)やブロッキングgetParameter, getPart)を契約とするServlet APIの部分から離れることになります。これが何らかのノンブロッキングランタイムの基盤として振る舞う新しい共通APIが求められた背景です。Nettyなどのサーバは非同期・ノンブロッキング空間で確立するのでこのことは重要です。

もう一つの理由は関数型プログラミングです。Java 5で追加されたアノテーションが、アノテーションベースのRESTコントローラーやユニットテストなど、新たな可能性を切り開いたように、Java 8のラムダ式追加は関数型APIの可能性を開きました。これらは、CompletableFutureReactiveXで一般的となった、ノンブロッキングアプリケーションとcontinuation style APIに恩恵を与えました。これらでは非同期ロジックの宣言的なコンポジションが可能です。プログラミングモデルのレベルでは、Java 8によって、Spring WebFluxはコントローラと共にファンクショナルなwebエンドポイントを提供できるようになりました。

1.1.2. Reactive: what and why?

ノンブロッキングとファンクショナルについては上記で触れましたが、リアクティブの理由とその意味については以下で説明します。

リアクティブ""reactive")という用語は、I/Oイベントに反応するネットワークコンポーネント、マウスイベントに反応するUIコントローラーなど、変更に対する反応で構築するプログラミングモデルを指します。そういう意味でノンブロッキングはリアクティブで、操作完了やデータ到着など、ブロッキングではなく通知に反応するモードを使用します。 また、Springチームが"リアクティブ"と関連付けたもう一つの重要なメカニズムにノンブロッキングのバックプレッシャがあります。非同期・命令型(imperative code)・ブロッキングの呼び出しでは、バックプレッシャは呼び出し元に待機を自然と強制する形になります。ノンブロッキングでは、イベントのレート制御が重要で、これは高速なプロデューサーがその宛先(destination)を溢れさせないようにするためです。

Reactive Streamsの仕様は小さく(small spec)Java 9で採用*1され、非同期コンポーネントとバックプレッシャ間の相互作用を定義しています。たとえばデータリポジトリ(data repository)は、Publisherとしての振る舞いはHTTPサーバのデータをプロデュースし、それからSubscriberとしての振る舞いはレスポンスを書き込みます。パプリッシャのデータプロデュースを、サブスクライバでどのくらい高速or低速に制御させるか、がReactive Streamsの主目的となります。

Common question: パブリッシャがスローダウン出来ない場合は?
eactive Streamsの目的は、あくまでも、メカニズムと境界を確立するものです。パブリッシャがスローダウン出来ない場合、バッファリング・廃棄・失敗、するかどうかを決定します。

1.1.3. Reactive API

Reactive Streamは相互運用性(interoperability)で重要な役割を担います。ライブラリと基盤コンポーネントに関心事が存在し、低レベルなのでアプリケーションAPIとしてはあまり有用ではありません。アプリケーションは、Java 8のStream APIのようだがコレクションに限定しない、非同期ロジックをまとめる関数型のAPIという高レベルでリッチなAPIを必要とします。リアクティブのライブラリの役割はこれです。

Spring WebFluxはリアクティブのライブラリにRactorを使用します。

ReactiveXのvocabulary of operatorsで定められた豊富な演算子で0..1および0..Nのデータシーケンスを動作させるMonoFlux APIがあります。ReactorはReactive Streamsのライブラリなので演算子はすべてノンブロッキングのバックプレッシャをサポートします。ReactorはサーバーサイドJavaを強く意識しており、Springと密に意見交換しながら開発されています。

WebFluxはコア依存性としてReactorを要求しますが、Reactive Streams経由で別のリアクティブライブラリとの相互運用性が存在します。通常、WebFlux APIは入力にプレーンなPublisherを取り、内部的にReactor型に変換して使用し、出力にFluxMonoのどちらかを返します。入力に何らかのPublisherを渡して出力で操作を適用できますが、それとは別のリアクティブライブラリでこれを使うには出力を変換する必要があります。変換可能な場合であれば、たとえばアノテーションを付与したコントローラーでは、WebFluxはRxJavaもしくはその他のリアクティブライブラリに透過的に変換します。

1.1.4. Programming models

spring-webモジュールにはリアクティブの基礎部分が含まれ、これにはHTTPの抽象化を含むSpring WebFlux、サポートするサーバ向けのReactive StreamsアダプタコーデックServlet API相当(ただしノンブロッキング)のコアWebHandler API、があります。

これら基礎部分の上にSpring WebFluxは二つのプログラミングモデルを提供しています。

1.1.5. Choosing a web framework

Spring MVCとWebFluxのどちらを選べば良いか。いくつかの異なる視点から見ていきます。

既に稼働中のSpring MVCアプリケーションがある場合、何も変える必要はありません。命令型プログラミングは、書きやすく、理解しやすくて、デバッグもしやすいです。歴史的に大半のライブラリはブロッキングなので過去の資産を最大限生かせます。

既にノンブロッキングのwebスタックの採用経験がある場合、 Spring WebFluxからそこのwbスタックと同一の実行モデルによる利点が得られます。また、サーバの選択肢、Netty, Tomcat, Jetty, Undertow, Servlet 3.1+コンテナ、プログラミングモデルの選択肢、アノテーション付与のコントローラとファンクショナルエンドポイント、リアクティブライブラリの選択肢、Reactor, RxJava, など、があります。

Java 8のラムダやKotlinで使うための軽量な関数型webフレームワークに関心がある場合はSpring WebFluxのファンクショナルエンドポイントを使います。小規模アプリケーションか、透過性と制御性で要求の複雑さを抑えるマイクロサービスでの選択肢の一つになります。

マイクロサービスアーキテクチャでは、 Spring MVCもしくはSpring WebFluxのコントローラを使うアプリケーションを混在するか、Spring WebFluxのファンクショナルエンドポイントを用います。両方のフレームワークとも同じアノテーションベースのプログラミングモデルをサポートしているので知識の再利用はしやすいです。ただし妙な使い方をしない場合に限る*3

アプリケーションを評価する方法の一つは依存性を調べることです。ブロッキングの永続化API(JPA, JDBC)や、ネットワークAPIがある場合、少なくともSpring MVCは良くあるアーキテクチャ向けに最適です。技術的には、ReactorもRxJavaも異なるスレッドでブロッキングAPIを実行することで適応可能ですが、ノンブロッキングwebスタックを最大限活用しているとは言えません。

Spring MVCアプリケーションでリモートサービス呼び出しがある場合、リアクティブのWebClientを検討してください。Spring MVCのコントローラメソッドで直接リアクティブの型(Reactor, RxJava, その他)を返せます。呼び出しごとのレイテンシや、呼び出し間の相互依存性が大きいほど、大きなメリットを得られます。Spring MVCのコントローラは同様にその他のリアクティブコンポーネントも呼び出せます。

大規模チームの場合、ノンブロッキング・関数型・宣言的プログラミングに移行する際の急激な学習曲線に気を付けてください。完全には移行しない現実的な方法としてリアクティブのWebClientから始めるものがあります。スモールスタートの後に効果を測定してください。我々の想定では、アプリケーションの大部分を移行する必要は無い、と考えています。

今一つメリットが分からない場合、ノンブロッキングI/Oの動作と効果を知る事から始めてください(例:シングルスレッドNode.jsの並行処理、は矛盾してるわけではありません)。"scale with less hardware"はキャッチフレーズで何らかの効果を保証するものではないし、スローダウンしたり予測不能なネットワークI/Oが無いわけでは無いです。Netflixblog postが好例です。

1.1.6. Choosing a server

Spring WebFluxはNetty, Undertow, Tomcat, Jetty, Servlet 3.1+コンテナでサポートしています。サーバは共通のReactive Streams APIに対応しています。Spring WebFluxのプログラミングモデルはその共通API上に作られています。

Common question: 両方のスタックでTomcatとJettyを使うには
TomcatとJettyのコアはノンブロッキングです。ブロッキングファサードを追加するServlet APIがそれに当たります。3.1からServlet APIはノンブロッキングI/Oを追加しています。ただし、同期やブロッキングを避ける必要があります。そのため、SpringのリアクティブwebスタックにはReactive Streamsとブリッジする低レベルなServletアダプタがありますが、Servlet APIを直接使う方法は公開していません。

Spring Boot 2はデフォルトではWebFluxを使用し、これはNettyが非同期・ノンブロッキングで広く使われており、また、クライアントとサーバの両方でリソースを共有できます。Nettyと比較するとServlet 3.1のノンブロッキングI/Oは、敷居が高いため、あまり使われていません。Spring WebFluxは導入の糸口となります。

Spring Bootのデフォルトサーバはすぐに使い始めるための意味合いが強いです。アプリケーションでは、パフォーマンス最適化・完全ノンブロッキング・Reactive Streamsバックプレッシャに変換、を行うその他のサーバを選択可能です。Spring Bootで別のサーバにスイッチするのは簡単です。

1.1.7. Performance vs scale

Performanceは様々な特徴と意味合いを持っています。Reactiveとノンブロッキングは基本的にはアプリケーションを高速にはしません。例えば、WebClientでパラレルにリモート呼び出しを実行すると、高速になる場合があります。一般論として、ノンブロッキング化には別途の開発が必要で、処理時間が少々上昇する可能性があります。

リアクティブとノンブロッキングに期待されるメリットは、小規模・固定スレッド数・省メモリでスケールする能力です。これにより、アプリケーションは予測可能な方法でスケールするので負荷に弾力性を持つようになります。ただし、これらの利点が活きるには、低速で予測不能なネットワークI/Oの混在するレイテンシの場合です。そういう場所ではリアクティブスタックは強靭さを発揮し、劇的な違いを見せるでしょう。

1.2. Reactive Spring Web

spring-webモジュールには、リアクティブwebアプリケーションを構築するための、クライアントとサーバ双方の、低レベル基盤とHTTP抽象化があります。実装にReactorを使用するReactive Streams上にすべてのpubli APIは構築されています。

サーバは二つのレイヤーに分けられます。

  • HttpHandlerとサーバアダプタ。Reactive StreamsバックプレッシャでHTTPリクエストを処理する、最も基本的な共通API
  • WebHandler API - やや高レベルなフィルターチェーンで処理をする汎用サーバweb API

1.2.1. HttpHandler

HTTPサーバはHTTPリクエストを処理するための何らかのAPIを備えています。HttpHandlerは、リクエストとレスポンスを処理する一つのメソッドからなる、シンプルな契約(simple contract)です。このAPIは意図的に小さくしています。その主な用途は、異なるサーバでHTTPリクエストを処理するReactive StreamsベースのAPIという、汎用部品の提供です。

spring-webモジュールにはサポート対象サーバ別のアダプタが含まれます。以下の表は使用されるサーバAPIとReactive Streamsのサポート方法です。

Server name Server API used Reactive Streams support
Netty Netty API Reactor Netty
Undertow Undertow API spring-web: Undertow to Reactive Streams bridge
Tomcat Servlet 3.1 non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge
Jetty Servlet 3.1 non-blocking I/O; Jetty API to write ByteBuffers vs byte spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge
Servlet 3.1 container Servlet 3.1 non-blocking I/O spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge

以下は必要となる依存性、サポートバージョン、各サーバ別のコード例です。

Server name Group id Artifact name
Reactor Netty io.projectreactor.ipc reactor-netty
Undertow io.undertow undertow-core
Tomcat org.apache.tomcat.embed tomcat-embed-core
Jetty org.eclipse.jetty jetty-server, jetty-servlet

Reactor Netty:

HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create(host, port).newHandler(adapter).block();

Undertow:

HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();

Tomcat:

HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);

Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();

Jetty:

HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);

Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();

ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();

Servlet 3.1+コンテナにWARとしてデプロイするには、ServletHttpHandlerAdapterHttpHandlerをラップして、Servletとして登録します。AbstractReactiveWebInitializerを使用する場合は自動的に行われます。

1.2.2. WebHandler API

HttpHandlerは異なるHTTPサーバ上で動かすための最も低レベルな契約です。これの上の、WebHandler APIはそれより少し上のレベルですが、WebExceptionHandler's, WebFilter's, WebHandlerのチェーンを形成する汎用用途のコンポーネント群になります。

WebHandler APIのすべてのコンポーネントは入力にServerWebExchangeを取り、このクラスの裏では、リクエスト変数・セッション変数・パースしたフォームデータへのアクセス・マルチパートなど、webアプリケーションで使用する構成要素を提供するためのServerHttpRequestServerHttpResponseが存在します。

WebHttpHandlerBuilderはリクエスト処理チェーンをアセンブルするのに使われます。コンポーネントを手動で追加するにはこのビルダーを使うか、たいていはSpringのApplicationContextから取得することになるHttpHandlerは、サーバアダプタ経由の実行準備が出来た状態で取得できます。

ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build()

以下の表はWebHttpHandlerBuilderが検出するコンポーネントのリストです。

Bean name Bean type Count Description
WebExceptionHandler 0..N すべてのWebFilterとそのターゲットWebHandlerの後に適用する例外ハンドラ
WebFilter 0..N ターゲットWebHandlerの前後で実行するフィルタ
"webHandler" WebHandler 1 リクエストハンドラ
"webSessionManager" WebSessionManager 0..1 ServerWebExchangeのメソッド経由で公開されるWebSession用のマネージャ。デフォルトはDefaultWebSessionManager
"serverCodecConfigurer" ServerCodecConfigurer 0..1 フォームとマルチパートをパースするHttpMessageReaderにアクセスする時に使う。ServerWebExchangeのメソッド経由で公開される。デフォルトはServerCodecConfigurer.create()
"localeContextResolver" LocaleContextResolver 0..1 ServerWebExchangeのメソッド経由で公開されるLocaleContext用のリゾルバ。デフォルトはAcceptHeaderLocaleContextResolver

Form Reader

ServerWebExchangeはフォームデータにアクセスする以下の公開メソッドがあります。

Mono<MultiValueMap<String, String>> getFormData();

DefaultServerWebExchangeは設定されているHttpMessageReaderでフォームデータ("application/x-www-form-urlencoded")をMultiValueMapにパースします。デフォルトではFormHttpMessageReaderServerCodecConfigurerのbeanを使用して設定されます。(Web Handler API参照)

Multipart Reader

Spring MVCと同様

ServerWebExchangeはマルチパートにアクセルする以下の公開メソッドがあります。

Mono<MultiValueMap<String, Part>> getMultipartData();

DefaultServerWebExchangeは設定されているHttpMessageReader<MultiValueMap<String, Part>>で"multipart/form-data"をMultiValueMapにパースします。現状、サポートされているサードパーティライブラリはSynchronoss NIO Multipartだけで、我々の知る限りではこのライブラリはマルチパートリクエストのノンブロッキングなパースが出来ます。ServerCodecConfigurerのbeanを通して有効化します。(Web Handler API

ストリーミングのマルチパートをパースするには、HttpMessageReader<Part>から返されるFlux<Part>を代わりに使います。たとえば、@RequestPartを使うコントローラーはnameとパートが対応するMapライクのアクセスを意味し、そのため、完全なマルチパートのパースが必要になります。対照的に、Flux<Part>のコンテンツをデコードするのに@RequestBodyを使用可能で、その際MultiValueMapを生成しません。

1.2.3. HTTP Message Codecs

Spring MVCと同等

spring-webモジュールはRective StreamsのPublisherを介してHTTPのリクエストとレスポンスボディのエンコードとデコードをするためのHttpMessageReaderHttpMessageWriterを定義しています。これらはクライアント側で使うものでは例えばWebClient、サーバ側ではアノテーションを付与するコントローラとファンクショナルエンドポイントで使います。

spring-coreモジュールはEncoderDecoderを定義しており、これらはHTTPに非依存で、NettyのByteBufjava.nio.ByteBufferなど異なるバイトバッファ表現を抽象化するDataBufferを使用します(Buffers and Codecsを参照)。EncoderHttpMessageWriterとして使うためにEncoderHttpMessageWriterでラップ可能で、DecoderHttpMessageReaderとして使うためにDecoderHttpMessageReaderでラップ可能です。

spring-coreモジュールには、byte[], ByteBuffer, DataBuffer, Resource, String、で使う基本的なEncoderDecoderの実装があります。spring-webモジュールにはJackson JSON, Jackson Smile, JAXB2で使うEncoderDecoderを追加しています。また、spring-webモジュールには、server-sentイベント、フォームデータ、マルチパートリクエストで使うweb固有のreaderとwriterがあります。

アプリケーションで使うためにreaderとwriterを設定したりカスタマイズするには、基本的にはClientCodecConfigurerServerCodecConfigurerを使います。

Jackson JSON

decoderはバイトのチャンクストリームをTokenBufferストリームへパースするのにJacksonのノンブロッキングなバイト配列パーサーを用います。これはJacksonのObjectMapperに変換できます。

encoderは以下のようにPublisher<?>を処理します。

  • PublisherMono(つまり単一の値)の場合、値はJSONにエンコードされる。
  • メディアタイプがapplication/stream+jsonの場合、Publisherが生成するそれぞれの値はJSONに改行つきでエンコードされます。
  • 上記以外の場合、Publisherのすべての中身はFlux#collectToList()に集約され、このコレクションがJSON配列にエンコードされます。

上記ルールの特殊ケースに、ServerSentEventHttpMessageWriterは入力のPublisherの内容をそれぞれMono<?>としてJackson2JsonEncoderに送ります。

注意点として、Jackson JSON encoderとdecoderはString型のレンダリング要素を明示的に返します。Instead String's are treated as low level content, (i.e. serialized JSON) and are rendered as-is by the CharSequenceEncoder.*4 JSON配列としてレンダリングされるFlux<String>にしたい場合、Flux#collectToList()を使用してMono<List<String>>にします。

1.3. DispatcherHandler

Spring MVCと同等

Spring WebFluxやSpring MVCはfront controller patternで設計されており、その中心にあるWebHandlerDispatcherHandlerは、リクエスト処理に共通なアルゴリズムがあり、実際の動作は設定可能でコンポーネントに処理を委譲します。このモデルは柔軟性があり多用な処理の流れに対応できます。

DispatcherHandlerはコンポーネントに処理を委譲し、委譲先はSpringのconfigurationから取得します。また、それ自身もSpringのbeanで、このbeanを実行するcontextにアクセスするためにApplicationContextAwareを実装しています。DispatcherHandlerはbean名"webHandler"で宣言されているので、これによってWebHttpHandlerBuilderでwebHandlerが参照可能となり、WebHandler APIで解説したリクエスト処理チェーンとの結びつけを行います。

WebFluxアプリケーションのSpring configurationは基本的には以下があります。

処理のチェーンを構築するためにWebHttpHandlerBuilderにconfigurationが渡されます。

ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context);

戻り値HttpHandlererver adapterで使える状態になっています。

1.3.1. Special bean types

Spring MVCと同等

DispatcherHandlerはリクエスト処理と適切なレスポンスレンダリングに別のbeanへ委譲します。Springマネージドオブジェクトインスタンスは以下表のフレームワークのクラスの一つを実装します。SpringのWebFluxはそうしたクラスのビルトイン実装を提供しており、カスタマイズ・拡張・置換も可能です。

Bean type Explanation
HandlerMapping リクエストとハンドラのマッピング。マッピングは何らかの基準をベースにするもので、HandlerMappingの実装に依ります。アノテーションを付与するコントローラー、シンプルなURLパターンマッピング、など。
メインとなるHandlerMapping実装は、@RequestMappingを付与するメソッドに基づくRequestMappingHandlerMapping、ファンクショナルエンドポイントのルーティングに基づくRouterFunctionMapping、ハンドラにURLパスパターンを明示的に登録するSimpleUrlHandlerMapping、があります。
HandlerAdapter リクエストにマッピングされているハンドラを呼び出すDispatcherHandlerのヘルパーです。ハンドラ呼び出しの実装に無関係の処理が担当です。例えば、コントローラ呼び出し時に必要となるアノテーション解決などです。HandlerAdapterの主な用途はそういた詳細をDispatcherHandlerから分離することです。
HandlerResultHandler ハンドラ呼び出し結果を処理してレスポンスを確定します。
ビルトインのHandlerResultHandler実装は、戻り値ResponseEntityResponseEntityResultHandler@ResponseBodyメソッドのResponseBodyResultHandler、ファンクショナルエンドポイントが返すServerResponseServerResponseResultHandler、viewとmodelでレンダリングするViewResolutionResultHandler、があります。

1.3.2. Framework Config

Spring MVCと同等

DispatcherHandlerApplicationContext内で自身が必要とするbeanを検出します。アプリケーションで必要に応じてそうしたbeanを宣言します。ただし、WebFluxのJava configは高レベルのAPIを設定済みで、ここに必要なbean宣言がしてあるので、これを起点に出来ます。詳細はWebFlux Configを参照してください。

1.3.3. Processing

Spring MVCと同等

DispatcherHandlerは以下のようにリクエストを処理します。

  • HandlerMappingそれぞれにハンドラマッチングを問い合わせ、最初にマッチしたものが使われる。
  • マッチするハンドラがある場合、適当なHandlerAdapterを介してハンドラを実行し、HandlerResultで実行結果を返す。
  • HandlerResultHandlerHandlerResultが与えられます。HandlerResultHandlerは処理を完了させるためのもので、レスポンスに直接書き込んだり、レンダリングするビューを使用したりします。

*1:リンク切れてるけどflow apiとかでぐぐる

*2:Functionalをファンクショナルとも関数型とも訳しちゃっててブレまくりだけど許して

*3:while also selecting the right tool for the right jobが原文。向いてる案件に向いてるツールを使うこと、って感じですかね?

*4:よくわからん