Spring Framework 6からRetfofitやFeignのような宣言的なHTTP Clientが利用可能になります。
以下はSpring I/O 2022で発表されたセッションです。
ℹ️ 今後、宣言的なRSocket Clientもサポートされるようです。
早速試してみたいと思います。
- Spring Boot 3.0.0-M3
- Spring Framework 6.0.0-M4
- Java 17
で試しました。
ソースコードはこちらです。
{JSON} PlaceholderのTodo APIに対するクライアントを作成します。
HTTP Clientの下回りにはWebClient
が使用されています。そのため、Dependencyにはspring-boot-starter-webflux
が必要です。
たまに誤解されますが、WebClient
やMono
、Flux
はSpring MVCでも利用可能です。
以下ではSprign MVCで宣言的クライアントを使用します。
<!-- 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
は次のようなインターフェースで定義できます。
直感的にやりたいことを理解できるのではないでしょうか。Controllerに使用する@GetMapping
アノテーションとは異なり@GetExchange
アノテーションを使用します。
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で定義できます。
package com.example;
public record Todo(Integer userId, Integer id, String title, boolean completed) {
}
このHTTP ClientのBean定義は次のようになります。クライアントのインタフェース毎にHttpServiceProxyFactory
からクライアントのインスタンスを生成してBean定義します。
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が生み出されます。
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からそのまま呼び出したサンプルが次のコードです。
たまに誤解されますが、Spring MVCでもコントローラーのメソッドの返り値にFlux
やMono
をそのまま利用できます。
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呼び出しをしたいという場合は、次のようなインタフェースを定義できます。
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定義が必要です。
@Bean
public BlockingTodoClient blockingTodoClient(HttpServiceProxyFactory proxyFactory) {
return proxyFactory.createClient(BlockingTodoClient.class);
}
BlockingTodoClient
をSpring MVCのControllerからそのまま呼び出したサンプルが次のコードです。
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つしか現れません。
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
を使用して次のようなインタフェースを定義できます。
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定義が必要です。
@Bean
public AsyncTodoClient asyncTodoClient(HttpServiceProxyFactory proxyFactory) {
return proxyFactory.createClient(AsyncTodoClient.class);
}
AsyncTodoClient
をSpring MVCのControllerからそのまま呼び出したサンプルが次のコードです。
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
を使用して次のようなインタフェースを定義できます。
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定義が必要です。
@Bean
public PublisherTodoClient publisherTodoClient(HttpServiceProxyFactory proxyFactory) {
return proxyFactory.createClient(PublisherTodoClient.class);
}
PublisherTodoClient
をSpring MVCのControllerからそのまま呼び出したサンプルが次のコードです。
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、Mutiny、Kotlin Coroutinesの型も使用できるので、 慣れている型があればそちらを使ってもいいかもしれません。
Spring Framework 6から導入される宣言的HTTP Clientを試しました。 APIの習得度に応じて、いろいろな定義方法を選択できることを確認しました。
Spring Framework 6になったら積極的に使っていきたい機能です。