kagamihogeの日記

kagamihogeの日記です。

アトラス作品ファンのオフ会 真・眼鏡祭Ⅳ(2018/04/07)に行ってきた

4/7 真・眼鏡祭Ⅳ - Twiplaに行ってきました。

f:id:kagamihoge:20180407142821j:plain

眼鏡祭はペルソナを初めとするアトラス作品ファンコミュニティの大規模オフ会です。参加者数は200人強と、当日がPERSONA5 the Animationの第一話放映日、P3D - ペルソナ3 ダンシング・ムーンナイトおよびP5D - ペルソナ5 ダンシング・スターナイトの発売を控えてか、今回も大変な盛況ぶりでした。天気予報では雨が危ぶまれましたが、天候は晴れ。場所はキリストンカフェ東京を貸し切っての開催です。

当日の様子はTwitterハッシュタグ#眼鏡祭0407から追えます。

アトラス作品のコスプレがたくさんいるオフ会

眼鏡祭の特徴は、色々ありますが、なんといっても目を引くのはコスプレOKな点です。

こちらの全体写真はコスプレ参加者ほぼ全員の集合写真です。キャラクターの分布としては、今回はやはりP5が一大勢力でした。怪盗団やサブキャラクター、早くもP5D衣装の方もいました。ジョーカーは当然のように大人気です。参加者総数のうち7~8割は何らかのコスプレをしているため、集合写真のインパクトはかなりのインパクトがあります。

P5の勢いがあるため他作品は相対的に少ない傾向にありました。しかし、P3やP4のダンス衣装、D×2 真・女神転生 リベレーション魔神転生、女神異聞録、ペルソナ2、など、今となっては懐かしい作品をこよなく愛するコスプレの方、異形の悪魔コスなど、P5と比べれば少数派であっても、相変わらず存在感がありました。

主催曰く「コンテンツは参加者自身」

眼鏡祭はいわゆるオフ会で、その目的は参加者同士のリアルなコミュニケーションです。お酒を飲み、コスプレ肴に、ゲーム・アニメ・マンガなどオタクの話題で気兼ねなく一日中盛り上がる。基本的にはそれだけです。しかし、コスプレを良くするレイヤーさんに聞いてみても、コスプレOKで交流がメイン、しかもアトラス作品に絞った会でここまで大規模なものは他に無いようです。このシンプルだが強力なコンセプトに様々な人が魅力を感じ、集まって来ています。

参加者の年代は、10年近く続いている会ゆえに、30代以上が多数です。ただし、アトラス作品はP5が初めてで眼鏡祭に来た、という20代もかなり居ます。おおむね1~2割は初参加で、初めて来たのが自分だけ、という事にはなりません。新しい方がどんどん来るので、する事は毎回同じでも、いつも新鮮な驚きがあるのが眼鏡祭のすごいところです。

コスプレは最高の会話のきっかけ

眼鏡祭ではコスプレを仮装と呼びます*1。200人超のイベントで何らかの会話のとっかかりを作るのは、普通に考えれば相当な困難です。そこでコスプレの出番ですが、コスプレと呼べるほど凝ったもので無くてもいい、という意味を込めて「仮装」と呼ばれています。

あるキャラのコスプレをしてるということは、そのキャラや作品に何らかの思い入れがある、その意思表示です。コスプレをとっかかりにして会話が広がっていくのは眼鏡祭ではありふれた光景です。そのためにコスプレを眼鏡祭でだけはする、という人も一定数存在します。

もちろんコスプレはそれなりの敷居があります。凝ろうとすれば果てが無いですが、簡単なもので眼鏡祭は十分。コスプレに抵抗があるなら、普段使いの難しい各種のグッズを身に付けたり、モルガナのぬいぐるみを持ってきたり、千枝ちゃんのジャージ羽織るだけでも違います。

眼鏡祭ではコスプレの撮影がメインではなく、コミュニケーションの切欠にするのがメインの目的です。テーマパークでテンションを高めるためにグッズや簡単なフェイスペイントなどをしますが、大半の参加者がコスプレをする眼鏡祭はある意味アトラクションのため、自らのテンションを高めないと会場の熱気に押し負けます。そのため、何らかの手段で己のゲージを高めて、色々な人と交流しやすくすることが推奨されています。

初参加しやすくする仕掛け

眼鏡祭には様々な初参加者向けの仕組みがあります。200人のイベントにいきなり放り込まれても困りものなため、まず来場すると7~8人に分けられたテーブルにつきます。この少人数グループで自己紹介(=好きな作品や推しキャラを語ること)をし、乾杯後は自由時間となります。

なお、不安なので知り合い同士で同じテーブルに座りたい、など初参加者向けの配慮は出来る限り応じてくれるため、申込フォームや主催のツイッターなどにあらかじめ書くと良いです。

それで実際のところ初参加で楽しめるのか? については、ステマっぽいですが、実際に初参加の方のツイートを見ると雰囲気が伝わってきます。

