WebClient

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖

本文是直接从官方文档中翻译的。

WebClient最终会成为Spring新一带的RestApi连接框架,所以单独列出了一个章节。

0. java docs

可以访问源码,看看WebClient到底有啥功能。

可以使用 create() or create(String), or builder()来准备一个实例。

如果要得到response可以通过下面三个方法:

  • retrieve()
  • exchangeToMono()
  • exchangeToFlux()

内部静态类

说明
WebClient.Builder用于创建 WebClient 的构建器。
WebClient.RequestBodySpec指定 request headers 和 body
WebClient.RequestBodyUriSpec指定 request headers, body 和 URI
WebClient.RequestHeadersSpec指定 request headers
WebClient.RequestHeadersUriSpec指定 request headers 和 URI
WebClient.ResponseSpec指定 response operations
WebClient.UriSpec指定 request URI

主要方法

返回值方法说明
WebClient.Builderbuilder()获取 WebClient 构建器。
WebClientcreate()默认使用 Reactor Netty 创建一个新的 WebClient。
WebClientcreate(String baseUrl)接受默认基本 URL 的 create() 变体。
WebClient.RequestHeadersUriSpecdelete()开始构建 HTTP DELETE 请求。
WebClient.RequestHeadersUriSpecget()开始构建 HTTP GET 请求。
WebClient.RequestHeadersUriSpechead()开始构建 HTTP HEAD 请求。
WebClient.RequestBodyUriSpecmethod(HttpMethod method)开始为给定的 HttpMethod 构建请求。
WebClient.Buildermutate()返回一个构建器以创建一个新的 WebClient,其设置是从当前 WebClient 复制的。
WebClient.RequestHeadersUriSpecoptions()开始构建 HTTP OPTIONS 请求。
WebClient.RequestHeadersUriSpecpatch()开始构建 HTTP PATCH 请求。
WebClient.RequestHeadersUriSpecpost()开始构建 HTTP POST 请求。
WebClient.RequestHeadersUriSpecput()开始构建 HTTP PUT 请求。

1. Configuration

创建 WebClient 的最简单方法是通过其中一种静态工厂方法:

  • WebClient.create()
  • WebClient.create(String baseUrl)

也可以将 WebClient.builder() 配合 builder 选择来创建 WebClient:

  • uriBuilderFactory: 自定 UriBuilderFactory 创建基础 URL.
  • defaultUriVariables: 传入一个 Map,为 URI 模板传入缺省的数据。
  • defaultHeader: Headers for every request.
  • defaultCookie: Cookies for every request.
  • defaultRequest: Consumer to customize every request.
  • filter: Client filter for every request.
  • exchangeStrategies: HTTP message reader/writer customizations.
  • clientConnector: HTTP client library settings.

例子

WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();

一旦构建,WebClient 是不可变的。但是,您可以克隆它并构建修改后的副本,如下所示:

WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD

1.1 MaxInMemorySize

Codecs可以限制内存中的缓冲数据,以避免应用程序内存问题。默认情况下,这些设置为 256KB。如果不够,您将收到以下错误:

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

要更改默认Codecs的限制,请使用以下命令:

WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();

1.2 Reactor Netty

要自定义 Reactor Netty 设置,请提供预配置的 HttpClient

HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();

Resources

默认情况下,HttpClient 参与 reactor.netty.http.HttpResources 中持有的全局 Reactor Netty资源,包括事件循环线程和连接池。这是推荐的模式,固定的共享资源会提供给事件循环并发。在这种模式下,全局资源保持活动状态,直到进程退出。

如果服务器与进程同步,则通常不需要显式关闭。但是,如果服务器可以在进程中启动或停止(例如,部署为 WARSpring MVC 应用程序),您可以使用 globalResources=true (默认)声明一个ReactorResourceFactory类型的 Spring 管理的 bean,以确保在 Spring ApplicationContext 关闭时关闭 Reactor Netty 全局资源,如以下示例所示:

@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}

您也可以选择不参与全局 Reactor Netty 资源。然而,在这种模式下,您有责任确保所有 Reactor Netty 客户端和服务器实例使用共享资源,如以下示例所示:

