--- title: Spring Framework 6から導入される宣言的HTTP Clientを試す tags: ["Reactor", "Reactor Netty", "Netty", "Spring 6", "Spring WebFlux", "Spring MVC", "Java"] categories: ["Programming", "Java", "org", "springframework", "web", "service", "invoker"] date: 2022-06-08T06:49:43Z updated: 2022-06-08T16:24:55Z --- Spring Framework 6から[Retfofit](https://square.github.io/retrofit/)や[Feign](https://github.com/OpenFeign/feign)のような[宣言的なHTTP Client](https://docs.spring.io/spring-framework/docs/6.0.0-M4/reference/html/integration.html#rest-http-interface)が利用可能になります。 以下はSpring I/O 2022で発表されたセッションです。 > ℹ️ 今後、宣言的なRSocket Clientもサポートされるようです。 早速試してみたいと思います。 * Spring Boot 3.0.0-M3 * Spring Framework 6.0.0-M4 * Java 17 で試しました。 ソースコードは[こちら](https://github.com/making/demo-declarative-client)です。 [{JSON} Placeholder](https://jsonplaceholder.typicode.com)の[Todo API](https://jsonplaceholder.typicode.com/todos)に対するクライアントを作成します。 HTTP Clientの下回りには`WebClient`が使用されています。そのため、Dependencyには`spring-boot-starter-webflux`が必要です。
たまに誤解されますが、`WebClient`や`Mono`、`Flux`は**Spring MVCでも利用可能です**。 以下ではSprign MVCで宣言的クライアントを使用します。 ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-webflux ``` Todo APIに対する`TodoClient`は次のようなインターフェースで定義できます。
直感的にやりたいことを理解できるのではないでしょうか。Controllerに使用する`@GetMapping`アノテーションとは異なり`@GetExchange`アノテーションを使用します。 ```java package com.example; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.service.annotation.PostExchange; @HttpExchange(url = "/todos") public interface TodoClient { @GetExchange Flux getTodos(); @GetExchange(url = "/{id}") Mono getTodo(@PathVariable("id") Integer id); @PostExchange Mono postTodo(@RequestBody Todo todo); } ``` `Todo`クラスは次の通りです。Recordで定義できます。 ```java package com.example; public record Todo(Integer userId, Integer id, String title, boolean completed) { } ``` このHTTP ClientのBean定義は次のようになります。クライアントのインタフェース毎に`HttpServiceProxyFactory`からクライアントのインスタンスを生成してBean定義します。 ```java package com.example; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.support.WebClientAdapter; import org.springframework.web.service.invoker.HttpServiceProxyFactory; @Configuration public class HttpClientConfig { @Bean public HttpServiceProxyFactory httpServiceProxyFactory(WebClient.Builder builder) { final WebClient webClient = builder.baseUrl("https://jsonplaceholder.typicode.com").build(); return HttpServiceProxyFactory.builder(new WebClientAdapter(webClient)).build(); } @Bean public TodoClient todoClient(HttpServiceProxyFactory proxyFactory) { return proxyFactory.createClient(TodoClient.class); } } ``` > ℹ️ 以下のコードと等価なProxyが生み出されます。 > ```java > package com.example; > > import reactor.core.publisher.Flux; > import reactor.core.publisher.Mono; > > import org.springframework.stereotype.Component; > import org.springframework.web.reactive.function.client.WebClient; > > @Component > public class TodoClientImpl implements TodoClient { > > private final WebClient webClient; > > public TodoClientImpl(WebClient.Builder webClientBuilder) { > this.webClient = webClientBuilder.baseUrl("https://jsonplaceholder.typicode.com").build(); > } > > @Override > public Flux getTodos() { > return this.webClient.post().uri("/todos").retrieve().bodyToFlux(Todo.class); > } > > @Override > public Mono getTodo(Integer id) { > return this.webClient.post().uri("/todos/{id}", id).retrieve().bodyToMono(Todo.class); > } > > @Override > public Mono postTodo(Todo todo) { > return this.webClient.post().uri("/todos").bodyValue(todo).retrieve().bodyToMono(Todo.class); > } > } > ``` `TodoClient`をSpring MVCのControllerからそのまま呼び出したサンプルが次のコードです。
たまに誤解されますが、Spring MVCでもコントローラーのメソッドの返り値に`Flux`や`Mono`をそのまま利用できます。 ```java import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(path = "/todos") public class TodoController { private final TodoClient todoClient; public TodoController(TodoClient todoClient) { this.todoClient = todoClient; } @GetMapping(path = "") public Flux getTodos() { return this.todoClient.getTodos(); } @GetMapping(path = "/{id}") public Mono getTodo(@PathVariable("id") Integer id) { return this.todoClient.getTodo(id); } @PostMapping(path = "") public Mono postTodo(@RequestBody Todo todo) { return this.todoClient.postTodo(todo); } } ``` 実行してリクエストを送ると、次のようなDEBUGログを確認できます。 ``` 2022-06-08T20:56:11.249+09:00 DEBUG 17045 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : GET "/todos/1", parameters={} 2022-06-08T20:56:11.250+09:00 DEBUG 17045 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.TodoController#getTodo(Integer) 2022-06-08T20:56:11.250+09:00 DEBUG 17045 --- [nio-8080-exec-3] o.s.w.r.f.client.ExchangeFunctions : [73f4bb5b] HTTP GET https://jsonplaceholder.typicode.com/todos/1 2022-06-08T20:56:11.251+09:00 DEBUG 17045 --- [nio-8080-exec-3] o.s.w.c.request.async.WebAsyncManager : Started async request 2022-06-08T20:56:11.251+09:00 DEBUG 17045 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : Exiting but response remains open for further handling 2022-06-08T20:56:12.730+09:00 DEBUG 17045 --- [ctor-http-nio-2] o.s.w.r.f.client.ExchangeFunctions : [73f4bb5b] [df74a6ba-2, L:/192.168.11.97:53409 - R:jsonplaceholder.typicode.com/104.21.4.48:443] Response 200 OK 2022-06-08T20:56:12.732+09:00 DEBUG 17045 --- [ctor-http-nio-2] o.s.http.codec.json.Jackson2JsonDecoder : [73f4bb5b] [df74a6ba-2, L:/192.168.11.97:53409 - R:jsonplaceholder.typicode.com/104.21.4.48:443] Decoded [Todo[userId=1, id=1, title=delectus aut autem, completed=false]] 2022-06-08T20:56:12.732+09:00 DEBUG 17045 --- [ctor-http-nio-2] o.s.w.c.request.async.WebAsyncManager : Async result set, dispatch to /todos/1 2022-06-08T20:56:12.733+09:00 DEBUG 17045 --- [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : "ASYNC" dispatch for GET "/todos/1", parameters={} 2022-06-08T20:56:12.733+09:00 DEBUG 17045 --- [nio-8080-exec-4] s.w.s.m.m.a.RequestMappingHandlerAdapter : Resume with async result [Todo[userId=1, id=1, title=delectus aut autem, completed=false]] 2022-06-08T20:56:12.733+09:00 DEBUG 17045 --- [nio-8080-exec-4] m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json] 2022-06-08T20:56:12.734+09:00 DEBUG 17045 --- [nio-8080-exec-4] m.m.a.RequestResponseBodyMethodProcessor : Writing [Todo[userId=1, id=1, title=delectus aut autem, completed=false]] 2022-06-08T20:56:12.734+09:00 DEBUG 17045 --- [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : Exiting from "ASYNC" dispatch, status 200 ``` * スレッド名が`[nio-8080-exec-3]`と出ているのはTomcatのスレッドがリクエストを受けて、`TodoClient`がHTTPリクエストを送る前までの処理のスレッド上でのログです。 * スレッド名が`[ctor-http-nio-2]`と出ているのは`TodoClient`がHTTPリクエストを送り、HTTPレスポンスを受け取ってデコードするまでの処理のスレッド上でのログです。 * スレッド名が`[nio-8080-exec-4]`と出ているのはTomcatのスレッドがHTTPレスポンスを書き込むまでの処理のスレッド上でのログです。 コントローラーのメソッドの返り値に`Flux`や`Mono`を使うとServletの非同期処理が行われ、リクエストとレスポンスで異なるスレッドが使われます。 `Flux`や`Mono`に馴染みがなく、これまでの通りのブロッキングなAPI呼び出しをしたいという場合は、次のようなインタフェースを定義できます。 ```java package com.example; import java.util.List; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.service.annotation.PostExchange; @HttpExchange(url = "/todos") public interface BlockingTodoClient { @GetExchange List getTodos(); @GetExchange(url = "/{id}") Todo getTodo(@PathVariable("id") Integer id); @PostExchange Todo postTodo(@RequestBody Todo todo); } ``` このクライアントを使うには次のBean定義が必要です。 ```java @Bean public BlockingTodoClient blockingTodoClient(HttpServiceProxyFactory proxyFactory) { return proxyFactory.createClient(BlockingTodoClient.class); } ``` `BlockingTodoClient`をSpring MVCのControllerからそのまま呼び出したサンプルが次のコードです。 ```java package com.example; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(path = "/blocking/todos") public class BlockingTodoController { private final BlockingTodoClient todoClient; public BlockingTodoController(BlockingTodoClient todoClient) { this.todoClient = todoClient; } @GetMapping(path = "") public List getTodos() { return this.todoClient.getTodos(); } @GetMapping(path = "/{id}") public Todo getTodo(@PathVariable("id") Integer id) { return this.todoClient.getTodo(id); } @PostMapping(path = "") public Todo postTodo(@RequestBody Todo todo) { return this.todoClient.postTodo(todo); } } ``` 実行してリクエストを送ると、次のようなDEBUGログを確認できます。 ``` 2022-06-08T21:14:24.414+09:00 DEBUG 17045 --- [nio-8080-exec-7] o.s.web.servlet.DispatcherServlet : GET "/blocking/todos/1", parameters={} 2022-06-08T21:14:24.415+09:00 DEBUG 17045 --- [nio-8080-exec-7] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.BlockingTodoController#getTodo(Integer) 2022-06-08T21:14:24.415+09:00 DEBUG 17045 --- [nio-8080-exec-7] o.s.w.r.f.client.ExchangeFunctions : [4e3c530d] HTTP GET https://jsonplaceholder.typicode.com/todos/1 2022-06-08T21:14:24.522+09:00 DEBUG 17045 --- [ctor-http-nio-5] o.s.w.r.f.client.ExchangeFunctions : [4e3c530d] [57ab5a04-1, L:/192.168.11.97:53554 - R:jsonplaceholder.typicode.com/172.67.131.170:443] Response 200 OK 2022-06-08T21:14:24.524+09:00 DEBUG 17045 --- [ctor-http-nio-5] o.s.http.codec.json.Jackson2JsonDecoder : [4e3c530d] [57ab5a04-1, L:/192.168.11.97:53554 - R:jsonplaceholder.typicode.com/172.67.131.170:443] Decoded [Todo[userId=1, id=1, title=delectus aut autem, completed=false]] 2022-06-08T21:14:24.524+09:00 DEBUG 17045 --- [nio-8080-exec-7] m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json] 2022-06-08T21:14:24.524+09:00 DEBUG 17045 --- [nio-8080-exec-7] m.m.a.RequestResponseBodyMethodProcessor : Writing [Todo[userId=1, id=1, title=delectus aut autem, completed=false]] 2022-06-08T21:14:24.525+09:00 DEBUG 17045 --- [nio-8080-exec-7] o.s.web.servlet.DispatcherServlet : Completed 200 OK ``` 先の例とは異なり、`[nio-8080-exec-7]`と`[ctor-http-nio-5]`の2つしか現れません。
`BlockingTodoClient`がHTTPリクエストを送るのは先の例と同じくサーブレットのリクエストを処理するスレッド(`[nio-8080-exec-7]`)とは別のスレッド(`[ctor-http-nio-5]`)上ですが、 今回の例ではServletの非同期処理は行われず、`BlockingTodoClient`の結果を`[nio-8080-exec-7]`スレッドで待ち(ブロックし)、そのスレッドでHTTPレスポンスを書き込みます。 ブロックしている間はTomcatは`[nio-8080-exec-7]`スレッドに別リクエストを割り当てることはできないため、並行処理度の観点から言うと`Flux`や`Mono`を使う方が良いです。 しかし、`Flux`・`Mono`を使用したReactiveなAPIに慣れない場合は、まずは直感的にコーディングできるこのブロッキングなAPIから始めてもいいかもしれません。 非同期処理はしたいけれども、`Flux`・`Mono`は使いたくないという場合は、代わりに`CompletabelFuture`を使用して次のようなインタフェースを定義できます。 ```java package com.example; import java.util.List; import java.util.concurrent.CompletableFuture; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.service.annotation.PostExchange; @HttpExchange(url = "/todos") public interface AsyncTodoClient { @GetExchange CompletableFuture> getTodos(); @GetExchange(url = "/{id}") CompletableFuture getTodo(@PathVariable("id") Integer id); @PostExchange CompletableFuture postTodo(@RequestBody Todo todo); } ``` このクライアントを使うには次のBean定義が必要です。 ```java @Bean public AsyncTodoClient asyncTodoClient(HttpServiceProxyFactory proxyFactory) { return proxyFactory.createClient(AsyncTodoClient.class); } ``` `AsyncTodoClient`をSpring MVCのControllerからそのまま呼び出したサンプルが次のコードです。 ```java package com.example; import java.util.List; import java.util.concurrent.CompletableFuture; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(path = "/async/todos") public class AsyncTodoController { private final AsyncTodoClient todoClient; public AsyncTodoController(AsyncTodoClient todoClient) { this.todoClient = todoClient; } @GetMapping(path = "") public CompletableFuture> getTodos() { return this.todoClient.getTodos(); } @GetMapping(path = "/{id}") public CompletableFuture getTodo(@PathVariable("id") Integer id) { return this.todoClient.getTodo(id); } @PostMapping(path = "") public CompletableFuture postTodo(@RequestBody Todo todo) { return this.todoClient.postTodo(todo); } } ``` 実行してリクエストを送ると、次のようなDEBUGログを確認できます。 ``` 2022-06-08T21:26:02.581+09:00 DEBUG 17045 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/async/todos/1", parameters={} 2022-06-08T21:26:02.581+09:00 DEBUG 17045 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.AsyncTodoController#getTodo(Integer) 2022-06-08T21:26:02.582+09:00 DEBUG 17045 --- [nio-8080-exec-1] o.s.w.r.f.client.ExchangeFunctions : [6d41b18f] HTTP GET https://jsonplaceholder.typicode.com/todos/1 2022-06-08T21:26:02.582+09:00 DEBUG 17045 --- [nio-8080-exec-1] o.s.w.c.request.async.WebAsyncManager : Started async request 2022-06-08T21:26:02.583+09:00 DEBUG 17045 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting but response remains open for further handling 2022-06-08T21:26:02.693+09:00 DEBUG 17045 --- [ctor-http-nio-7] o.s.w.r.f.client.ExchangeFunctions : [6d41b18f] [a5fc1dba-1, L:/192.168.11.97:53587 - R:jsonplaceholder.typicode.com/104.21.4.48:443] Response 200 OK 2022-06-08T21:26:02.694+09:00 DEBUG 17045 --- [ctor-http-nio-7] o.s.http.codec.json.Jackson2JsonDecoder : [6d41b18f] [a5fc1dba-1, L:/192.168.11.97:53587 - R:jsonplaceholder.typicode.com/104.21.4.48:443] Decoded [Todo[userId=1, id=1, title=delectus aut autem, completed=false]] 2022-06-08T21:26:02.694+09:00 DEBUG 17045 --- [ctor-http-nio-7] o.s.w.c.request.async.WebAsyncManager : Async result set, dispatch to /async/todos/1 2022-06-08T21:26:02.695+09:00 DEBUG 17045 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : "ASYNC" dispatch for GET "/async/todos/1", parameters={} 2022-06-08T21:26:02.695+09:00 DEBUG 17045 --- [nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerAdapter : Resume with async result [Todo[userId=1, id=1, title=delectus aut autem, completed=false]] 2022-06-08T21:26:02.695+09:00 DEBUG 17045 --- [nio-8080-exec-2] m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json] 2022-06-08T21:26:02.696+09:00 DEBUG 17045 --- [nio-8080-exec-2] m.m.a.RequestResponseBodyMethodProcessor : Writing [Todo[userId=1, id=1, title=delectus aut autem, completed=false]] 2022-06-08T21:26:02.696+09:00 DEBUG 17045 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Exiting from "ASYNC" dispatch, status 200 ``` 最初の例と同じく、Servletの非同期処理が行われ、3つのスレッドが使用されていることがわかります。 ReactiveなAPIを利用したいけれども、`Flux`・`Mono`は使いたくないという場合は、代わりにJDKの`Flow.Publisher`を使用して次のようなインタフェースを定義できます。 ```java package com.example; import java.util.concurrent.Flow.Publisher; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.service.annotation.PostExchange; @HttpExchange(url = "/todos") public interface PublisherTodoClient { @GetExchange Publisher getTodos(); @GetExchange(url = "/{id}") Publisher getTodo(@PathVariable("id") Integer id); @PostExchange Publisher postTodo(@RequestBody Todo todo); } ``` このクライアントを使うには次のBean定義が必要です。 ```java @Bean public PublisherTodoClient publisherTodoClient(HttpServiceProxyFactory proxyFactory) { return proxyFactory.createClient(PublisherTodoClient.class); } ``` `PublisherTodoClient`をSpring MVCのControllerからそのまま呼び出したサンプルが次のコードです。 ```java package com.example; import java.util.concurrent.Flow.Publisher; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(path = "/publisher/todos") public class PublisherTodoController { private final PublisherTodoClient todoClient; public PublisherTodoController(PublisherTodoClient todoClient) { this.todoClient = todoClient; } @GetMapping(path = "") public Publisher getTodos() { return this.todoClient.getTodos(); } @GetMapping(path = "/{id}") public Publisher getTodo(@PathVariable("id") Integer id) { return this.todoClient.getTodo(id); } @PostMapping(path = "") public Publisher postTodo(@RequestBody Todo todo) { return this.todoClient.postTodo(todo); } } ``` 実行してリクエストを送ると、次のようなDEBUGログを確認できます。 ``` 2022-06-08T21:30:26.322+09:00 DEBUG 17045 --- [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : GET "/publisher/todos/1", parameters={} 2022-06-08T21:30:26.323+09:00 DEBUG 17045 --- [nio-8080-exec-4] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.PublisherTodoController#getTodo(Integer) 2022-06-08T21:30:26.325+09:00 DEBUG 17045 --- [nio-8080-exec-4] o.s.w.r.f.client.ExchangeFunctions : [5a75169f] HTTP GET https://jsonplaceholder.typicode.com/todos/1 2022-06-08T21:30:26.326+09:00 DEBUG 17045 --- [nio-8080-exec-4] o.s.w.c.request.async.WebAsyncManager : Started async request 2022-06-08T21:30:26.326+09:00 DEBUG 17045 --- [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : Exiting but response remains open for further handling 2022-06-08T21:30:26.348+09:00 DEBUG 17045 --- [ctor-http-nio-7] o.s.w.r.f.client.ExchangeFunctions : [5a75169f] [a5fc1dba-2, L:/192.168.11.97:53587 - R:jsonplaceholder.typicode.com/104.21.4.48:443] Response 200 OK 2022-06-08T21:30:26.358+09:00 DEBUG 17045 --- [ctor-http-nio-7] o.s.http.codec.json.Jackson2JsonDecoder : [5a75169f] [a5fc1dba-2, L:/192.168.11.97:53587 - R:jsonplaceholder.typicode.com/104.21.4.48:443] Decoded [Todo[userId=1, id=1, title=delectus aut autem, completed=false]] 2022-06-08T21:30:26.359+09:00 DEBUG 17045 --- [ctor-http-nio-7] o.s.w.c.request.async.WebAsyncManager : Async result set, dispatch to /publisher/todos/1 2022-06-08T21:30:26.359+09:00 DEBUG 17045 --- [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet : "ASYNC" dispatch for GET "/publisher/todos/1", parameters={} 2022-06-08T21:30:26.359+09:00 DEBUG 17045 --- [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerAdapter : Resume with async result [[Todo[userId=1, id=1, title=delectus aut autem, completed=false]]] 2022-06-08T21:30:26.363+09:00 DEBUG 17045 --- [nio-8080-exec-5] m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json] 2022-06-08T21:30:26.363+09:00 DEBUG 17045 --- [nio-8080-exec-5] m.m.a.RequestResponseBodyMethodProcessor : Writing [[Todo[userId=1, id=1, title=delectus aut autem, completed=false]]] 2022-06-08T21:30:26.364+09:00 DEBUG 17045 --- [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet : Exiting from "ASYNC" dispatch, status 200 ``` これも最初の例と同じです。 ただし、`Flow.Publisher`だと`Flux`と`Mono`のように"0個以上の要素を持つストリーム"と"0または1個の要素を持つストリーム"を区別できないため、こちらのAPIを使用するメリットはないでしょう。 [RxJava 3](https://github.com/ReactiveX/RxJava)、[Mutiny](https://smallrye.io/smallrye-mutiny/)、[Kotlin Coroutines](https://github.com/Kotlin/kotlinx.coroutines)の型も使用できるので、 慣れている型があればそちらを使ってもいいかもしれません。 --- Spring Framework 6から導入される宣言的HTTP Clientを試しました。 APIの習得度に応じて、いろいろな定義方法を選択できることを確認しました。 Spring Framework 6になったら積極的に使っていきたい機能です。