Spring5的WebClient使用详解

前言

Spring5带来了新的响应式web开发框架WebFlux,同时,也引入了新的HttpClient框架WebClient。WebClient是Spring5中引入的执行 HTTP 请求的非阻塞、反应式客户端。它对同步和异步以及流方案都有很好的支持,WebClient发布后,RestTemplate将在未来版本中弃用,而且不会向前添加主要新功能。html

WebClient与RestTemplate比较

WebClient是一个功能完善的Http请求客户端,与RestTemplate相比,WebClient支持如下内容:react

  • 非阻塞 I/O。
  • 反应流背压(消费者消费负载太高时主动反馈生产者放慢生产速度的一种机制)。
  • 具备高并发性,硬件资源消耗更少。
  • 流畅的API设计。
  • 同步和异步交互。
  • 流式传输支持

HTTP底层库选择

Spring5的WebClient客户端和WebFlux服务器都依赖于相同的非阻塞编解码器来编码和解码请求和响应内容。默认底层使用Netty,内置支持Jetty反应性HttpClient实现。同时,也能够经过编码的方式实现ClientHttpConnector接口自定义新的底层库;如切换Jetty实现:git

WebClient.builder()
                .clientConnector(new JettyClientHttpConnector())
                .build();

WebClient配置

基础配置

WebClient实例构造器能够设置一些基础的全局的web请求配置信息,好比默认的cookie、header、baseUrl等github

WebClient.builder()
                .defaultCookie("kl","kl")
                .defaultUriVariables(ImmutableMap.of("name","kl"))
                .defaultHeader("header","kl")
                .defaultHeaders(httpHeaders -> {
                    httpHeaders.add("header1","kl");
                    httpHeaders.add("header2","kl");
                })
                .defaultCookies(cookie ->{
                    cookie.add("cookie1","kl");
                    cookie.add("cookie2","kl");
                })
                .baseUrl("http://www.kailing.pub")
                .build();

底层依赖Netty库配置

经过定制Netty底层库,能够配置SSl安全链接,以及请求超时,读写超时等。这里须要注意一个问题,默认的链接池最大链接500。获取链接超时默认是45000ms,你能够配置成动态的链接池,就能够突破这些默认配置,也能够根据业务本身制定。包括Netty的select线程和工做线程也均可以本身设置。web

//配置动态链接池
         //ConnectionProvider provider = ConnectionProvider.elastic("elastic pool");
         //配置固定大小链接池,如最大链接数、链接获取超时、空闲链接死亡时间等
         ConnectionProvider provider = ConnectionProvider.fixed("fixed", 45, 4000, Duration.ofSeconds(6));
         HttpClient httpClient = HttpClient.create(provider)
                 .secure(sslContextSpec -> {
                     SslContextBuilder sslContextBuilder = SslContextBuilder.forClient()
                             .trustManager(new File("E://server.truststore"));
                     sslContextSpec.sslContext(sslContextBuilder);
                 }).tcpConfiguration(tcpClient -> {
                     //指定Netty的select 和 work线程数量
                     LoopResources loop = LoopResources.create("kl-event-loop", 1, 4, true);
                     return tcpClient.doOnConnected(connection -> {
                         //读写超时设置
                         connection.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS))
                                 .addHandlerLast(new WriteTimeoutHandler(10));
                     })
                             //链接超时设置
                             .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
                             .option(ChannelOption.TCP_NODELAY, true)
                             .runOn(loop);
                 });
 
         WebClient.builder()
                 .clientConnector(new ReactorClientHttpConnector(httpClient))
                 .build();

关于链接池的设置,据群友反馈,他们在使用WebClient是并发场景下会抛获取链接异常。异常以下:编程

Caused by: reactor.netty.internal.shaded.reactor.pool.PoolAcquireTimeoutException: Pool#acquire(Duration) has been pending for more than the configured timeout of 45000ms

后经博主深刻研究发现,WebClient底层依赖库reactory-netty在不一样的版本下,初始化默认TcpTcpResources策略不同,博主在网关系统中使用的reactory-netty版本是0.8.3,默认建立的是动态的链接池,即便在并发场景下也没发生过这种异常。而在0.9.x后,初始化的是固定大小的链接池,这位群友正是由于使用的是0.9.1的reactory-netty,在并发时致使链接不可用,等待默认的45s后就抛异常了。因此,使用最新版本的WebClient必定要根据本身的业务场景结合博主上面的Netty HttpClient配置示例合理设置好底层资源。api

编解码配置

针对特定的数据交互格式,能够设置自定义编解码的模式,以下:安全

ExchangeStrategies strategies = ExchangeStrategies.builder()
                .codecs(configurer -> {
                    configurer.customCodecs().decoder(new Jackson2JsonDecoder());
                    configurer.customCodecs().encoder(new Jackson2JsonEncoder());
                })
                .build();
        WebClient.builder()
                .exchangeStrategies(strategies)
                .build();

get请求示例

uri构造时支持属性占位符,真实参数在入参时排序好就能够。同时能够经过accept设置媒体类型,以及编码。最终的结果值是经过Mono和Flux来接收的,在subscribe方法中订阅返回值。服务器

