一、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()。 - 在生产环境,合理设置超时、重试、错误处理等,以避免调用远程服务不可靠导致的问题。
- 异步场景下尽量使用