2. WebClient
spring-webflux
モジュールには、Reactive Streamsのバックプレッシャを用いるHTTPリクエスト用のノンブロッキング・リアクティブなクライアントが含まれます。
このクライアントはHTTP codecsおよびサーバのfunctional web frameworkの基礎部分は共通です。
WebClient
はHTTPクライアントライブラリを下側に置く高レベルのAPIです。デフォルトではReactor Nettyを使いますが、別のClientHttpConnector
を使用してプラグイン可能です。WebClient
APIは出力にFlux
かMono
を返し、入力に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
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の構築やデフォルトヘッダー(とクッキー)などその他のオプションについてはIDEでWebClient.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を「変更」と訳すのはだいぶニュアンスが失われているとは思うが、あんま良い日本語浮かばず。