---
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になったら積極的に使っていきたい機能です。