写在前面
在 Spring 5 之前,如果我们想要调用其他系统提供的 HTTP 服务,通常可以使用 Spring 提供的 RestTemplate 来访问,不过由于 RestTemplate 是 Spring 3 中引入的同步阻塞式 HTTP 客户端,因此存在一定性能瓶颈。根据 Spring 官方文档介绍,在将来的版本中它可能会被弃用。
WebClient是Spring WebFlux模块提供的一个非阻塞的基于响应式编程的进行Http请求的客户端工具,从Spring5.0开始提供。 WebClient有一个基于Reactor的功能性的、流畅的API,它支持异步逻辑的声明式组合,而无需处理线程或并发性。它是完全无阻塞的,支持流,并且依赖于同样的编解码器,这些编解码器也用于在服务器端编码和解码请求和响应内容。
一、配置-Configuration
1、基本用法
(1)、创建WebClient
# 创建WebClient
WebClient.create()
# 创建WebClient并且指定baseURL
WebClient.create(String baseUrl)
(2)、指定额外配置 可以使用WebClient.builder() 指定额外的配置。
- uriBuilderFactory: 用作定制baseURL。
- defaultUriVariables: 扩展URI模板时使用的默认值。
- defaultHeader: 设置每个请求的默认header。
- defaultCookie: 设置每个请求的默认cookie
- defaultRequest: 设置每个消费者自定义请求
- filter: 请求过滤器。
- exchangeStrategies: HTTP消息读取器/写入器自定义。
- clientConnector: HTTP客户端库设置。
例如:
WebClient.builder(...)
.uriBuilderFactory(...)
.defaultCookie(...)
.defaultHeaders(...)
.build();
(3)、不变性 一旦构建完成,WebClient就是不可变的。但是,可以克隆它并构建一个修改后的副本,如下所示:
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
2、最大内存大小-MaxInMemorySize
为了避免应用程序内存问题,编解码器对内存中的数据缓存有限制。默认情况下,它们被设置为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();
3、配置Reactor Netty
要自定义Reactor Netty设置,请提供一个预配置的HttpClient:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
(1)、资源(Resources) 默认情况下,HttpClient参与reactor.netty.http.HttpResources中保存的全局Reactor Netty资源,包括事件循环线程和连接池。这是推荐的模式,因为固定的共享资源是事件循环并发的首选。在这种模式下,全局资源保持活动状态,直到进程退出。
如果服务器与进程同步,则通常不需要显式关闭。然而,如果服务器可以在进程内启动或停止(例如,一个部署为WAR的Spring MVC应用程序),你可以声明一个类型为ReactorResourceFactory的Spring管理bean,使用globalResources=true(默认值)来确保当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 -> {
};
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper);
return WebClient.builder().clientConnector(connector).build();
}
(2)、超时时间设定(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)));
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
4、配置使用Jetty
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();
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory());
return WebClient.builder().clientConnector(connector).build();
}
5、配置Apache的HttpComponents
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
二、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);
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
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()方法适用于需要更多控制的更高级情况,例如根据响应状态对响应进行不同的解码
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 {
return response.createException().flatMap(Mono::error);
}
});
当使用上述方法时,在返回的Mono或Flux完成后,检查响应体,如果没有被使用,则释放它以防止内存和连接泄漏。因此,该响应不能在下游进一步解码。如果需要,由提供的函数来声明如何解码响应。
四、设置请求体(Request Body)
1、发送json
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);
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
2、发送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);
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);
3、发送Multipart Data
要发送多部分数据,需要提供一个MultiValueMap<String,?>其值是表示部件内容的对象实例或表示部件内容和标题的HttpEntity实例。MultipartBodyBuilder提供了一个方便的API来准备多部分请求。以下示例显示了如何创建多值映射< 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);
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
在大多数情况下,不必为每个部分指定内容类型。内容类型是根据选择用来序列化它的HttpMessageWriter自动确定的,或者在资源的情况下,是根据文件扩展名确定的。如有必要,可以通过重载的构建器部件方法之一显式地提供用于每个部件的MediaType。
准备好多值映射后,将它传递给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包装器。
作为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);
MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("timestamp", time);
param.add("file", new FileSystemResource(tempFile));
result = client.post()
.uri(/path)
.body(BodyInserters.fromMultipartData(param))
.retrieve()
.bodyToMono(Void.class)
五、过滤器Filters
通过WebClient注册客户端过滤器(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上全局配置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 -> {
})
.contextWrite(context -> context.put("foo", ...));
八、阻塞用法
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();
以上仅仅是一个例子。有许多其他的模式和操作符可以组合成一个反应式的管道,进行许多远程调用,可能是嵌套的、相互依赖的,直到最后都不会阻塞。
有了Flux或Mono,你就永远不必在Spring MVC或Spring WebFlux控制器中阻塞。只需从控制器方法返回结果反应类型。同样原理也适用于Kotlin协同程序和Spring WebFlux,只需在控制器方法中使用暂停函数或返回流。
参考资料
https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client
|