侧边栏壁纸
  • 累计撰写 98 篇文章
  • 累计创建 20 个标签
  • 累计收到 3 条评论

WebClient学习笔记

林贤钦
2021-01-17 / 0 评论 / 0 点赞 / 833 阅读 / 10,274 字
温馨提示:
本文最后更新于 2021-03-01,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

WebClient学习笔记

Spring中文文档

Spring WebClient vs. RestTemplate

springboot-bucket-gradle全家桶

1、WebClient简介

Spring WebFlux 包含了一个用于HTTP请求的Reactive 非阻塞webClient。Client端具有功能性,Fluent的API,具有用于声明式组个的反应式类型。WebFluxClient端和服务器依靠相同的非阻塞编码解码codecs对请求和响应内容进行编码和解码。

2、阻塞式vs非阻塞式

web应用中,对其他服务进行HTTP调用式一个很常见的需求,因此,我们需要一个web客户端工具。

2.1 RestTemplate 阻塞式客户端

在很长的一段时间中,spring一直提供RestTemplate作为web的客户端抽象,在底层,RestTemplate使用基于每个请求对应一个线程模型的Java Servlet API。

这意味着,知道web端收到响应之前,线程都将一直阻塞下去,而阻塞的代码带来的问题式,每一个线程都消耗了一定的内存和cpu周期。

如果很多传入请求,而且这些请求需要等待产生结果所需的一些慢服务,请求将堆积起来,程序会创建很多线程,这些线程会耗尽线程池或占用所有可用内存。而且频繁的CPU上下文切换,我们还会遇到性能下降的问题。

2.2 WebClient 非阻塞式客户端

而webClient则是Spring Reactive Framework 所提供的异步非阻塞解决方案。

在spring的官方文档中,建议我们使用webClient,这个是spring在5.x声明替换RestTemplate的一种解决方案,并且以后不会为RestTemplate添加任何主要的新功能。

当RestTemplate为每个事件(HTTP请求)创建一个新的线程时,WebClient将为每个事件创建一个类似任务的东东 ,幕后。Reactive框架会对这些“任务”进行排队,并且仅在适当的响应可用时执行他们。

Reactive 框架使用事件驱动的体系结构。它提供了通过 Reactive Streams API 组合异步逻辑的方法。因此,与同步/阻塞方法相比,Reactive 可以使用更少的线程和系统资源来处理更多的逻辑。

WebClient是 Spring WebFlux 库的一部分。因此,我们还可以使用流畅的函数式 API 编写客户端代码,并将响应类型(Mono 和 Flux)作为声明来进行组合。

3、Configuration 配置

3.1、创建WebClient

创建WebClient的最简单方法是通过静态工厂方法之一

  • WebClient.create()
  • WebClient.create(String baseUrl)

上面的方法使用HttpClient具有默认设置的Reactor Netty ,并且期望 io.projectreactor.netty:reactor-netty在类路径上。

还可以将WebClient.builder()与其他选项一起使用:

  • uriBuilderFactory:自定义UriBuilderFactory用作基本 URL。
  • defaultHeader:每个请求的标题。
  • defaultCookie:每个请求的 cookie。
  • defaultRequestConsumer自定义每个请求。
  • filter:针对每个请求的 Client 端过滤器。
  • exchangeStrategies:HTTP 消息读取器/写入器定制。
  • clientConnector:HTTPClient 端库设置。
ExchangeStrategies strategies = ExchangeStrategies.builder()
            .codecs(configurer -> {
                // ...
            })
            .build();
    WebClient client = WebClient.builder()
            .exchangeStrategies(strategies)
            .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

3.2、自定义设置

要自定义 Reactor Netty 设置,只需提供一个预先配置的HttpClient

HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);

    WebClient webClient = WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();

Resources

默认情况下,HttpClient参与reactor.netty.http.HttpResources拥有的全局 Reactor Netty 资源,包括事件循环线程和连接池。这是推荐的模式,因为固定的共享资源是事件循环并发的首选。在这种模式下,全局资源将保持活动状态,直到进程退出。

如果服务器为该进程计时,则通常无需显式关闭。但是,如果服务器可以启动或停止进程内(例如,作为 WAR 部署的 Spring MVC 应用程序),则可以声明ReactorResourceFactoryglobalResources=true(默认值)类型的 Spring 托管 bean,以确保 Reactor Netty 全局 SpringApplicationContext关闭时,资源将关闭,如以下示例所示:

@Bean
public ReactorResourceFactory reactorResourceFactory() {
	return new ReactorResourceFactory();
}