コスイベとは違う点がいくつか

一つ注意点があるとすれば、いわゆるコスプレイベントとは性質がかなり違います。例えば、会場はカフェ・レストランなので、撮影スタジオに比べると設備的にはどうしても劣ります。照明は控え目ですし、更衣室はフロアを区切っただけなので暗めな上にさほど広くはないです。特に女子更衣室は年々コスプレ参加者の増加に伴い中々難しい状況にあるようです。

みんなで作る眼鏡祭

眼鏡祭の運営は有志スタッフが行っています。会場や飲食は別にすると、その他すべては有志に頼っているため、眼鏡祭では運営スタッフを募集しています。

眼鏡祭の運営は多岐に渡ります。当日目に見える仕事だけでも、会場設営・受付・更衣室管理・全体進行係に各テーブル進行係があり、ノベルティのイラストや、名札や進行表印刷など各種物品の準備など。金銭管理や会場予約など事務作業もあります。200人を一日中動かす企画・運営なので、世の中に存在するスキルのほとんどはどこかしらで出番があります。

眼鏡祭は本当にすごいイベントで、これに感謝を示す方法は色々ですが、運営スタッフに参加してみるのも一つの手と言えるでしょう。

眼鏡祭から次につなげる

実を言うと、眼鏡祭の当日だけで知り合いを増やすのは至難の業です。200人も居るので覚えるのも覚えられるのも大変な苦労を伴うためです。そのため、もうちょっと人数の少ない眼鏡祭関連のイベントに顔を出してみるのをオススメします。

まずは眼鏡祭おつかれ的なおかわり会。こちらは単なる飲み会です。

ガンプラをわいわい言いながら作る会。

以下は眼鏡祭関連のイベントではないですが、眼鏡勢で行く人はかなり居ると予想されます。

次回の眼鏡祭は7/28に決定したようです。

他にも、眼鏡祭界隈にはお祭り好きがたくさんいるので、食べ歩きやら聖地巡礼やらP3D・P5Dに向けたダンス練習会やら、多種多様なことをやっている人がいます。お誘いがあれば気軽に足を運んでみてはいかがでしょうか。

さいごに

主催のマソーさんはじめ運営スタッフの皆さん、当日俺の話し相手になって頂いた方、常連顔なじみの皆さんなど、今回も非常にたのしい一日を過ごすことが出来ました。また、この日記を書くにあたり、眼鏡祭の雰囲気を伝えるには極めて効果的なため、沢山のツイートを引用させて頂いた方*2にも感謝致します。

それでは次回もよろしくお願い致します。

URL一覧

*1:この日記では一般的な分かりやすさ優先で「コスプレ」を使います

*2:問題があれば面倒ですが https://twitter.com/kagamihoge にお願いします。

Web on Reactive Stack(version 5.0.3.RELEASE) 2. WebClientをテキトーに訳した

https://docs.spring.io/spring/docs/5.0.3.RELEASE/spring-framework-reference/web-reactive.html#webflux-client

2. WebClient

spring-webfluxモジュールには、Reactive Streamsのバックプレッシャを用いるHTTPリクエスト用のノンブロッキング・リアクティブなクライアントが含まれます。

このクライアントはHTTP codecsおよびサーバのfunctional web frameworkの基礎部分は共通です。

WebClientはHTTPクライアントライブラリを下側に置く高レベルのAPIです。デフォルトではReactor Nettyを使いますが、別のClientHttpConnectorを使用してプラグイン可能です。WebClient APIは出力にFluxMonoを返し、入力にReactive StreamsのPublisherを受けます。(Reactive Libraries参照)

RestTemplateと比較すると、WebClientはより関数型でJava 8のラムダを取り入れたfluent APIです。同期・非同期の両方を備え、ストリーミングを含むので、ノンブロッキングI/Oで有用です。

2.1. Retrieve

retrieve()メソッドで簡単にレスポンスボディを取得してデコードできます。

    WebClient client = WebClient.create("http://example.org");

    Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(Person.class);

また、レスポンスからデコードするオブジェクトのストリームを得られます。

    Flux<Quote> result = client.get()
            .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
            .retrieve()
            .bodyToFlux(Quote.class);

デフォルトでは、4xxか5xxのステータスコードのレスポンスはWebClientResponseExceptionのエラーになりますが、これはカスタマイズ可能です。

    Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .onStatus(HttpStatus::is4xxServerError, response -> ...)
            .onStatus(HttpStatus::is5xxServerError, response -> ...)
            .bodyToMono(Person.class);

2.2. Exchange

exchange()ではより細かい制御ができます。以下の例はretrieve()と同等ですが、ClientResponseにアクセスしています。

    Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.bodyToMono(Person.class));

この段階で、完全なResponseEntityを生成することもできます。

    Mono<ResponseEntity<Person>> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.toEntity(Person.class));

retrieve()と異なり、exchange()では、4xxと5xxレスポンスにおける自動的なエラー通知はありません。自前でステータスコードをチェックして続行するかを決めるコードを書く必要があります。

