①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖
本文是直接从官方文档中翻译的。
WebClient
最终会成为Spring
新一带的RestApi
连接框架,所以单独列出了一个章节。
参考网址
有几个必要的高级功能
google
,查询renewTokenFilter
的完整代码,可以访问源码,看看WebClient
到底有啥功能。
可以使用 create() or create(String), or builder()
来准备一个实例。
如果要得到response
可以通过下面三个方法:
内部静态类
类 | 说明 |
---|---|
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.Builder | builder() | 获取 WebClient 构建器。 |
WebClient | create() | 默认使用 Reactor Netty 创建一个新的 WebClient。 |
WebClient | create(String baseUrl) | 接受默认基本 URL 的 create() 变体。 |
WebClient.RequestHeadersUriSpec | delete() | 开始构建 HTTP DELETE 请求。 |
WebClient.RequestHeadersUriSpec | get() | 开始构建 HTTP GET 请求。 |
WebClient.RequestHeadersUriSpec | head() | 开始构建 HTTP HEAD 请求。 |
WebClient.RequestBodyUriSpec | method(HttpMethod method) | 开始为给定的 HttpMethod 构建请求。 |
WebClient.Builder | mutate() | 返回一个构建器以创建一个新的 WebClient,其设置是从当前 WebClient 复制的。 |
WebClient.RequestHeadersUriSpec | options() | 开始构建 HTTP OPTIONS 请求。 |
WebClient.RequestHeadersUriSpec | patch() | 开始构建 HTTP PATCH 请求。 |
WebClient.RequestHeadersUriSpec | post() | 开始构建 HTTP POST 请求。 |
WebClient.RequestHeadersUriSpec | put() | 开始构建 HTTP PUT 请求。 |
创建 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
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();
要自定义 Reactor Netty
设置,请提供预配置的 HttpClient
:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();
默认情况下,HttpClient
参与 reactor.netty.http.HttpResources
中持有的全局 Reactor Netty
资源,包括事件循环线程和连接池。这是推荐的模式,固定的共享资源会提供给事件循环并发。在这种模式下,全局资源保持活动状态,直到进程退出。
如果服务器与进程同步,则通常不需要显式关闭。但是,如果服务器可以在进程中启动或停止(例如,部署为 WAR
的 Spring MVC
应用程序),您可以使用 globalResources=true
(默认)声明一个ReactorResourceFactory
类型的 Spring
管理的 bean
,以确保在 Spring ApplicationContext
关闭时关闭 Reactor Netty
全局资源,如以下示例所示:
@Beanpublic ReactorResourceFactory reactorResourceFactory() {return new ReactorResourceFactory();}
您也可以选择不参与全局 Reactor Netty
资源。然而,在这种模式下,您有责任确保所有 Reactor Netty
客户端和服务器实例使用共享资源,如以下示例所示:
@Beanpublic ReactorResourceFactory resourceFactory() {ReactorResourceFactory factory = new ReactorResourceFactory();factory.setUseGlobalResources(false); //①return factory;}@Beanpublic 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
。
要配置连接超时:
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);
背景介绍
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
客户端(和服务器)的多个实例之间共享资源,并通过声明类型为 JettyResourceFactory
的 Spring
管理的 bean
来确保在关闭 Spring ApplicationContext
时关闭资源,如以下示例所示:
@Beanpublic JettyResourceFactory resourceFactory() {return new JettyResourceFactory();}@Beanpublic 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
。
以下示例显示如何自定义 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();
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);
默认情况下,4xx
或 5xx
响应会导致 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);
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 errorreturn response.createException().flatMap(Mono::error);}});
使用上述方法时,在返回的 Mono 或 Flux 完成后,会检查响应正文,如果未使用,则将其释放以防止内存和连接泄漏。因此,response 无法在下游进一步解码。如果需要,由提供的函数声明如何解码响应。
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);
要发送表单数据,您可以提供 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);
要发送多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 requestMultiValueMap<String, HttpEntity<?>> parts = builder.build();
在大多数情况下,您不必为每个部分指定 Content-Type
。 Content-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);
您可以通过 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
的完整代码
您可以向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
数据填充请求属性。
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", ...));
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。
要测试使用 WebClient
的代码,您可以使用模拟 Web 服务器,例如 OkHttp MockWebServer
。要查看其使用示例,请查看 Spring Framework
测试套件中的 WebClientIntegrationTests
或 OkHttp
存储库中的静态服务器示例。
浏览到一个老的Spring 文档,描述了如何使用 WebClient 进行 OAuth2 登陆,看了以后做了翻译。这段文档的代码,对应了官方的例子。
Spring Framework 内置了对设置 Bearer 令牌的支持。
webClient.get().headers(h -> h.setBearerAuth(token))...
Spring Security 建立在此支持之上以提供额外的好处:
第一步是确保正确设置 WebClient
。下面是在 servlet
环境中设置 WebClient
的示例:
@BeanWebClient 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();}
如果我们在设置中将 defaultOAuth2AuthorizedClient
设置为 true
,并且用户使用oauth2Login
(即 OIDC
)进行身份验证,则当前身份验证用于自动提供访问令牌。或者,如果我们将 defaultClientRegistrationId
设置为有效的 ClientRegistration id
,则该注册用于提供访问令牌。这很方便,但在并非所有端点都应该获取访问令牌的环境中,这是危险的(您可能向端点提供了错误的访问令牌)。
Mono<String> body = this.webClient.get().uri(this.uri).retrieve().bodyToMono(String.class);
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
,WebClient
将尝试查找 OAuth2AuthorizedClient
。如果没有找到,将自动获取一个。
Mono<String> body = this.webClient.get().uri(this.uri).attributes(clientRegistrationId("client-id")).retrieve().bodyToMono(String.class);
@Configurationpublic class WebClientConfig {@BeanWebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);return WebClient.builder().apply(oauth2Client.oauth2Configuration()).build();}@BeanOAuth2AuthorizedClientManager 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 中使用
@Controllerpublic 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";}.........................................