springboot-webFlux的webclient详细使用介绍,细节拉满

文章目录

  • 写在前面
  • 一、配置-Configuration
    • 1、基本用法
    • 2、最大内存大小-MaxInMemorySize
    • 3、配置Reactor Netty
    • 4、配置使用Jetty
    • 5、配置Apache的HttpComponents
  • 二、retrieve()方法
  • 三、Exchange方法
  • 四、设置请求体(Request Body)
    • 1、发送json
    • 2、发送Form Data
    • 3、发送Multipart Data
  • 五、过滤器Filters
  • 六、设置属性Attributes
  • 七、设置上下文Context
  • 八、阻塞用法
  • 参考资料

写在前面

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

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD

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客户端和服务器实例使用共享资源的责任在您身上,如下面的示例所示:

// 1.创建独立于全局资源的资源
@Bean
public ReactorResourceFactory resourceFactory() {
    ReactorResourceFactory factory = new ReactorResourceFactory();
    factory.setUseGlobalResources(false); 
    return factory;
}

@Bean
public WebClient webClient() {

    Function<HttpClient, HttpClient> mapper = client -> {
        // Further customizations...
    };
// 2.将ReactorClientHttpConnector构造函数与资源工厂一起使用。
    ClientHttpConnector connector =
            new ReactorClientHttpConnector(resourceFactory(), mapper); 
// 3.将连接器插入WebClient.Builder。
    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)));

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

4、配置使用Jetty

// 使用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();
    // Further customizations...

// 1.使用资源工厂的JettyClientHttpConnector构造函数
    ClientHttpConnector connector =
            new JettyClientHttpConnector(httpClient, resourceFactory()); 

// 2.将连接器插入WebClient.Builder
    return WebClient.builder().clientConnector(connector).build(); 
}

5、配置Apache的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()方法可用于声明如何提取响应,将响应转为ResponseEntity。
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体,将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);
// 默认情况下,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()方法适用于需要更多控制的更高级情况,例如根据响应状态对响应进行不同的解码

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完成后,检查响应体,如果没有被使用,则释放它以防止内存和连接泄漏。因此,该响应不能在下游进一步解码。如果需要,由提供的函数来声明如何解码响应。

四、设置请求体(Request Body)

1、发送json

// 请求体可以从ReactiveAdapterRegistry处理的任何异步类型进行编码
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);

2、发送Form Data

要发送表单数据,可以提供一个MultiValueMap作为主体。注意,内容是由FormHttpMessageWriter自动设置为application/x-www-form-urlencoded的。以下示例显示了如何使用MultiValueMap:

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

3、发送Multipart Data

要发送多部分数据,需要提供一个MultiValueMap其值是表示部件内容的对象实例或表示部件内容和标题的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); // Part from a server request

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
MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("timestamp", time);
param.add("file", new FileSystemResource(tempFile));
// post方式
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 -> {
                // 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();

以上仅仅是一个例子。有许多其他的模式和操作符可以组合成一个反应式的管道,进行许多远程调用,可能是嵌套的、相互依赖的,直到最后都不会阻塞。

有了Flux或Mono,你就永远不必在Spring MVC或Spring WebFlux控制器中阻塞。只需从控制器方法返回结果反应类型。同样原理也适用于Kotlin协同程序和Spring WebFlux,只需在控制器方法中使用暂停函数或返回流。

参考资料

https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client

你可能感兴趣的:(springboot,spring,boot,spring,java)