exchange()を使う場合、ClientResponseのtoEntityかbodyメソッドのいずれかを必ず使用してください。これは、リソースを開放してHTTPコネクションプーリングに潜在的な問題を抱えるのを回避するためです。もしレスポンスの中身が無い場合はbodyToMono(Void.class)を使います。ただし、レスポンスの中身がある場合、コネクションは閉じてプールには戻らない点に注意してください。

2.3. Request body

リクエストボディはObjectからエンコードします。

    Mono<Person> personMono = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .body(personMono, Person.class)
            .retrieve()
            .bodyToMono(Void.class);

エンコードされたオブジェクトのストリームも使えます。

    Flux<Person> personFlux = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_STREAM_JSON)
            .body(personFlux, Person.class)
            .retrieve()
            .bodyToMono(Void.class);

値そのままを使う場合、syncBodyというショートカットのメソッドを使います。

    Person person = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .syncBody(person)
            .retrieve()
            .bodyToMono(Void.class);

2.3.1. Form data

フォームデータの送信にはボディにMultiValueMap<String, String>を使用します。なお、MultiValueMap<String, String>によって自動的に"application/x-www-form-urlencoded"をセットします。

    MultiValueMap<String, String> formData = ... ;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .syncBody(formData)
            .retrieve()
            .bodyToMono(Void.class);

BodyInsertersでインラインにフォームデータを記述できます。

    import static org.springframework.web.reactive.function.BodyInserters.*;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .body(fromFormData("k1", "v1").with("k2", "v2"))
            .retrieve()
            .bodyToMono(Void.class);

2.3.2. Multipart data

マルチパートデータの送信にはMultiValueMap<String, ?>を使用し、その値には、パートのボディを表すObjectか、パートのボディとヘッダーを表すHttpEntity、のどちらかを使います。パートを作るにはMultipartBodyBuilderを使います。

    MultipartBodyBuilder builder = new MultipartBodyBuilder();
    builder.part("fieldPart", "fieldValue");
    builder.part("filePart", new FileSystemResource("...logo.png"));
    builder.part("jsonPart", new Person("Jason"));

    MultiValueMap<String, HttpEntity<?>> parts = builder.build();

    Mono<Void> result = client.post()
            .uri("/path", id)
            .syncBody(parts)
            .retrieve()
            .bodyToMono(Void.class);

なお、各パートのコンテンツタイプは書き込むファイルの拡張子かObjectの型に基づいて自動設定します。また、各パートごとにコンテンツタイプを明示的に指定することも可能です。

BodyInsertersでインラインにマルチパートデータを記述するやり方もあります。

    import static org.springframework.web.reactive.function.BodyInserters.*;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
            .retrieve()
            .bodyToMono(Void.class);

2.4. Builder options

WebClientの生成は簡単で、staticファクトリメソッドcreate()と、すべてのリクエストのベースとなるURLを与えるcreate(String)で行います。WebClient.builder()にはより細かいオプションがあります。

基底のHTTPクライアントをカスタマイズするには以下のようにします。

    SslContext sslContext = ...

    ClientHttpConnector connector = new ReactorClientHttpConnector(
            builder -> builder.sslContext(sslContext));

    WebClient webClient = WebClient.builder()
            .clientConnector(connector)
            .build();

HTTPメッセージのエンコード・デコードで使われるHTTP codecsをカスタマイズするには以下のようにします。

    ExchangeStrategies strategies = ExchangeStrategies.builder()
            .codecs(configurer -> {
                // ...
            })
            .build();

    WebClient webClient = WebClient.builder()
            .exchangeStrategies(strategies)
            .build();

ビルダーはFiltersを追加するのに使用します。

URIの構築やデフォルトヘッダー(とクッキー)などその他のオプションについてはIDEWebClient.Builderを確認してみてください。

WebClientの作成後は、新しくWebClientを生成するための新しいbuilderを常に取得可能です。元のインスタンスをベースにしますが、現在のインスタンスには何も影響を与えません。

    WebClient modifiedClient = client.mutate()
            // user builder methods...
            .build();

2.5. Filters

WebClient supports interception style request filtering:

    WebClient client = WebClient.builder()
            .filter((request, next) -> {
                ClientRequest filtered = ClientRequest.from(request)
                        .header("foo", "bar")
                        .build();
                return next.exchange(filtered);
            })
            .build();

ExchangeFilterFunctionsはベーシック認証用のフィルターです。

    // static import of ExchangeFilterFunctions.basicAuthentication

    WebClient client = WebClient.builder()
            .filter(basicAuthentication("user", "pwd"))
            .build();

また、オリジナルのインスタンスに影響を与えることなくWebClientインスタンスを変更できます*1

*1:You can also mutate an existing WebClient instance without affecting the original:が原文。mutateを「変更」と訳すのはだいぶニュアンスが失われているとは思うが、あんま良い日本語浮かばず。

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