WebClient client = WebClient.create("http://www.kailing.pub");
        Mono<String> result = client.get()
                .uri("/article/index/arcid/{id}.html", 256)
                .acceptCharset(StandardCharsets.UTF_8)
                .accept(MediaType.TEXT_HTML)
                .retrieve()
                .bodyToMono(String.class);
        result.subscribe(System.err::println);

若是须要携带复杂的查询参数,能够经过UriComponentsBuilder构造出uri请求地址,如:websocket

//定义query参数
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("name", "kl");
        params.add("age", "19");
        //定义url参数
        Map<String, Object> uriVariables = new HashMap<>();
        uriVariables.put("id", 200);
        String uri = UriComponentsBuilder.fromUriString("/article/index/arcid/{id}.html")
                .queryParams(params)
                .uriVariables(uriVariables)
                .toUriString();

下载文件时,由于不清楚各类格式文件对应的MIME Type,能够设置accept为MediaType.ALL,而后使用Spring的Resource来接收数据便可,如:

WebClient.create("https://kk-open-public.oss-cn-shanghai.aliyuncs.com/xxx.xlsx")
                .get()
                .accept(MediaType.ALL)
                .retrieve()
                .bodyToMono(Resource.class)
                .subscribe(resource -> {
                    try {
                        File file = new File("E://abcd.xlsx");
                        FileCopyUtils.copy(StreamUtils.copyToByteArray(resource.getInputStream()), file);
                    }catch (IOException ex){}
                });

post请求示例

post请求示例演示了一个比较复杂的场景,同时包含表单参数和文件流数据。若是是普通post请求,直接经过bodyValue设置对象实例便可。不用FormInserter构造。

WebClient client = WebClient.create("http://www.kailing.pub");
        FormInserter formInserter = fromMultipartData("name","kl")
                .with("age",19)
                .with("map",ImmutableMap.of("xx","xx"))
                .with("file",new File("E://xxx.doc"));
        Mono<String> result = client.post()
                .uri("/article/index/arcid/{id}.html", 256)
                .contentType(MediaType.APPLICATION_JSON)
                .body(formInserter)
                //.bodyValue(ImmutableMap.of("name","kl"))
                .retrieve()
                .bodyToMono(String.class);
        result.subscribe(System.err::println);

同步返回结果

上面演示的都是异步的经过mono的subscribe订阅响应值。固然,若是你想同步阻塞获取结果,也能够经过.block()阻塞当前线程获取返回值。

WebClient client =  WebClient.create("http://www.kailing.pub");
      String result = client .get()
                .uri("/article/index/arcid/{id}.html", 256)
                .retrieve()
                .bodyToMono(String.class)
                .block();
        System.err.println(result);

可是,若是须要进行多个调用,则更高效地方式是避免单独阻塞每一个响应,而是等待组合结果,如:

WebClient client =  WebClient.create("http://www.kailing.pub");
        Mono<String> result1Mono = client .get()
                .uri("/article/index/arcid/{id}.html", 255)
                .retrieve()
                .bodyToMono(String.class);
        Mono<String> result2Mono = client .get()
                .uri("/article/index/arcid/{id}.html", 254)
                .retrieve()
                .bodyToMono(String.class);
        Map<String,String>  map = Mono.zip(result1Mono, result2Mono, (result1, result2) -> {
            Map<String, String> arrayList = new HashMap<>();
            arrayList.put("result1", result1);
            arrayList.put("result2", result2);
            return arrayList;
        }).block();
        System.err.println(map.toString());

Filter过滤器

能够经过设置filter拦截器,统一修改拦截请求,好比认证的场景,以下示例,filter注册单个拦截器,filters能够注册多个拦截器,basicAuthentication是系统内置的用于basicAuth的拦截器,limitResponseSize是系统内置用于限制响值byte大小的拦截器

WebClient.builder()
                .baseUrl("http://www.kailing.pub")
                .filter((request, next) -> {
                    ClientRequest filtered = ClientRequest.from(request)
                            .header("foo", "bar")
                            .build();
                    return next.exchange(filtered);
                })
                .filters(filters ->{
                    filters.add(ExchangeFilterFunctions.basicAuthentication("username","password"));
                    filters.add(ExchangeFilterFunctions.limitResponseSize(800));
                })
                .build().get()
                .uri("/article/index/arcid/{id}.html", 254)
                .retrieve()
                .bodyToMono(String.class)
                .subscribe(System.err::println);

websocket支持

WebClient不支持websocket请求,请求websocket接口时须要使用WebSocketClient,如:

WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
        session.receive()
                .doOnNext(System.out::println)
                .then());

结语

咱们已经在业务api网关、短信平台等多个项目中使用WebClient,从网关的流量和稳定足以可见WebClient的性能和稳定性。响应式编程模型是将来的web编程趋势,RestTemplate会逐步被取缔淘汰,而且官方已经不在更新和维护。WebClient很好的支持了响应式模型,并且api设计友好,是博主力荐新的HttpClient库。赶忙试试吧。

相关文章
相关标签/搜索