---
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で発表されたセッションです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/5LNOnVJKW_4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

> ℹ️ 今後、宣言的な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`が必要です。<br>
たまに誤解されますが、`WebClient`や`Mono`、`Flux`は**Spring MVCでも利用可能です**。

以下ではSprign MVCで宣言的クライアントを使用します。

```xml
<!-- required only for Spring MVC -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- required for both Spring MVC and Spring WebFlux -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
```

Todo APIに対する`TodoClient`は次のようなインターフェースで定義できます。<br>
直感的にやりたいことを理解できるのではないでしょうか。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<Todo> getTodos();

	@GetExchange(url = "/{id}")
	Mono<Todo> getTodo(@PathVariable("id") Integer id);

	@PostExchange
	Mono<Todo> 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<Todo> getTodos() {
> 		return this.webClient.post().uri("/todos").retrieve().bodyToFlux(Todo.class);
> 	}
> 
> 	@Override
> 	public Mono<Todo> getTodo(Integer id) {
> 		return this.webClient.post().uri("/todos/{id}", id).retrieve().bodyToMono(Todo.class);
> 	}
> 
> 	@Override
> 	public Mono<Todo> postTodo(Todo todo) {
> 		return this.webClient.post().uri("/todos").bodyValue(todo).retrieve().bodyToMono(Todo.class);
> 	}
> }
> ```

`TodoClient`をSpring MVCのControllerからそのまま呼び出したサンプルが次のコードです。<br>
たまに誤解されますが、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<Todo> getTodos() {
		return this.todoClient.getTodos();
	}

	@GetMapping(path = "/{id}")
	public Mono<Todo> getTodo(@PathVariable("id") Integer id) {
		return this.todoClient.getTodo(id);
	}

	@PostMapping(path = "")
	public Mono<Todo> 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<Todo> 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<Todo> 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つしか現れません。<br>
`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<List<Todo>> getTodos();

	@GetExchange(url = "/{id}")
	CompletableFuture<Todo> getTodo(@PathVariable("id") Integer id);

	@PostExchange
	CompletableFuture<Todo> 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<List<Todo>> getTodos() {
		return this.todoClient.getTodos();
	}

	@GetMapping(path = "/{id}")
	public CompletableFuture<Todo> getTodo(@PathVariable("id") Integer id) {
		return this.todoClient.getTodo(id);
	}

	@PostMapping(path = "")
	public CompletableFuture<Todo> 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<Todo> getTodos();

	@GetExchange(url = "/{id}")
	Publisher<Todo> getTodo(@PathVariable("id") Integer id);

	@PostExchange
	Publisher<Todo> 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<Todo> getTodos() {
		return this.todoClient.getTodos();
	}

	@GetMapping(path = "/{id}")
	public Publisher<Todo> getTodo(@PathVariable("id") Integer id) {
		return this.todoClient.getTodo(id);
	}

	@PostMapping(path = "")
	public Publisher<Todo> 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になったら積極的に使っていきたい機能です。
