Spring5的WebClient使用详解

SpringBoot 专栏收录该内容
72 篇文章 4 订阅

 

1 前言

以前文章介绍了:

RestTemplate:https://blog.csdn.net/zzhongcy/article/details/104674808

AsyncRestTemplate:https://blog.csdn.net/zzhongcy/article/details/105410316

这里介绍一下另外一个新兴的http客户端:WebClient

2 介绍

在Spring 5之前,如果我们想要调用其他系统提供的HTTP服务,我们通常可以使用Spring提供的RestTemplate来访问。

RestTemplate用法很简单,但是它的不足之处在于它的请求是同步阻塞模式,因此存在一定性能瓶颈。当然,如果想要使用异步方式请求,也可以使用AsyncRestTemplate。

从Spring 5开始,Spring中全面引入了Reactive响应式编程,WebClient就属于Spring WebFlux的一部分。WebClient的请求模式属于异步非阻塞、反应式的,能够以少量固定的线程处理高并发的HTTP请求。它对同步和异步以及流方案都有很好的支持。

因此,从Spring 5开始,HTTP服务之间的通信我们就可以考虑使用WebClient来取代之前的RestTemplate。

有个这样的说法:RestTemplate将在将来版本中弃用,并且不会向前添加主要新功能。

3 WebClient与RestTemplate比较

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

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

4 添加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

5 HTTP底层库选择

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

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

6 WebClient配置

6.1 基础配置

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

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

6.2 底层依赖Netty库配置

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

        //配置动态连接池
         //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配置示例合理设置好底层资源。

6.3 编解码配置

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

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

7 示例

7.1 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请求地址,如:

        //定义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){}
                });

另外get请求示例:

  • 使用placeholder传递参数
    @Test
    public void testUrlPlaceholder(){
        Mono<String> resp = WebClient.create()
                .get()
                //多个参数也可以直接放到map中,参数名与placeholder对应上即可
                .uri("http://www.baidu.com/s?wd={key}&other={another}","北京天气","test") //使用占位符
                .retrieve()
                .bodyToMono(String.class);
        LOGGER.info("result:{}",resp.block());

    }
  • 使用uriBuilder传递参数
    @Test
    public void testUrlBiulder(){
        Mono<String> resp = WebClient.create()
                .get()
                .uri(uriBuilder -> uriBuilder
                        .scheme("http")
                        .host("www.baidu.com")
                        .path("/s")
                        .queryParam("wd", "北京天气")
                        .queryParam("other", "test")
                        .build())
                .retrieve()
                .bodyToMono(String.class);
        LOGGER.info("result:{}",resp.block());
    }

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

post表单

    @Test
    public void testFormParam(){
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("name1","value1");
        formData.add("name2","value2");
        Mono<String> resp = WebClient.create().post()
                .uri("http://www.w3school.com.cn/test/demo_form.asp")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .body(BodyInserters.fromFormData(formData))
                .retrieve().bodyToMono(String.class);
        LOGGER.info("result:{}",resp.block());
    }

post json

  • 使用bean来post
static class Book {
        String name;
        String title;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }
    }

    @Test
    public void testPostJson(){
        Book book = new Book();
        book.setName("name");
        book.setTitle("this is title");
        Mono<String> resp = WebClient.create().post()
                .uri("http://localhost:8080/demo/json")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(Mono.just(book),Book.class)
                .retrieve().bodyToMono(String.class);
        LOGGER.info("result:{}",resp.block());
    }
  • 直接post raw json
    @Test
    public void testPostRawJson(){
        Mono<String> resp = WebClient.create().post()
                .uri("http://localhost:8080/demo/json")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject("{\n" +
                        "  \"title\" : \"this is title\",\n" +
                        "  \"author\" : \"this is author\"\n" +
                        "}"))
                .retrieve().bodyToMono(String.class);
        LOGGER.info("result:{}",resp.block());
    }

post二进制--上传文件

    @Test
    public void testUploadFile(){
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.IMAGE_PNG);
        HttpEntity<ClassPathResource> entity = new HttpEntity<>(new ClassPathResource("parallel.png"), headers);
        MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
        parts.add("file", entity);
        Mono<String> resp = WebClient.create().post()
                .uri("http://localhost:8080/upload")
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .body(BodyInserters.fromMultipartData(parts))
                .retrieve().bodyToMono(String.class);
        LOGGER.info("result:{}",resp.block());
    }

 

7.3 同步返回结果

上面演示的都是异步的通过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());

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

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

 

