IK.AM

@making's tech note


Spring Framework 6から導入される宣言的HTTP Clientを試す

🗃 {Programming/Java/org/springframework/web/service/invoker}
🏷 Reactor 🏷 Reactor Netty 🏷 Netty 🏷 Spring 6 🏷 Spring WebFlux 🏷 Spring MVC 🏷 Java 
🗓 Updated at 2022-06-09T01:24:55+09:00  🗓 Created at 2022-06-08T15:49:43+09:00 {✒️️ Edit  ⏰ History  🗑 Delete}

Spring Framework 6からRetfofitFeignのような宣言的なHTTP Clientが利用可能になります。

以下はSpring I/O 2022で発表されたセッションです。

ℹ️ 今後、宣言的なRSocket Clientもサポートされるようです。

早速試してみたいと思います。

  • Spring Boot 3.0.0-M3
  • Spring Framework 6.0.0-M4
  • Java 17

で試しました。

ソースコードはこちらです。

{JSON} PlaceholderTodo APIに対するクライアントを作成します。

HTTP Clientの下回りにはWebClientが使用されています。そのため、Dependencyにはspring-boot-starter-webfluxが必要です。
たまに誤解されますが、WebClientMonoFluxSpring 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でもコントローラーのメソッドの返り値にFluxMonoをそのまま利用できます。

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レスポンスを書き込むまでの処理のスレッド上でのログです。

コントローラーのメソッドの返り値にFluxMonoを使うとServletの非同期処理が行われ、リクエストとレスポンスで異なるスレッドが使われます。

FluxMonoに馴染みがなく、これまでの通りのブロッキングな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]スレッドに別リクエストを割り当てることはできないため、並行処理度の観点から言うとFluxMonoを使う方が良いです。 しかし、FluxMonoを使用したReactiveなAPIに慣れない場合は、まずは直感的にコーディングできるこのブロッキングなAPIから始めてもいいかもしれません。

非同期処理はしたいけれども、FluxMonoは使いたくないという場合は、代わりに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を利用したいけれども、FluxMonoは使いたくないという場合は、代わりに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だとFluxMonoのように"0個以上の要素を持つストリーム"と"0または1個の要素を持つストリーム"を区別できないため、こちらのAPIを使用するメリットはないでしょう。

RxJava 3MutinyKotlin Coroutinesの型も使用できるので、 慣れている型があればそちらを使ってもいいかもしれません。


Spring Framework 6から導入される宣言的HTTP Clientを試しました。 APIの習得度に応じて、いろいろな定義方法を選択できることを確認しました。

Spring Framework 6になったら積極的に使っていきたい機能です。