WebClient是一种非阻塞、响应式客户端,用于执行HTTP请求。它在5.0中引入,并提供了RestTemplate的替代方案,支持同步、异步和流式场景。
WebClient支持以下功能:
非阻塞I/O。
反应流背压。
硬件资源较少的高并发性。
函数式风格,流畅的API,利用了Java 8 lambda。
同步和异步交互。
向上流动或从服务器向下流动。
一、配置
创建WebClient的最简单方法是通过静态工厂方法之一:
WebClient.create()
WebClient.create(字符串baseUrl)
您还可以将WebClient.builder()与其他选项一起使用:
uriBuilderFactory:要用作基URL的自定义uriBuilderFactory。
defaultUriVariables:展开URI模板时使用的默认值。
defaultHeader:每个请求的标头。
defaultCookie:每个请求的Cookie。
defaultRequest:消费者自定义每个请求。
filter:每个请求的客户端筛选器。
exchangeStrategies:HTTP消息读取器/写入器自定义。
clientConnector:HTTP客户端库设置。
observationRegistry:用于启用Observability支持的注册表。
observationConvention:一种可选的自定义约定,用于提取记录观测的元数据。
示例:
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、最大内存大小
编解码器对缓冲内存中的数据有限制,以避免应用程序内存问题。默认情况下,这些设置为256KB。如果这还不够,则会出现以下错误:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
若要更改默认编解码器的限制,请使用以下命令:
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
2、Reactor Netty
要自定义Reactor Netty设置,请提供预配置的HttpClient:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Resources
默认情况下,HttpClient参与Reactor.nety.http.HttpResources中保存的全局Reactor Netty资源,包括事件循环线程和连接池。这是推荐的模式,因为固定的共享资源是事件循环并发的首选。在此模式下,全局资源保持活动状态,直到进程退出。
如果服务器与进程同步,则通常不需要显式关闭。但是,如果服务器可以在进程内启动或停止(例如,部署为WAR的Spring 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); (1)
return factory;
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// Further customizations...
};
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper); (2)
return WebClient.builder().clientConnector(connector).build(); (3)
}
(1)创建独立于全球资源的资源。
(2)使用资源工厂的ReactorClientHttpConnector构造函数。
(3)将连接器插入WebClient.Builder。
超时
要配置连接超时:
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
要配置读取或写入超时:
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);
3、JDK HttpClient
以下示例显示了如何自定义JDK HttpClient:
HttpClient httpClient = HttpClient.newBuilder()
.followRedirects(Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
ClientHttpConnector connector =
new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory());
WebClient webClient = WebClient.builder().clientConnector(connector).build();
4、Jetty
以下示例显示了如何自定义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时关闭资源,如下例所示:
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Further customizations...
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory()); (1)
return WebClient.builder().clientConnector(connector).build(); (2)
}
(1)使用资源工厂的JettyClientHttpConnector构造函数。
(2)将连接器插入WebClient.Builder。
5、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();
二、retrieve()
retrieve()方法可用于声明如何提取响应。例如:
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状态代码的子类。要自定义错误响应的处理,请使用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);
三、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.createError();
}
});
当使用上述方法时,在返回的Mono或Flux完成后,将检查响应体,如果没有使用,则释放响应体以防止内存和连接泄漏。因此,不能在下游进一步解码响应。如果需要,由提供的函数声明如何解码响应。
四、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);
1、 表单数据
要发送表单数据,可以提供一个MultiValueMap<String,String>作为主体。请注意,内容自动设置为application/x-www-form-urlencoded,由FormHttpMessageWriter编码。以下示例显示了如何使用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);
2、Multipart Data
要发送多部分数据,您需要提供一个MultiValueMap<String,?>其值是表示部件内容的Object实例或表示部件内容和头的HttpEntity实例。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();
在大多数情况下,不必为每个零件指定内容类型。内容类型是根据选择序列化它的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-wwww-form-urlencoded),则无需将内容类型设置为multipart/form数据。使用MultipartBodyBuilder时总是这样,它确保了HttpEntity包装器。
作为MultipartBodyBuilder的替代方案,您还可以通过内置的BodyInserters提供多部分内容、内联样式,如下例所示:
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);
PartEvent
要按顺序流式传输多部分数据,可以通过PartEvent对象提供多部分内容。
可以通过FormPartEvent::create创建表单字段。
文件上传可以通过FilePartEvent::create创建。
您可以通过Flux::concat连接从方法返回的流,并为WebClient创建请求。
例如,此示例将POST包含表单字段和文件的多部分表单。
Resource resource = ...
Mono<String> result = webClient
.post()
.uri("https://example.com")
.body(Flux.concat(
FormPartEvent.create("field", "field value"),
FilePartEvent.create("file", resource)
), PartEvent.class)
.retrieve()
.bodyToMono(String.class);
在服务器端,通过@RequestBody或ServerRequest::bodyToFlux(PartEvent.class)接收的PartEvent对象可以通过WebClient中继到另一个服务。
五、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实例,该实例不会影响原始实例。例如:
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。它提供了一个工作流,用于发出请求,对更高级别的对象进行编码,并有助于确保始终使用响应内容。当过滤器以某种方式处理响应时,必须特别注意始终使用其内容,或者将其传播到下游的WebClient,以确保相同。下面是一个过滤器,它处理未经授权的状态代码,但确保释放任何响应内容(无论是否预期):
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);
}
});
}
六、Attributes
您可以向请求添加属性。如果您想传递信息,这很方便 通过过滤器链并影响给定请求的过滤器行为。 例如:
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数据填充请求属性。
七、Context
属性提供了将信息传递到过滤器链的便捷方式,但它们只影响当前请求。如果您希望传递传播到嵌套的其他请求的信息,例如通过flatMap,或在嵌套后执行的信息,如通过concatMap,则需要使用Reactor上下文。
反应器上下文需要填充在反应链的末端,以便应用于所有操作。例如:
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可以通过在末尾阻止结果以同步方式使用:
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();
以上只是一个例子。还有很多其他模式和运算符用于放置 一起是一个反应式管道,可以进行许多远程调用,可能有些是嵌套的, 相互依存,直到最后都没有阻塞。
欢迎来到Doker,欢迎点赞和评论!或者加微信进入技术群聊!