WebClient 原理及实践—官方原版

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();

以上只是一个例子。还有很多其他模式和运算符用于放置 一起是一个反应式管道,可以进行许多远程调用,可能有些是嵌套的, 相互依存,直到最后都没有阻塞。

官网:webclient (spring.io)

欢迎来到Doker,欢迎点赞和评论!或者加微信进入技术群聊!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值