您也可以选择不参与全局 Reactor Netty 资源。但是,在这种模式下,确保所有 Reactor NettyClient 端和服务器实例使用共享资源是您的重担,如以下示例所示:

@Bean
public ReactorResourceFactory resourceFactory() {
    ReactorResourceFactory factory = new ReactorResourceFactory();
    factory.setUseGlobalResources(false);
    return factory;
}

@Bean
public WebClient webClient() {
    Function<HttpClient, HttpClient> mapper = client -> {
        // Further customizations...
        return client;
    };
    ClientHttpConnector connector =
        new ReactorClientHttpConnector(resourceFactory(), mapper);
    return WebClient.builder().clientConnector(connector).build();
}
  • (1) 创建独立于全局资源的资源。
  • (2)ReactorClientHttpConnector构造函数与资源工厂一起使用。
  • (3) 将连接器插入WebClient.Builder

3.3、Timeouts

配置连接超时:

import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));

要配置读取和/或写入超时值

import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(10))
                        .addHandlerLast(new WriteTimeoutHandler(10))));

在创建的时候在clientConnector指定

WebClient client = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build();

也可以通过.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))配置

TcpClient tcpClient = TcpClient
        .create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS,5000)
        .doOnConnected(connection -> {
            connection.addHandlerLast(new ReadTimeoutHandler(3000, TimeUnit.MILLISECONDS));
            connection.addHandlerLast(new WriteTimeoutHandler(3000, TimeUnit.MILLISECONDS));
         });
        WebClient client = WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))   
                .build();

4、retrieve

该retrieve()方法是获取响应主体并对其进行解码的最简单方法。以下示例显示了如何执行此操作:

WebClient client = WebClient.create("http://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 状态特定的子类之一,例如WebClientResponseException.BadRequestWebClientResponseException.NotFound等。您还可以使用onStatus方法来自定义产生的异常,如以下示例所示:

Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .onStatus(HttpStatus::is4xxServerError, response -> ...)
            .onStatus(HttpStatus::is5xxServerError, response -> ...)
            .bodyToMono(Person.class);

使用onStatus时,如果期望响应包含内容,则onStatus回调应使用它。否则,内容将自动耗尽以确保释放资源。

5、exchange

exchange()方法比retrieve方法提供更多的控制权。以下示例与retrieve()等效,但也提供对ClientResponse的访问

Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.bodyToMono(Person.class));

在此级别,您还可以创建完整的ResponseEntity

Mono<ResponseEntity<Person>> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.toEntity(Person.class));

请注意(与retrieve()不同),对于exchange(),没有 4xx 和 5xx 响应的自动错误 signal。您必须检查状态码并决定如何进行。

使用exchange()时,必须始终使用ClientResponse的任何bodytoEntity方法,以确保释放资源并避免 HTTP 连接池的潜在问题。如果不需要响应内容,则可以使用bodyToMono(Void.class)。但是,如果响应中确实包含内容,则连接将关闭并且不会放回池中。

6、请求正文

Object实体

可以从Object编码请求主体,如以下示例所示:

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

如果您具有实际值,则可以使用syncBody快捷方式,如以下示例所示:

Person person = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .syncBody(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)
            .syncBody(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);

7、 Client 端过滤器

可以通过WebClient.Builder注册 Client 端过滤器(ExchangeFilterFunction),以拦截和修改请求,如以下示例所示:

WebClient client = WebClient.builder()
        .filter((request, next) -> {

            ClientRequest filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build();

            return next.exchange(filtered);
        })
        .build();

这可以用于跨领域的关注,例如身份验证。以下示例使用过滤器通过静态工厂方法进行基本身份验证:

WebClient client = WebClient.builder()
        .filter(basicAuthentication("user", "password"))
        .build();

过滤器全局应用于每个请求。要更改特定请求的过滤器行为,可以将请求属性添加到ClientRequest,然后链中的所有过滤器都可以访问该属性,如以下示例所示:

WebClient client = WebClient.builder()
        .filter((request, next) -> {
            Optional<Object> usr = request.attribute("myAttribute");
            // ...
        })
        .build();

client.get().uri("http://example.org/")
        .attribute("myAttribute", "...")
        .retrieve()
        .bodyToMono(Void.class);

    }

您也可以复制现有的WebClient,插入新的过滤器或删除已注册的过滤器。以下示例在索引 0 处插入一个基本身份验证过滤器:

WebClient client = webClient.mutate()
        .filters(filterList -> {
            filterList.add(0, basicAuthentication("user", "password"));
        })
        .build();

8、代码测试

https://gitee.com/linxianqin/springboot-bucket-gradle/tree/master/WebClient

0

评论区