Spring5的WebClient使用详解

前言

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

WebClient与RestTemplate比较

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

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

HTTP底层库选择

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

1 WebClient.builder() 2 .clientConnector(new JettyClientHttpConnector()) 3 .build();

WebClient配置

基础配置

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

1WebClient.builder() 2 .defaultCookie("kl","kl") 3 .defaultUriVariables(ImmutableMap.of("name","kl")) 4 .defaultHeader("header","kl") 5 .defaultHeaders(httpHeaders -> { 6 httpHeaders.add("header1","kl"); 7 httpHeaders.add("header2","kl"); 8 }) 9 .defaultCookies(cookie ->{ 10 cookie.add("cookie1","kl"); 11 cookie.add("cookie2","kl"); 12 }) 13 .baseUrl("http://www.kailing.pub") 14 .build();

底层依赖Netty库配置

通过定制Netty底层库,可以配置SSl安全连接,以及请求超时,读写超时等。这里需要注意一个问题,默认的连接池最大连接500。获取连接超时默认是45000ms,你可以配置成动态的连接池,就可以突破这些默认配置,也可以根据业务自己制定。包括Netty的select线程和工作线程也都可以自己设置。

1 //配置动态连接池 2 //ConnectionProvider provider = ConnectionProvider.elastic("elastic pool"); 3 //配置固定大小连接池,如最大连接数、连接获取超时、空闲连接死亡时间等 4 ConnectionProvider provider = ConnectionProvider.fixed("fixed", 45, 4000, Duration.ofSeconds(6)); 5 HttpClient httpClient = HttpClient.create(provider) 6 .secure(sslContextSpec -> { 7 SslContextBuilder sslContextBuilder = SslContextBuilder.forClient() 8 .trustManager(new File("E://server.truststore")); 9 sslContextSpec.sslContext(sslContextBuilder); 10 }).tcpConfiguration(tcpClient -> { 11 //指定Netty的select 和 work线程数量 12 LoopResources loop = LoopResources.create("kl-event-loop", 1, 4, true); 13 return tcpClient.doOnConnected(connection -> { 14 //读写超时设置 15 connection.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS)) 16 .addHandlerLast(new WriteTimeoutHandler(10)); 17 }) 18 //连接超时设置 19 .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) 20 .option(ChannelOption.TCP_NODELAY, true) 21 .runOn(loop); 22 }); 23 24 WebClient.builder() 25 .clientConnector(new ReactorClientHttpConnector(httpClient)) 26 .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配置示例合理设置好底层资源。

编解码配置

针对特定的数据交互格式,可以设置自定义编解码的模式,如下:

1 ExchangeStrategies strategies = ExchangeStrategies.builder() 2 .codecs(configurer -> { 3 configurer.customCodecs().decoder(new Jackson2JsonDecoder()); 4 configurer.customCodecs().encoder(new Jackson2JsonEncoder()); 5 }) 6 .build(); 7 WebClient.builder() 8 .exchangeStrategies(strategies) 9 .build();

get请求示例

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

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

如果需要携带复杂的查询参数,可以通过UriComponentsBuilder构造出uri请求地址,如:

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

下载文件时,因为不清楚各种格式文件对应的MIME Type,可以设置accept为MediaType.ALL,然后使用Spring的Resource来接收数据即可,如:

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

post请求示例

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

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

同步返回结果

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

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

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

1 WebClient client = WebClient.create("http://www.kailing.pub"); 2 Mono<String> result1Mono = client .get() 3 .uri("/article/index/arcid/{id}.html", 255) 4 .retrieve() 5 .bodyToMono(String.class); 6 Mono<String> result2Mono = client .get() 7 .uri("/article/index/arcid/{id}.html", 254) 8 .retrieve() 9 .bodyToMono(String.class); 10 Map<String,String> map = Mono.zip(result1Mono, result2Mono, (result1, result2) -> { 11 Map<String, String> arrayList = new HashMap<>(); 12 arrayList.put("result1", result1); 13 arrayList.put("result2", result2); 14 return arrayList; 15 }).block(); 16 System.err.println(map.toString());

Filter过滤器

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

1 WebClient.builder() 2 .baseUrl("http://www.kailing.pub") 3 .filter((request, next) -> { 4 ClientRequest filtered = ClientRequest.from(request) 5 .header("foo", "bar") 6 .build(); 7 return next.exchange(filtered); 8 }) 9 .filters(filters ->{ 10 filters.add(ExchangeFilterFunctions.basicAuthentication("username","password")); 11 filters.add(ExchangeFilterFunctions.limitResponseSize(800)); 12 }) 13 .build().get() 14 .uri("/article/index/arcid/{id}.html", 254) 15 .retrieve() 16 .bodyToMono(String.class) 17 .subscribe(System.err::println);

websocket支持

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

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

结语

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

作者简介:

陈凯玲,2016年5月加入凯京科技。现任凯京科技研发中心架构组经理,救火队队长。独立博客KL博客(http://www.kailing.pub)博主。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )