一、WebClient 概述
1.1 什么是 WebClient
- WebClient 是 Spring 5 引入的一个 基于响应式编程模型 的 HTTP 客户端。
- 与传统的 RestTemplate 相比,WebClient 采用了 Reactor 库,支持 非阻塞式(异步)调用,可充分利用多核 CPU 资源,提升高并发场景的吞吐量。
- 它能够非常灵活地构造并发送 HTTP 请求(支持 GET、POST、PUT、DELETE、PATCH 等所有常见方法),并以 流(
Mono
/Flux
)的方式处理响应结果。
1.2 适用场景
- 高并发:在微服务或需要频繁调用远程服务的项目中,WebClient 的异步特性可以极大提升性能。
- 响应式应用:如果你的项目基于 Spring WebFlux(响应式编程),WebClient 就是标配。
- 替代 RestTemplate:Spring 官方逐渐建议用 WebClient 替代 RestTemplate,尤其在新项目中。
二、如何创建 WebClient
2.1 最简单的创建方式
WebClient webClient = WebClient.create();
解释:
WebClient.create()
:调用 WebClient 的静态方法create()
,创建一个最简单的 WebClient 实例。- 没有 传入任何参数,意味着没有设置基础 URL,也没有任何默认请求头等配置,完全靠调用时手动指定。
2.2 带基础 URL 的创建方式
WebClient webClient = WebClient.create("https://api.example.com");
解释:
- 同样使用
WebClient.create(...)
,但这次传入一个字符串https://api.example.com
。 - 这表示 WebClient 在发起请求时,如果使用相对路径(如
/users
),就会自动拼接这个基础路径(变成https://api.example.com/users
)。 - 这样可以简化多次调用同一远程服务时的路径书写。
2.3 使用 Builder 进行更多配置
WebClient webClient = WebClient.builder().baseUrl("https://api.example.com").defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer my-token").build();
解释:
WebClient.builder()
:返回一个 Builder 对象,允许配置更多细节。.baseUrl("...")
:设置基础 URL,作用与前一个示例类似。.defaultHeader(HttpHeaders.CONTENT_TYPE, ...)
:为 所有 请求添加默认请求头,指定内容类型为application/json
。.defaultHeader(HttpHeaders.AUTHORIZATION, ...)
:设置授权信息(如 Bearer Token),方便需要身份认证的场景。.build()
:最终构建出一个拥有上述配置的 WebClient 实例。
2.4 在 Spring 项目中注册为 Bean
@Configuration
public class WebClientConfig {@Beanpublic WebClient webClient(WebClient.Builder builder) {return builder.baseUrl("https://api.example.com").defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build();}
}
解释:
- 这是一个 配置类(
@Configuration
),Spring 会在容器启动时扫描并初始化它。 @Bean
:声明一个 Bean 方法webClient
,返回类型是WebClient
。- 方法参数
WebClient.Builder builder
:Spring Boot 在启动时会自动注入一个 Builder。 .build()
:构建并返回一个带基础 URL 和默认请求头的 WebClient。- 这样,其他类中只需
@Autowired WebClient webClient;
就可直接使用。
三、WebClient 的核心方法与示例
3.1 通用流程
每次使用 WebClient 发起请求,大致分为三步:
- 指定 HTTP 方法(GET、POST、PUT、DELETE 等)和 URI;
- 调用
.retrieve()
或.exchangeToMono()
发送请求; - 使用
.bodyToMono()
或.bodyToFlux()
解析响应体。
3.2 GET 请求示例
Mono<User> userMono = webClient.get().uri("/users/{id}", 1).retrieve().bodyToMono(User.class);
解释:
webClient.get()
:声明一个 GET 请求。.uri("/users/{id}", 1)
:指定请求路径/users/1
,其中{id}
会被1
替换。.retrieve()
:执行请求,并准备获取响应体;如果响应状态码是 4xx/5xx,将会自动抛出异常。.bodyToMono(User.class)
:将响应 JSON 反序列化成User
对象,并封装在一个Mono<User>
中。Mono
表示 “可能产生 0 或 1 个元素” 的流,在这里就意味着一个User
。
WebClient
支持通过ParameterizedTypeReference
指定泛型类型,从而避免原始类型的问题。new ParameterizedTypeReference<Map<String, Object>>() {}
会创建一个匿名类,保留泛型信息。-
import org.springframework.core.ParameterizedTypeReference; import org.springframework.web.reactive.function.client.WebClient;Map<String, Object> response = webClient.get().uri(url).retrieve().bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {}).block();
如何拿到 User?
- 阻塞方式(不推荐在高并发场景):
User user = userMono.block(); System.out.println(user.getName());
- 异步方式(推荐):
userMono.subscribe(user -> {System.out.println(user.getName()); }); System.out.println("我先执行,不会阻塞等待");
3.3 POST 请求示例
User newUser = new User(2, "Alice");Mono<User> createdUserMono = webClient.post().uri("/users").contentType(MediaType.APPLICATION_JSON) .bodyValue(newUser).retrieve().bodyToMono(User.class);
解释:
webClient.post()
:声明一个 POST 请求。.uri("/users")
:目标路径为/users
。.contentType(...)
:设置请求体的 Content-Type 为application/json
。.bodyValue(newUser)
:将newUser
对象作为请求体发送。.retrieve()
:执行请求并返回响应。.bodyToMono(User.class)
:将响应体 JSON 解析为一个User
对象,封装在Mono
中。
3.4 PUT 请求示例
User updatedUser = new User(1, "Updated Name");webClient.put().uri("/users/{id}", 1).contentType(MediaType.APPLICATION_JSON).bodyValue(updatedUser).retrieve().bodyToMono(Void.class).block();
解释:
webClient.put()
:声明一个 PUT 请求。.uri("/users/{id}", 1)
:目标资源是/users/1
。.bodyValue(updatedUser)
:更新的用户数据。.retrieve().bodyToMono(Void.class)
:PUT 请求通常不返回数据,这里用Void.class
代表空。.block()
:此处为了示例,使用阻塞方式直接等待请求完成。
3.5 DELETE 请求示例
webClient.delete().uri("/users/{id}", 1).retrieve().bodyToMono(Void.class).block();
解释:
webClient.delete()
:声明一个 DELETE 请求。.uri("/users/{id}", 1)
:删除路径/users/1
。- 同样
.retrieve() -> .bodyToMono(Void.class)
,一般 DELETE 请求也不会返回体。 - 最后
.block()
以便我们知道删除完成(在真实响应式场景也可用.subscribe()
)。
四、响应式编程中的常见操作
4.1 Mono
与 Flux
区别
Mono<T>
:最多产生一个元素(0 或 1 个)。Flux<T>
:可以产生多个元素,类似 “数据流”。
如何把 Flux<User>
转为列表?
Flux<User> userFlux = webClient.get().uri("/users").retrieve().bodyToFlux(User.class);List<User> userList = userFlux.collectList().block();
.collectList()
:将流中的所有 User 收集为一个List<User>
。.block()
:阻塞方式拿到列表。
4.2 block()
与 subscribe()
-
block()
:阻塞当前线程,直到Mono
或Flux
产生结果。- 简化代码,但失去异步优势。
- 在响应式编程中尽量少用,除非在测试或必须同步的场合。
-
subscribe()
:非阻塞,注册回调函数。- 当数据就绪时,会自动调用回调。
- 适合真正的响应式应用场景。
五、高级特性与配置
5.1 自定义错误处理
默认情况下,.retrieve()
在遇到 4xx/5xx 状态码时会抛出 WebClientResponseException
。可以自定义处理:
Mono<User> userMono = webClient.get().uri("/users/{id}", 999) // 假设该用户不存在.retrieve().onStatus(HttpStatus::is4xxClientError, response -> response.bodyToMono(String.class).flatMap(err -> Mono.error(new RuntimeException("Client Error: " + err)))).onStatus(HttpStatus::is5xxServerError, response -> response.bodyToMono(String.class).flatMap(err -> Mono.error(new RuntimeException("Server Error: " + err)))).bodyToMono(User.class);userMono.subscribe(user -> System.out.println("User: " + user),error -> System.err.println("Error: " + error.getMessage())
);
解释:
.onStatus(...)
:自定义对特定状态码的处理。bodyToMono(String.class)
:这里获取错误信息(如后端返回的错误描述)。Mono.error(new RuntimeException(...))
:抛出自定义异常,交给后续处理。subscribe(...)
:处理正常结果和错误结果。
5.2 并发请求与结果合并
当需要同时调用多个接口时,可以并行发起多个请求,然后合并结果。例如,一个请求获取 User
,另一个请求获取 Order
。
Mono<User> userMono = webClient.get().uri("/users/{id}", 1).retrieve().bodyToMono(User.class);Mono<Order> orderMono = webClient.get().uri("/orders/{id}", 101).retrieve().bodyToMono(Order.class);Mono<String> combined = Mono.zip(userMono, orderMono).map(tuple -> {User user = tuple.getT1();Order order = tuple.getT2();return "User: " + user.getName() + ", Order: " + order.getProductName();});combined.subscribe(System.out::println);
解释:
Mono<User> userMono
:获取用户数据的 Mono。Mono<Order> orderMono
:获取订单数据的 Mono。Mono.zip(userMono, orderMono)
:并行执行两个 Mono,并在都完成后合并结果。tuple.getT1()/getT2()
:分别代表第一个和第二个 Mono 的结果。.subscribe(...)
:非阻塞执行,最后打印结果。
5.3 超时设置
可以通过整合 Reactor Netty 配置超时。示例如下:
import reactor.netty.http.client.HttpClient;WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create().responseTimeout(Duration.ofSeconds(5)) // 响应超时)).build();
解释:
HttpClient.create()
:创建一个 Reactor Netty 的 HTTP 客户端。.responseTimeout(Duration.ofSeconds(5))
:表示如果在 5 秒内没收到响应,就会超时。new ReactorClientHttpConnector(...)
:将自定义的 HttpClient 注入到 WebClient 中。.build()
:构建出带超时配置的 WebClient。
5.4 文件上传
假设后端接口接收一个名为 file
的字段(Multipart),可通过以下代码:
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("file", new FileSystemResource("path/to/file.jpg"));webClient.post().uri("/upload").body(BodyInserters.fromMultipartData(formData)).retrieve().bodyToMono(String.class).subscribe(response -> System.out.println("Upload response: " + response));
解释:
MultiValueMap<String, Object> formData
:多值映射,用于构建 multipart/form-data 请求体。new FileSystemResource("path/to/file.jpg")
:将本地文件包装成一个Resource
对象。.body(BodyInserters.fromMultipartData(formData))
:把上面的表单数据作为请求体(Multipart)。.retrieve().bodyToMono(String.class)
:接收服务器返回的字符串(如上传结果)。.subscribe(...)
:异步处理结果。
六、.retrieve()
与 .exchangeToMono()
区别
-
.retrieve()
:- 简化操作,自动 在 4xx/5xx 异常时抛出
WebClientResponseException
。 - 无法直接访问响应头或状态码,需通过
.onStatus()
做额外处理。 - 推荐在只关心响应体时使用。
- 简化操作,自动 在 4xx/5xx 异常时抛出
-
.exchangeToMono()
:- 返回
ClientResponse
,可访问 全部 响应信息(状态码、头部、Body)。 - 需要手动判断状态码并决定下一步操作。
- 适用于需要更灵活控制响应的场景。
- 返回
示例:
Mono<User> userMono = webClient.get().uri("/users/{id}", 1).exchangeToMono(response -> {if (response.statusCode().is2xxSuccessful()) {// 成功则将 body 转为 Userreturn response.bodyToMono(User.class);} else {// 非 2xx,拿到错误体或抛出异常return response.createException().flatMap(Mono::error);}});
七、总结
-
WebClient 优势
- 非阻塞:在高并发场景下能更高效地使用线程。
- 响应式:与
Mono
、Flux
完整配合,支持流式数据和异步操作。 - 强大灵活:可自定义请求头、请求体、超时、拦截器,还可应对文件上传、并发调用等复杂场景。
- Spring 官方推荐:新项目或使用 Spring WebFlux 的场景下,优先使用 WebClient 而非 RestTemplate。
-
注意事项
- 异步场景下尽量使用
.subscribe(...)
,避免滥用.block()
导致阻塞。 - 如果需要更复杂的响应处理(访问响应头或状态码),考虑用
.exchangeToMono()
代替.retrieve()
。 - 在生产环境,合理设置超时、重试、错误处理等,以避免调用远程服务不可靠导致的问题。
- 异步场景下尽量使用