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
其中,exchangeToMono
、exchangeToFlux
是可以拿到响应 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