@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false); //①
return factory;
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// Further customizations...
};
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper); //②
return WebClient.builder().clientConnector(connector).build(); //③
}

① 创建独立于全局资源的资源。

② 将 ReactorClientHttpConnector 构造函数与resourceFactory一起使用。

③ 将连接器插入 WebClient.Builder

Timeouts

要配置连接超时:

import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();

要配置读取或写入超时:

import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)));
// Create WebClient...

为所有请求配置响应超时:

HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...

为特定请求配置响应超时:

WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);

1.3 Jetty

背景介绍

Servlet:在代码层面,Servlet其实就是一个接口
Tomcat:是一个由Apache软件基金会(ASF)开源的Java Servlet容器。
Tomcat实现了一些Java EE规范,包括Servlet,JSP,Java EL和WebSocket;同时提供了一个纯Java的HTTP Web服务运行环境。
Jetty:是一个Java HTTP(Web)服务器和Java Servlet容器。
尽管Jetty通常被人们记录成Web服务器,但是Jetty现在主要被用于大型软件架构中机器之间的通讯。Jetty是被作为Eclipse基金会的一部分来开发的开源项目。
Netty:是一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持,作为一个异步 NIO 框架,Netty 的所有 IO 操作都是异步非阻塞的,通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。
Tomcat和Jetty一般作为Servlet容器和Web服务器使用,性能也不错(大概几千 QPS的样子)。
Netty主要用于对性能要求比较高的通信场景,如果处理的好的话性能会很高(几万、几十万 QPS)。

以下示例显示了如何自定义 Jetty HttpClient 设置:

HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();

默认情况下,HttpClient 创建自己的资源(Executor、ByteBufferPool、Scheduler),这些资源在进程退出或调用 stop() 之前保持活动状态。

您可以在 Jetty 客户端(和服务器)的多个实例之间共享资源,并通过声明类型为 JettyResourceFactorySpring 管理的 bean 来确保在关闭 Spring ApplicationContext 时关闭资源,如以下示例所示:

@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Further customizations...
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory()); //①
return WebClient.builder().clientConnector(connector).build(); //②
}

① 将 JettyClientHttpConnector 构造函数与resourceFactory一起使用。

② 将connector插入 WebClient.Builder

1.4. HttpComponents

以下示例显示如何自定义 Apache HttpComponents HttpClient 设置:

HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();

2. retrieve()

retrieve() 方法可用于声明如何提取 response 内容。例如:

WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);

或者只得到 body:

WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);

要获取解码对象流:

Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);

默认情况下,4xx5xx 响应会导致 WebClientResponseException,包括特定 HTTP status codes的子类。要自定义错误响应的处理,请使用 onStatus 处理程序,如下所示:

Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);

3. Exchange

exchangeToMono() exchangeToFlux() 方法(或 Kotlin 中的 awaitExchange { }exchangeToFlow { })对于需要更多控制的更高级的情况很有用,例如根据响应状态对响应进行不同的解码:

Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else {
// Turn to error
return response.createException().flatMap(Mono::error);
}
});

使用上述方法时,在返回的 Mono 或 Flux 完成后,会检查响应正文,如果未使用,则将其释放以防止内存和连接泄漏。因此,response 无法在下游进一步解码。如果需要,由提供的函数声明如何解码响应。

4. Request Body

request body可以从 ReactiveAdapterRegistry 处理的任何异步类型编码,如 Mono Kotlin Coroutines Deferred,如下例所示:

Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);

您还可以对对象流进行编码,如以下示例所示:

Flux<Person> personFlux = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);

或者,如果您有实际值,则可以使用 bodyValue 快捷方法,如以下示例所示:

Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);

4.1. Form Data

要发送表单数据,您可以提供 MultiValueMap<String, String> 作为正文。请注意,FormHttpMessageWriter 会自动将内容设置为 application/x-www-form-urlencoded。下面的例子展示了如何使用 MultiValueMap<String, String>

MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);

您还可以使用 BodyInserters 提供表单数据,如以下示例所示:

import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);

4.2. Multipart Data

要发送多multipart data,您需要提供一个 MultiValueMap<String, ?>MultipartBodyBuilder 提供了一个方便的 API 来准备多部分请求。以下示例显示如何创建 MultiValueMap<String, ?>

MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request
MultiValueMap<String, HttpEntity<?>> parts = builder.build();

在大多数情况下,您不必为每个部分指定 Content-TypeContent-Type是根据选择序列化它的 HttpMessageWriter 自动确定的,或者,基于文件扩展名。如有必要,您可以通过重载的构建器部件方法之一显式提供用于每个部件的 MediaType

准备好 MultiValueMap 后,将其传递给 WebClient 的最简单方法是通过 body 方法,如以下示例所示:

MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);

如果 MultiValueMap 至少包含一个非字符串值,它也可以表示常规表单数据(即 application/x-www-form-urlencoded),您无需将 Content-Type 设置为 multipart/form-data。使用 MultipartBodyBuilder 时总是如此,它确保了一个 HttpEntity 包装器。

import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.bodyToMono(Void.class);

5. Filters

您可以通过 WebClient.Builder 注册客户端过滤器(ExchangeFilterFunction),以拦截和修改请求,如下例所示:

WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();

这可用于横切关注点,例如身份验证。以下示例通过静态工厂方法使用过滤器进行基本身份验证:

import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();

过滤器可以通过改变现有的 WebClient 实例来添加或删除,从而产生一个不影响原始 WebClient 实例的新 WebClient 实例。例如:

import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();

WebClient 是一个围绕过滤器链的薄封装,后跟一个 ExchangeFunction。它提供了一个工作流程来发出requests,与更高级别的对象进行编码,它有助于确保始终使用response内容。当过滤器以某种方式处理response时,必须格外小心以始终使用其内容或以其他方式将其传播到下游的 WebClient 以确保相同。下面是一个过滤器,它处理 UNAUTHORIZED 状态代码,但确保任何response内容(无论是否预期)都被释放:

public ExchangeFilterFunction renewTokenFilter() {
return (request, next) -> next.exchange(request).flatMap(response -> {
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
return response.releaseBody()
.then(renewToken())
.flatMap(token -> {
ClientRequest newRequest = ClientRequest.from(request).build();
return next.exchange(newRequest);
});
} else {
return Mono.just(response);
}
});
}

google,查询renewTokenFilter的完整代码

6. Attributes

您可以向request添加属性。如果您想通过filter chain传递信息并影响给定request的过滤器行为,这很方便。例如:

WebClient client = WebClient.builder()
.filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute");
// ...
})
.build();
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}

请注意,您可以在 WebClient.Builder 级别全局配置 defaultRequest 回调,它允许您将属性插入所有请求,例如,可以在 Spring MVC 应用程序中使用它来根据 ThreadLocal 数据填充请求属性。

7. Context

Attributes提供了一种将信息传递给filter chain的便捷方式,但它们只影响当前request。如果您想传递传播到嵌套的其他request的信息,例如通过 flatMap,或在之后执行,例如通过 concatMap,那么您将需要使用 Reactor Context

Reactor Context需要在reactive chain的末端填充,以便应用于所有操作。例如:

WebClient client = WebClient.builder()
.filter((request, next) ->
Mono.deferContextual(contextView -> {
String value = contextView.get("foo");
// ...
}))
.build();
client.get().uri("https://example.org/")
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
// perform nested request (context propagates automatically)...
})
.contextWrite(context -> context.put("foo", ...));

8. Synchronous Use

WebClient 可以通过blocking在最后阻塞结果以使用同步方式:

Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();

但是,如果需要进行多次调用,避免单独阻塞每个响应会更有效,而是等待组合结果:

Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class);
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
Map<String, String> map = new LinkedHashMap<>();
map.put("person", person);
map.put("hobbies", hobbies);
return map;
})
.block();

以上只是一个例子。还有许多其他模式和运算符可以组合一个 reactive pipeline,该管道可以进行许多远程调用,可能是一些嵌套的、相互依赖的,直到最后都不会阻塞。

备注:

  • 使用 Flux 或 Mono,您永远不必阻塞 Spring MVC 或 Spring WebFlux 的 controller。只需从 controller 方法返回 reactive type 的结果。同样的原则也适用于 Kotlin Coroutines 和 Spring WebFlux,只是在你的控制器方法中使用挂起函数或返回 Flow。

