kagamihogeの日記

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を「変更」と訳すのはだいぶニュアンスが失われているとは思うが、あんま良い日本語浮かばず。