kagamihogeの日記

kagamihogeの日記です。

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:よくわからん

JEP 323: Local-Variable Syntax for Lambda Parametersをテキトーに訳した

http://openjdk.java.net/jeps/323 を読んだ。

JEP 323: Local-Variable Syntax for Lambda Parameters

Author   Brian Goetz
Owner   Vicente Arturo Romero Zaldivar
Created 2017/12/08 15:15
Updated 2018/02/12 17:25
Type    Feature
Status  Targeted
Component   specification/language
Scope   SE
Discussion  amber dash dev at openjdk dot java dot net
Effort  XS
Duration    XS
Priority    3
Reviewed by Alex Buckley
Release 11
Issue   8193259
Relates to  JEP 286: Local-Variable Type Inference

Summary

暗黙的に型指定されるラムダ式(implicitly typed lambda expressions)の仮引数宣言でvarを使用可能にします。

Goals

ローカル変数宣言と暗黙的に型指定されるラムダ式の仮引数宣言でのシンタックスを揃える。

Non-goals

メソッドの仮引数などその他の変数宣言は対象外。

Motivation

ラムダ式は暗黙的な型指定が可能で、この場合はすべての仮引数は型推論されます。

(x, y) -> x.process(y)    // 暗黙的に型指定されるラムダ式

Java SE 10ではローカル変数で暗黙的な型指定が使用可能になります。

var x = new Foo();
for (var x : xs) { ... }
try (var x = ...) { ... } catch ...

ローカル変数の統一感の観点から、暗黙的に型指定されるラムダ式の仮引数でも'var'を使えるようにしたい、と我々は考えました。

(var x, var y) -> x.process(y)   // 暗黙的に型指定されるラムダ式

統一感によるメリットには、修飾子やnotably annotationsをローカル変数とラムダ式の仮引数に、簡潔さを失うことなく、適用できる点です。

@Nonnull var x = new Foo();
(@Nonnull var x, @Nullable var y) -> x.process(y)

Description

暗黙的に型指定されるラムダ式の仮引数で、予約済みの型名varを使用可能にします。

(var x, var y) -> x.process(y)

上記は以下と同等です。

(x, y) -> x.process(y)

暗黙的に型指定されるラムダ式は、すべての仮引数でvarを使うか、全く使わないか、のどちらかでなければなりません。また、varは暗黙的に型指定されるラムダ式の仮引数でだけ使用可能で、明示的に型指定されるラムダ式では引き続きすべての仮引数でマニフェスト型(manifest types)を指定します。よって、マニフェスト型の仮引数を使いつつvarも使うことは出来ません。以下の使い方は出来ません。

(var x, y) -> x.process(y)         // 暗黙的に型指定されるラムダ式で'var'と'非var'を一緒には使えない
(var x, int y) -> x.process(y)     // 暗黙的に型指定されるラムダ式で'var'とマニフェスト型を一緒には使えない

理屈の上では、上記例の下側、半明示的な型指定(semi-explicitly typed )(もしくは半暗黙的な型指定(semi-implicitly typed)、どちらに重きを置くかの見方の違いに依る)のようなラムダ式は可能に見えます。しかし、このJEPではそれはスコープ外とし、これは型推論オーバーロード解決に深く影響するためです。これが、ラムダ式ですべてマニフェスト引数型にするか全くしないか、という制限を設ける主な理由です。同様に、暗黙的に型指定されるラムダ式の引数での型推論varを使うか全く使わないか、という制限を設ける考えです。将来のJEPでこの部分推論(partial inference)の課題に取り組むかもしれません。また、省略のシンタックスの簡潔さを損ないたくないので、以下のような表現が出来るようにはしない予定です。*1

var x -> x.foo()

Alternatives

従来通りJava SE 8 の暗黙的に型指定されるラムダ式の宣言を使用し続ける。

Risks and Assumptions

このJEPの、暗黙的に型指定されるラムダ式の引数名の前にvarを追加するのは、ソース互換性に関してはノーリスクです。これはvarなし引数の型推論var型推論は同一なためです。

*1:Also, we do not wish to compromise the brevity of the shorthand syntax, so we won't allow expressions like:が原文。compromise がよくわからん