9. Testing

要测试使用 WebClient 的代码,您可以使用模拟 Web 服务器,例如 OkHttp MockWebServer。要查看其使用示例,请查看 Spring Framework 测试套件中的 WebClientIntegrationTestsOkHttp 存储库中的静态服务器示例。

10. Example

10.1 OAuth2 Setup

浏览到一个老的Spring 文档,描述了如何使用 WebClient 进行 OAuth2 登陆,看了以后做了翻译。这段文档的代码,对应了官方的例子。

Spring Framework 内置了对设置 Bearer 令牌的支持。

webClient.get()
.headers(h -> h.setBearerAuth(token))
...

Spring Security 建立在此支持之上以提供额外的好处:

  • Spring Security 将自动刷新过期令牌(如果存在刷新令牌)
  • 如果请求访问令牌但不存在,Spring Security 将自动请求访问令牌。
    • 对于 authorization_code,这涉及执行重定向,然后重播原始请求.
    • 对于 client_credentials,只需请求并保存令牌
  • 支持透明地包含当前 OAuth 令牌或明确选择应使用哪个令牌的能力。

① Setup

第一步是确保正确设置 WebClient。下面是在 servlet 环境中设置 WebClient 的示例:

@Bean
WebClient webClient(ClientRegistrationRepository clientRegistrations,
OAuth2AuthorizedClientRepository authorizedClients) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
// (optional) explicitly opt into using the oauth2Login to provide an access token implicitly
// oauth.setDefaultOAuth2AuthorizedClient(true);
// (optional) set a default ClientRegistration.registrationId
// oauth.setDefaultClientRegistrationId("client-registration-id");
return WebClient.builder()
.apply(oauth2.oauth2Configuration())
.build();
}

② Implicit OAuth2AuthorizedClient

如果我们在设置中将 defaultOAuth2AuthorizedClient 设置为 true,并且用户使用oauth2Login(即 OIDC)进行身份验证,则当前身份验证用于自动提供访问令牌。或者,如果我们将 defaultClientRegistrationId 设置为有效的 ClientRegistration id,则该注册用于提供访问令牌。这很方便,但在并非所有端点都应该获取访问令牌的环境中,这是危险的(您可能向端点提供了错误的访问令牌)。

Mono<String> body = this.webClient
.get()
.uri(this.uri)
.retrieve()
.bodyToMono(String.class);

③ Explicit OAuth2AuthorizedClient

OAuth2AuthorizedClient 可以通过在请求属性上设置来显式提供。在下面的示例中,我们使用 Spring WebFlux Spring MVC 参数解析器支持解析 OAuth2AuthorizedClient。但是,如何解析 OAuth2AuthorizedClient 并不重要。

@GetMapping("/explicit")
Mono<String> explicit(@RegisteredOAuth2AuthorizedClient("client-id") OAuth2AuthorizedClient authorizedClient) {
return this.webClient
.get()
.uri(this.uri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class);
}

④ clientRegistrationId

或者,可以在请求属性上指定 clientRegistrationIdWebClient 将尝试查找 OAuth2AuthorizedClient。如果没有找到,将自动获取一个。

Mono<String> body = this.webClient
.get()
.uri(this.uri)
.attributes(clientRegistrationId("client-id"))
.retrieve()
.bodyToMono(String.class);

⑤ 相关代码

github 上的官方代码

@Configuration
public class WebClientConfig {
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}

在 controller 中使用

@Controller
public class AuthorizationController {
private final WebClient webClient;
private final String messagesBaseUri;
public AuthorizationController(WebClient webClient,
@Value("${messages.base-uri}") String messagesBaseUri) {
this.webClient = webClient;
this.messagesBaseUri = messagesBaseUri;
}
@GetMapping(value = "/authorize", params = "grant_type=authorization_code")
public String authorizationCodeGrant(Model model,
@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code")
OAuth2AuthorizedClient authorizedClient) {
String[] messages = this.webClient
.get()
.uri(this.messagesBaseUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String[].class)
.block();
model.addAttribute("messages", messages);
return "index";
}
.........................................