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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通