7.6 请求携带header

  • 携带cookie
    @Test
    public void testWithCookie(){
        Mono<String> resp = WebClient.create()
                .method(HttpMethod.GET)
                .uri("http://baidu.com")
                .cookie("token","xxxx")
                .cookie("JSESSIONID","XXXX")
                .retrieve()
                .bodyToMono(String.class);
        LOGGER.info("result:{}",resp.block());
    }
  • 携带basic auth
    @Test
    public void testWithBasicAuth(){
        String basicAuth = "Basic "+ Base64.getEncoder().encodeToString("user:pwd".getBytes(StandardCharsets.UTF_8));
        LOGGER.info(basicAuth);
        Mono<String> resp = WebClient.create()
                .get()
                .uri("http://baidu.com")
                .header(HttpHeaders.AUTHORIZATION,basicAuth)
                .retrieve()
                .bodyToMono(String.class);
        LOGGER.info("result:{}",resp.block());
    }
  • 设置全局user-agent
    @Test
    public void testWithHeaderFilter(){
        WebClient webClient = WebClient.builder()
                .defaultHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36")
                .filter(ExchangeFilterFunctions
                        .basicAuthentication("user","password"))
                .filter((clientRequest, next) -> {
                    LOGGER.info("Request: {} {}", clientRequest.method(), clientRequest.url());
                    clientRequest.headers()
                            .forEach((name, values) -> values.forEach(value -> LOGGER.info("{}={}", name, value)));
                    return next.exchange(clientRequest);
                })
                .build();
        Mono<String> resp = webClient.get()
                .uri("https://baidu.com")
                .retrieve()
                .bodyToMono(String.class);
        LOGGER.info("result:{}",resp.block());
    }

7.7 下载二进制

  • 下载图片
    @Test
    public void testDownloadImage() throws IOException {
        Mono<Resource> resp = WebClient.create().get()
                .uri("http://www.toolip.gr/captcha?complexity=99&size=60&length=9")
                .accept(MediaType.IMAGE_PNG)
                .retrieve().bodyToMono(Resource.class);
        Resource resource = resp.block();
        BufferedImage bufferedImage = ImageIO.read(resource.getInputStream());
        ImageIO.write(bufferedImage, "png", new File("captcha.png"));

    }
  • 下载文件
    @Test
    public void testDownloadFile() throws IOException {
        Mono<ClientResponse> resp = WebClient.create().get()
                .uri("http://localhost:8080/file/download")
                .accept(MediaType.APPLICATION_OCTET_STREAM)
                .exchange();
        ClientResponse response = resp.block();
        String disposition = response.headers().asHttpHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION);
        String fileName = disposition.substring(disposition.indexOf("=")+1);
        Resource resource = response.bodyToMono(Resource.class).block();
        File out = new File(fileName);
        FileUtils.copyInputStreamToFile(resource.getInputStream(),out);
        LOGGER.info(out.getAbsolutePath());
    }

7.8 错误处理

    @Test
    public void testRetrieve4xx(){
        WebClient webClient = WebClient.builder()
                .baseUrl("https://api.github.com")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
                .defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
                .build();
        WebClient.ResponseSpec responseSpec = webClient.method(HttpMethod.GET)
                .uri("/user/repos?sort={sortField}&direction={sortDirection}",
                        "updated", "desc")
                .retrieve();
        Mono<String> mono = responseSpec
                .onStatus(e -> e.is4xxClientError(),resp -> {
                    LOGGER.error("error:{},msg:{}",resp.statusCode().value(),resp.statusCode().getReasonPhrase());
                    return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
                })
                .bodyToMono(String.class)
                .doOnError(WebClientResponseException.class, err -> {
                    LOGGER.info("ERROR status:{},msg:{}",err.getRawStatusCode(),err.getResponseBodyAsString());
                    throw new RuntimeException(err.getMessage());
                })
                .onErrorReturn("fallback");
        String result = mono.block();
        LOGGER.info("result:{}",result);
    }
  • 可以使用onStatus根据status code进行异常适配
  • 可以使用doOnError异常适配
  • 可以使用onErrorReturn返回默认值

8. RestTemplate、WebClient、Feign 的介绍

https://mp.weixin.qq.com/s/udVIPbkuoHRyGbIgZDtrpg

 

9. 结语

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

webclient是新一代的async rest template,api也相对简洁,而且是reactive的,非常值得使用。

 

参考

https://segmentfault.com/a/1190000021133071

https://www.zifangsky.cn/1343.html

https://segmentfault.com/a/1190000012916413

https://stackoverflow.com/questions/44514263/decode-bytearray-with-spring-5-webflux-framework

https://www.594cto.com/content/049cf4caac6941a7b94f7190dcb0d66f

http://blog.didispace.com/spring-cloud-alibaba-2/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

https://mp.weixin.qq.com/s/udVIPbkuoHRyGbIgZDtrpg

  • 3
    点赞
  • 1
    评论
  • 11
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

评论 1 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:程序猿惹谁了 设计师:我叫白小胖 返回首页

打赏作者

zzhongcy

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值