Spring WebClient 介绍

Part 1 WebClient 概述

在本教程中,我们将简单说明 WebClient,什么是 WebClient?

简单地说,WebClient 是用于发送 HTTP(S) 请求工具。它是 Spring Web Reactive 模块的一部分,从发展的趋势来看,WebClient 后续将会替代传统 Spring RestTemplate。WebClient 具有响应式、非阻塞特点,需要在 HTTP/1.1 协议上运行。需要注意的是,WebClient 也支持同步调用方案哦~

Part 2 依赖

我们测试用的是 Spring Boot 项目,所以我这里需要引入 spring-boot-starter-webflux 依赖:

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

Part 3 WebClient 的使用

WebClient 的使用,主要包括三个方面:

  • 创建 WebClient 的实例
  • 发出请求
  • 处理响应结果

3.1 创建 WebClient 的实例

3.1.1 简单创建

创建实例有多种形式,第一种也是最简单的一种,直接使用默认的配置:

WebClient client = WebClient.create();

当然,在创建的时候,我们也可以指定 URL 的前缀地址:

WebClient client = WebClient.create("http://localhost:8080");

除了,这种简单粗暴的形式,我们也可以使用 DefaultWebClientBuilder 去构建,可以客制化一些配置:

WebClient client = WebClient.builder()
  .baseUrl("http://localhost:8080")
  .defaultCookie("cookieKey", "cookieValue")
  .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
  .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
  .build();

3.1.2 配置超时时间

默认的 HTTP 连接超时是 30 秒,这个默认值对于我们的需求,往往太长了,如果我们想客制化,我们需要创建一个 HttpClient 实例,进行相关设置:

  • 通过 ChannelOption.CONNECT_TIMEOUT_MILLIS 设置连接超时
  • 通过 ReadTimeoutHandler and WriteTimeoutHandler 分别设置读和写的超时
  • 通过 responseTimeout 直接配置响应超时
HttpClient httpClient = HttpClient.create();
httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
httpClient.responseTimeout(Duration.ofMillis(5000));
httpClient.doOnConnected(conn -> 
           conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS));
httpClient.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));

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

注意:我们也可以通过 WebClient 调用 timeout(),这个超时是针对 Mono/Flux publisher 的,并不是 HTTP 的连接,读写以及响应等超时

3.1.3 免除 SSL 校验

在内网的一些场景中,有时候会访问类似于 https://192.168.221.151/dzgs 的接口地址(真坑,不符合逻辑),我们可以使用 TrustManagerFactory 去创建:

@Bean
public WebClient createWebClient() throws SSLException {
    SslContext sslContext = SslContextBuilder
            .forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE)
            .build();
    HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(sslContext));
    return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();
}

3.2 发出请求

3.2.1 定义请求方法

首先,我们需要指定 HTTP 请求方法,可以使用 method() 方法:

UriSpec<RequestBodySpec> uriSpec = client.method(HttpMethod.POST);

也可以直接指定:

UriSpec<RequestBodySpec> uriSpec = client.post();

3.2.2 定义 URL

第二步,指定 URL,这里我们提供三种方式:

// 指定传路径的字符串
RequestBodySpec bodySpec = uriSpec.uri("/resource");

// 通过 uri 构造器
RequestBodySpec bodySpec = uriSpec.uri(
  uriBuilder -> uriBuilder.pathSegment("/resource").build());

// 使用 java.net.URL 实例
RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));

注意:如果我们定义了 baseURL,这时候我们的路径是全路径的话,base URL 就不会生效

3.2.3 定义请求体

设置请求体,有多种多样的方法

// 使用 bodyValue 方法:
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");

// Publisher 
RequestHeadersSpec<?> headersSpec = bodySpec.body(
  Mono.just(new Foo("name")), Foo.class);

// 使用 BodyInserters 工具类
RequestHeadersSpec<?> headersSpec = bodySpec.body(
  BodyInserters.fromValue("data"));

RequestHeadersSpec headersSpec = bodySpec.body(
  BodyInserters.fromPublisher(Mono.just("data")),
  String.class);

// 重要:比较常用的一种方式,这种方式会使 Content-Type 是 multipart/form-data
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap();
map.add("key1", "value1");
map.add("key2", "value2");
RequestHeadersSpec<?> headersSpec = bodySpec.body(
  BodyInserters.fromMultipartData(map));

3.2.4 定义请求头

在我们设置完请求体,我们可以设置 Headers、Cookies、接受的 Content Type

ResponseSpec responseSpec = headersSpec.header(
    HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
  .accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
  .acceptCharset(StandardCharsets.UTF_8)
  .ifNoneMatch("*")
  .ifModifiedSince(ZonedDateTime.now())
  .retrieve();

3.3 获取响应

设置完以上,我们可以发送请求,获取响应。有三种方式:

  • exchangeToMono
  • exchangeToFlux
  • retrieve

其中,exchangeToMonoexchangeToFlux是可以拿到响应 status 和响应 headers 的,而 retrieve 直接获活响应体

Mono<String> response = headersSpec.exchangeToMono(response -> {
  if (response.statusCode().equals(HttpStatus.OK)) {
      return response.bodyToMono(String.class);
  } else if (response.statusCode().is4xxClientError()) {
      return Mono.just("Error response");
  } else {
      return response.createException()
        .flatMap(Mono::error);
  }
});


// 重要:比较常用的一种方式
Mono<String> response = headersSpec.retrieve().bodyToMono(String.class);

// 阻塞处理: 等待获取响应结果
String responseStr = monoResponse.block();
    
// 非阻塞处理:通过 response 消费接口
monoResponse.subscribe(callBack)

注意:当响应的 status 是 4xx 和 5xx 的时候,ResponseSpec.bodyToMono 方法会抛出 WebClientException 异常

Part 4 示例代码

package club.hwtechzone.utilities;

import com.fasterxml.jackson.databind.JsonNode;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

import javax.net.ssl.SSLException;
import java.io.FileInputStream;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.function.Consumer;

public class RequestSender {

    public static void main(String[] args) throws InterruptedException, IOException {
        String getTestUrl = "https://1.13.193.135:10000/apis/getversion";
        String postTestUrl = "https://xxxx:10000/apis/account/Login";
        String uploadFileUrl = "https://xxxx:10000/apis/attachment/UploadFile";

        MultiValueMap<String, String> requestParams = new LinkedMultiValueMap<>();
        requestParams.add("name", "chaosMoor");
        requestParams.add("pageSize", "100");
        get(getTestUrl, requestParams, System.out::println);

        LinkedMultiValueMap<String, Object> loginInfo = new LinkedMultiValueMap<>();
        loginInfo.add("loginName", "MTU2MKYU1MzQ=");
        loginInfo.add("password", "MTIzNDU2");
        JsonNode loginResponse = post(postTestUrl, loginInfo);
        System.out.println(loginResponse.toPrettyString());

        LinkedMultiValueMap<String, Object> uploadFile = new LinkedMultiValueMap<>();
        FileInputStream fileInputStream = new FileInputStream("C:\\Users\\ChaosMoor\\Desktop\\serverInfo\\01-sinotrans\\test.txt");
        ByteArrayResource contentsAsResource = new ByteArrayResource(fileInputStream.readAllBytes()) {
            @Override
            public String getFilename() {
                return "text.xlsx";
            }
        };

        uploadFile.add("file", contentsAsResource);
        Map<String, String> headers = Map.of("hwtoken", "KUKU");
        JsonNode uploadFileResponse = post(uploadFileUrl, uploadFile, headers);
        System.out.println(uploadFileResponse.toPrettyString());

    }


    public static JsonNode get(String requestPath, MultiValueMap<String, String> requestParams) {
        Mono<JsonNode> monoResponse = sendGet(requestPath, requestParams, null);
        return monoResponse.block();
    }

    public static JsonNode get(String requestPath, MultiValueMap<String, String> requestParams, Map<String, String> headers) {
        Mono<JsonNode> monoResponse = sendGet(requestPath, requestParams, headers);
        return monoResponse.block();
    }

    public static JsonNode get(String requestPath, MultiValueMap<String, String> requestParams, int timeout) {
        Mono<JsonNode> monoResponse = sendGet(requestPath, requestParams, null);
        return monoResponse.block(Duration.ofSeconds(timeout));
    }

    public static JsonNode get(String requestPath, MultiValueMap<String, String> requestParams, int timeout, Map<String, String> headers) {
        Mono<JsonNode> monoResponse = sendGet(requestPath, requestParams, headers);
        return monoResponse.block(Duration.ofSeconds(timeout));
    }

    public static void get(String requestPath, MultiValueMap<String, String> requestParams, Consumer<JsonNode> callBack) {
        Mono<JsonNode> monoResponse = sendGet(requestPath, requestParams, null);
        monoResponse.subscribe(callBack);
    }

    public static void get(String requestPath, MultiValueMap<String, String> requestParams, Map<String, String> headers, Consumer<JsonNode> callBack) {
        Mono<JsonNode> monoResponse = sendGet(requestPath, requestParams, headers);
        monoResponse.subscribe(callBack);
    }

    public static JsonNode post(String requestPath, LinkedMultiValueMap<String, Object> requestParams) {
        Mono<JsonNode> monoResponse = sendPost(requestPath, requestParams, null);
        return monoResponse.block();
    }

    public static JsonNode post(String requestPath, LinkedMultiValueMap<String, Object> requestParams, Map<String, String> headers) {
        Mono<JsonNode> monoResponse = sendPost(requestPath, requestParams, headers);
        return monoResponse.block();
    }

    public static JsonNode post(String requestPath, LinkedMultiValueMap<String, Object> requestParams, int timeout) {
        Mono<JsonNode> monoResponse = sendPost(requestPath, requestParams, null);
        return monoResponse.block(Duration.ofSeconds(timeout));
    }

    public static JsonNode post(String requestPath, LinkedMultiValueMap<String, Object> requestParams, int timeout, Map<String, String> headers) {
        Mono<JsonNode> monoResponse = sendPost(requestPath, requestParams, headers);
        return monoResponse.block(Duration.ofSeconds(timeout));
    }

    public static void post(String requestPath, LinkedMultiValueMap<String, Object> requestParams, Consumer<JsonNode> callBack) {
        Mono<JsonNode> monoResponse = sendPost(requestPath, requestParams, null);
        monoResponse.subscribe(callBack);
    }

    public static void post(String requestPath, LinkedMultiValueMap<String, Object> requestParams, Map<String, String> headers, Consumer<JsonNode> callBack) {
        Mono<JsonNode> monoResponse = sendPost(requestPath, requestParams, headers);
        monoResponse.subscribe(callBack);
    }
    
    private static Mono<JsonNode> sendPost(String requestPath, LinkedMultiValueMap<String, Object> requestParams, Map<String, String> headers) {
        WebClient.RequestBodySpec requestBodySpec = createIgnoreSslWebClient().post().uri(requestPath);
        if (headers != null) {
            headers.forEach(requestBodySpec::header);
        }
        return requestBodySpec.body(BodyInserters.fromMultipartData(requestParams)).retrieve().bodyToMono(JsonNode.class);
    }

    private static Mono<JsonNode> sendGet(String requestPath, MultiValueMap<String, String> requestParams, Map<String, String> headers) {
        String url = composeGetRequestPath(requestPath, requestParams);
        WebClient.RequestHeadersSpec<?> requestBodySpec = createIgnoreSslWebClient().get().uri(url);
        if (headers != null) {
            headers.forEach(requestBodySpec::header);
        }
        return requestBodySpec.retrieve().bodyToMono(JsonNode.class);
    }

    private static String composeGetRequestPath(String requestPath, MultiValueMap<String, String> requestParams) {
        return requestParams == null? requestPath: UriComponentsBuilder.fromHttpUrl(requestPath).queryParams(requestParams).toUriString();
    }

    public static WebClient createIgnoreSslWebClient() {
        try {
            SslContext sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
            HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(sslContext));

            return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();
        } catch (SSLException sslException) {
            throw new RuntimeException(sslException);
        }
    }


}

reference web site: https://www.baeldung.com/spring-5-webclient

posted @ 2022-09-17 17:08  ChaosMoor  阅读(3564)  评论(3编辑  收藏  举报