WebClient 与 RestClient
概述
在 Spring 生态中,HTTP 客户端经历了从 RestTemplate 到 WebClient 再到 RestClient 的演进。Spring Framework 6.x 引入了 RestClient,它结合了 RestTemplate 的同步编程模型和 WebClient 的现代化 API 设计。WebClient 作为响应式 HTTP 客户端,在高并发和流式处理场景中扮演重要角色。本文将深入剖析这两大客户端的核心原理、配置方式以及生产级实战规范。
一、Spring 6 RestClient 新特性
1.1 诞生背景
RestTemplate 自 Spring 3.0 起就是主力 HTTP 客户端,但其 API 设计逐渐臃肿。Spring 5.0 推出响应式的 WebClient,但对同步模型项目迁移成本过高。Spring 6.1 推出的 RestClient 填补了这一空白:
- 提供与
WebClient一致的 Fluent API 风格 - 保持与
RestTemplate相同的同步阻塞语义 - 天然支持 HTTP Interface 声明式客户端
1.2 快速上手
java
RestClient restClient = RestClient.create();
RestClient customClient = RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer {token}")
.defaultRequest(request -> {
request.header("X-Request-Id", UUID.randomUUID().toString());
})
.build();1.3 GET 请求
java
String result = restClient.get()
.uri("/users/{id}", 1001).retrieve().body(String.class);
User user = restClient.get()
.uri("/users/{id}", 1001).retrieve().body(User.class);
ResponseEntity<User> response = restClient.get()
.uri("/users/{id}", 1001).retrieve().toEntity(User.class);
List<User> users = restClient.get()
.uri("/users").retrieve()
.body(new ParameterizedTypeReference<>() {});1.4 POST / PUT / DELETE
java
User createdUser = restClient.post()
.uri("/users").contentType(MediaType.APPLICATION_JSON)
.body(new User("Alice", "alice@example.com"))
.retrieve().body(User.class);
restClient.put().uri("/users/{id}", 1001)
.contentType(MediaType.APPLICATION_JSON)
.body(new User("Alice Updated", "alice@example.com"))
.retrieve().toBodilessEntity();
restClient.delete().uri("/users/{id}", 1001)
.retrieve().toBodilessEntity();1.5 错误处理
java
User user = restClient.get().uri("/users/{id}", 99999).retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
throw new CustomClientException("用户不存在");
})
.onStatus(HttpStatusCode::is5xxServerError, (req, res) -> {
throw new CustomServerException(new String(res.getBody().readAllBytes()));
})
.body(User.class);
// Exchange 接口
Optional<User> userOpt = restClient.get().uri("/users/{id}", 1001)
.exchange((req, res) -> res.getStatusCode().is2xxSuccessful()
? Optional.of(objectMapper.readValue(res.getBody(), User.class))
: Optional.empty());1.6 HTTP Interface 声明式客户端
java
@HttpExchange("/users")
public interface UserClient {
@GetExchange("/{id}") User getUser(@PathVariable Long id);
@PostExchange User createUser(@RequestBody User user);
}
@Bean
public UserClient userClient(RestClient.Builder builder) {
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(
builder.baseUrl("https://api.example.com").build()))
.build().createClient(UserClient.class);
}二、WebClient 响应式 HTTP 客户端
2.1 核心特性
WebClient 基于 Reactor 构建,核心特性包括:
- 非阻塞 I/O:基于 Netty/Jetty 事件驱动模型
- 响应式背压:天然支持 Reactive Streams 背压机制
- 流式处理:支持请求体和响应体的流式读写
2.2 创建 WebClient
java
WebClient webClient = WebClient.create();
WebClient webClient = WebClient.create("https://api.example.com");
WebClient webClient = WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultCookie("session-id", UUID.randomUUID().toString())
.build();
WebClient customized = webClient.mutate()
.defaultHeader("X-Custom-Header", "value")
.build();2.3 基本请求
java
User user = webClient.get().uri("/users/{id}", 1001)
.retrieve().bodyToMono(User.class).block(Duration.ofSeconds(5));
Mono<User> userMono = webClient.get().uri("/users/{id}", 1001)
.retrieve().bodyToMono(User.class);
Flux<User> users = webClient.get().uri("/users")
.retrieve().bodyToFlux(User.class);
Mono<User> created = webClient.post().uri("/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new User("Bob", "bob@example.com"))
.retrieve().bodyToMono(User.class);
// exchangeToMono
Mono<User> result = webClient.get().uri("/users/{id}", 1001)
.exchangeToMono(res -> res.statusCode().is2xxSuccessful()
? res.bodyToMono(User.class)
: res.createException().flatMap(Mono::error));2.4 过滤器 Filter
java
WebClient loggingClient = WebClient.builder()
.filter((request, next) -> {
Instant start = Instant.now();
return next.exchange(request).doOnNext(response ->
log.info("{} {} -> {} 耗时: {}ms", request.method(), request.url(),
response.statusCode(),
Duration.between(start, Instant.now()).toMillis()));
})
.build();三、连接池 HttpComponentsClientHttpConnector
3.1 RestClient 连接池配置
java
@Configuration
public class RestClientPoolConfig {
@Bean
public RestClient pooledRestClient() {
PoolingHttpClientConnectionManager mgr = new PoolingHttpClientConnectionManager();
mgr.setMaxTotal(200);
mgr.setDefaultMaxPerRoute(50);
ConnectionConfig connCfg = ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(5))
.setSocketTimeout(Timeout.ofSeconds(10))
.setValidateAfterInactivity(Timeout.ofSeconds(3))
.build();
mgr.setDefaultConnectionConfig(connCfg);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(mgr)
.evictExpiredConnections()
.evictIdleConnections(Timeout.ofSeconds(30))
.build();
return RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient))
.build();
}
}3.2 WebClient Reactor Netty 连接池
java
@Configuration
public class ReactorNettyPoolConfig {
@Bean
public WebClient reactorNettyWebClient() {
ConnectionProvider provider = ConnectionProvider.builder("custom-pool")
.maxConnections(200)
.maxIdleTime(Duration.ofSeconds(30))
.maxLifeTime(Duration.ofMinutes(5))
.pendingAcquireTimeout(Duration.ofSeconds(10))
.evictInBackground(Duration.ofSeconds(30))
.build();
HttpClient httpClient = HttpClient.create(provider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.option(ChannelOption.TCP_NODELAY, true)
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS))
.addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS)))
.responseTimeout(Duration.ofSeconds(10));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}四、超时重试配置与实现
4.1 RestClient 超时配置
java
@Bean
public RestClient timeoutRestClient() {
var factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
factory.setConnectionRequestTimeout(3000); // 连接池获取超时
return RestClient.builder().requestFactory(factory).build();
}4.2 WebClient 超时配置
java
@Bean
public WebClient timeoutWebClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(10))
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}4.3 RestClient 重试
java
@Component
public class RestClientRetryService {
private final RestClient restClient;
// 方式一:Spring Retry 注解
@Retryable(retryFor = {HttpServerErrorException.class, ResourceAccessException.class},
maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2.0, maxDelay = 5000))
public User getUserWithRetry(Long id) {
return restClient.get().uri("/users/{id}", id).retrieve().body(User.class);
}
// 方式二:RetryTemplate 编程式
public User getUserWithRetryTemplate(Long id) {
RetryTemplate template = RetryTemplate.builder()
.maxAttempts(3).exponentialBackoff(1000, 2.0, 5000)
.retryOn(HttpServerErrorException.class).retryOn(ResourceAccessException.class)
.build();
return template.execute(ctx -> {
log.info("第 {} 次尝试", ctx.getRetryCount() + 1);
return restClient.get().uri("/users/{id}", id).retrieve().body(User.class);
});
}
}4.4 WebClient 重试
java
@Component
public class WebClientRetryService {
private final WebClient webClient;
public Mono<User> getUserBasicRetry(Long id) {
return webClient.get().uri("/users/{id}", id).retrieve()
.bodyToMono(User.class).retryWhen(Retry.max(3));
}
public Mono<User> getUserWithBackoff(Long id) {
return webClient.get().uri("/users/{id}", id).retrieve()
.bodyToMono(User.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.maxBackoff(Duration.ofSeconds(5)).jitter(0.5)
.filter(ex -> ex instanceof HttpServerErrorException
|| ex instanceof ConnectException || ex instanceof TimeoutException));
}
}4.5 统一配置
yaml
http-client:
connect-timeout: 5000
read-timeout: 10000
max-connections: 200
max-per-route: 50
retry:
max-attempts: 3
initial-interval: 1000
multiplier: 2.0
max-interval: 5000java
@Configuration
@ConfigurationProperties(prefix = "http-client")
@Data
public class HttpClientProperties {
private int connectTimeout = 5000;
private int readTimeout = 10000;
private int maxConnections = 200;
private int maxPerRoute = 50;
private RetryProperties retry = new RetryProperties();
@Data
public static class RetryProperties {
private int maxAttempts = 3;
private long initialInterval = 1000;
private double multiplier = 2.0;
private long maxInterval = 5000;
}
}五、负载均衡集成(Spring Cloud LoadBalancer)
5.1 RestClient 集成
java
@Configuration
public class LoadBalancerConfig {
@Bean @LoadBalanced
public RestClient.Builder loadBalancedRestClientBuilder() {
return RestClient.builder();
}
@Bean
public RestClient loadBalancedRestClient(@LoadBalanced RestClient.Builder builder) {
return builder.build();
}
}
@Service
public class UserServiceClient {
@Autowired private RestClient restClient;
public User getUserById(Long id) {
return restClient.get()
.uri("http://user-service/api/users/{id}", id)
.retrieve().body(User.class);
}
}5.2 WebClient 集成
java
@Configuration
public class WebClientLoadBalancerConfig {
@Bean @LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
@Bean
public WebClient loadBalancedWebClient(@LoadBalanced WebClient.Builder builder) {
return builder.build();
}
}
@Service
public class OrderServiceClient {
@Autowired private WebClient webClient;
public Mono<Order> getOrderById(Long id) {
return webClient.get()
.uri("http://order-service/api/orders/{id}", id)
.retrieve().bodyToMono(Order.class);
}
}5.3 自定义负载均衡策略
java
@Bean
@LoadBalancerClient(name = "user-service", configuration = RandomConfig.class)
public RestClient userServiceClient(@LoadBalanced RestClient.Builder builder) {
return builder.build();
}
public static class RandomConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorLoadBalancer(
Environment env, LoadBalancerClientFactory factory) {
String name = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}5.4 服务发现配置
yaml
spring:
cloud:
loadbalancer:
enabled: true
retry:
enabled: true
health-check:
path: /actuator/health
interval: 30s
nacos:
discovery:
server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
namespace: ${NACOS_NAMESPACE:public}
group: DEFAULT_GROUP六、RestClient vs WebClient vs RestTemplate 对比
6.1 功能对比
| 特性 | RestTemplate | WebClient | RestClient |
|---|---|---|---|
| 推出版本 | Spring 3.0 | Spring 5.0 | Spring 6.1 |
| 编程模型 | 同步阻塞 | 响应式非阻塞 | 同步阻塞 |
| API 风格 | 模板方法 | Fluent Chain | Fluent Chain |
| 底层引擎 | HttpComponents | Reactor Netty | HttpComponents |
| 非阻塞 I/O | ❌ | ✅ | ❌ |
| HTTP Interface | ❌ | ✅ | ✅ |
| 错误处理 | ResponseErrorHandler | onStatus / exchange | onStatus / exchange |
| 高并发性能 | 一般 | 优秀 | 一般 |
| 学习曲线 | 低 | 中高 | 低 |
| 维护状态 | 维护模式 | 活跃开发 | 活跃开发(推荐) |
6.2 选型建议
text
项目使用 WebFlux? → 是 → WebClient(响应式栈天然选择)
→ 否 → 需要高并发 (>1000 TPS)?
├── 是 → WebClient + block()
└── 否 → RestClient(推荐)
Spring Boot 版本:
├── < 3.0 → RestTemplate
├── 3.0 ≤ x < 3.2 → WebClient
└── ≥ 3.2 → RestClient(首选)+ WebClient(响应式场景)6.3 迁移示例
java
// RestTemplate 旧代码
public class OldService {
private final RestTemplate rt;
public User get(Long id) { return rt.getForObject("/u/{id}", User.class, id); }
public User create(User u) { return rt.postForObject("/u", u, User.class); }
}
// RestClient 新代码
public class NewService {
private final RestClient rc;
public User get(Long id) {
return rc.get().uri("/u/{id}", id).retrieve().body(User.class);
}
public User create(User u) {
return rc.post().uri("/u").contentType(MediaType.APPLICATION_JSON)
.body(u).retrieve().body(User.class);
}
}七、实战:微服务间 HTTP 调用规范
7.1 整体架构
text
调用方:统一 HTTP 客户端 → TraceId 透传 → 超时重试 → 熔断降级
↓ 服务名解析 ↓ 负载均衡 ↓
注册中心 (Nacos/Consul) → 服务提供方
↓
服务方:接收 TraceId → 统一异常响应 → 监控指标 (Micrometer)7.2 统一 HTTP 客户端 Starter
java
@AutoConfiguration
@ConditionalOnClass(RestClient.class)
@EnableConfigurationProperties(HttpClientProperties.class)
public class HttpClientAutoConfiguration {
@Bean @ConditionalOnMissingBean
public RestClient.Builder restClientBuilder(
HttpClientProperties props,
ObjectProvider<ClientHttpRequestInterceptor> interceptors) {
var connCfg = ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(props.getConnectTimeout()))
.setSocketTimeout(Timeout.ofMilliseconds(props.getReadTimeout())).build();
var mgr = new PoolingHttpClientConnectionManager();
mgr.setMaxTotal(props.getMaxConnections());
mgr.setDefaultMaxPerRoute(props.getMaxPerRoute());
mgr.setDefaultConnectionConfig(connCfg);
var client = HttpClients.custom().setConnectionManager(mgr)
.evictExpiredConnections().evictIdleConnections(Timeout.ofSeconds(30))
.disableAutomaticRetries().build();
var builder = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory(client));
interceptors.orderedStream().forEach(builder::requestInterceptor);
return builder;
}
@Bean @ConditionalOnMissingBean
public RestClient restClient(RestClient.Builder builder) { return builder.build(); }
}7.3 TraceId 透传
java
// RestClient 拦截器
public class TraceIdRestClientInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest req, byte[] body,
ClientHttpRequestExecution exec) throws IOException {
String traceId = MDC.get("traceId");
req.getHeaders().set("X-Trace-Id",
traceId != null ? traceId : UUID.randomUUID().toString().replace("-", ""));
return exec.execute(req, body);
}
}
// WebClient 过滤器
@Component
public class TraceIdWebClientFilter implements ExchangeFilterFunction {
@Override
public Mono<ClientResponse> filter(ClientRequest req, ExchangeFunction next) {
String traceId = MDC.get("traceId");
return next.exchange(ClientRequest.from(req)
.header("X-Trace-Id",
traceId != null ? traceId : UUID.randomUUID().toString().replace("-", ""))
.build());
}
}7.4 统一异常处理
java
@Data
public class HttpInvocationException extends RuntimeException {
private int statusCode;
private String serviceName;
private String requestUri;
private String errorBody;
public HttpInvocationException(String serviceName, String uri,
HttpStatusCode statusCode, String errorBody) {
super(String.format("调用 [%s] %s 返回异常: %s, body: %s",
serviceName, uri, statusCode, errorBody));
this.serviceName = serviceName;
this.requestUri = uri;
this.statusCode = statusCode.value();
this.errorBody = errorBody;
}
}
@Data
public class ErrorResponse {
private int code;
private String message;
private String traceId;
private String path;
private long timestamp;
}7.5 熔断降级 + 统一调用模板
java
@Component
public class ServiceInvoker {
private static final Logger log = LoggerFactory.getLogger(ServiceInvoker.class);
private final RestClient restClient;
private final Map<String, CircuitBreaker> cbMap = new ConcurrentHashMap<>();
private static <T> T handleError(String svc, HttpRequest req, byte[] body,
ClientHttpResponse resp) throws IOException {
String b = new String(resp.getBody().readAllBytes(), StandardCharsets.UTF_8);
throw new HttpInvocationException(svc, req.getURI().toString(), resp.getStatusCode(), b);
}
public <T> T get(String svc, String uri, Class<T> type, T fallback) {
return invoke(svc, () -> restClient.get()
.uri("http://{s}" + uri, svc).retrieve()
.onStatus(HttpStatusCode::isError, ServiceInvoker::handleError)
.body(type), fallback);
}
public <T, R> R post(String svc, String uri, T body, Class<R> type, R fallback) {
return invoke(svc, () -> restClient.post()
.uri("http://{s}" + uri, svc)
.contentType(MediaType.APPLICATION_JSON).body(body).retrieve()
.onStatus(HttpStatusCode::isError, ServiceInvoker::handleError)
.body(type), fallback);
}
private <T> T invoke(String svc, Supplier<T> call, T fallback) {
CircuitBreaker cb = cbMap.computeIfAbsent(svc,
n -> CircuitBreakerRegistry.of(CircuitBreakerConfig.custom()
.failureRateThreshold(50).waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(10).minimumNumberOfCalls(5).build()).circuitBreaker(n));
return Decorators.ofSupplier(call).withCircuitBreaker(cb)
.withFallback(ex -> { log.error("[{}] 降级: {}", svc, ex.getMessage()); return fallback; })
.get();
}
}
@Service
@RequiredArgsConstructor
public class OrderFacadeService {
private final ServiceInvoker invoker;
public User getUser(Long id) {
return invoker.get("user-service", "/api/users/" + id, User.class,
new User(id, "降级用户", null));
}
}八、最佳实践总结
8.1 场景选型
| 场景 | 推荐方案 |
|---|---|
| 新项目 (Spring Boot ≥ 3.2) | RestClient 首选,WebClient 用于响应式 |
| 旧项目升级 | RestTemplate → RestClient 渐进迁移 |
| 高并发网关 / BFF | WebClient + Reactor Netty |
| 简单 CRUD | RestClient |
8.2 配置建议
- 配置连接池 — 避免每次请求创建新连接
- 超时必设 — connectTimeout、readTimeout、connectionRequestTimeout
- 退避重试 — Exponential Backoff + Jitter,避免重试风暴
- 熔断保护 — 关键路径配置熔断器,防止级联故障
- TraceId 透传 — 全链路日志可追溯,结合 MDC 实现
8.3 依赖坐标
xml
<dependency> <!-- RestClient(Spring Boot 3.x 内置) -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <!-- WebClient -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency> <!-- Apache HttpClient 5 连接池 -->
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency> <!-- Spring Retry -->
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency> <!-- Resilience4j 熔断 -->
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<dependency> <!-- LoadBalancer -->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency> <!-- 链路追踪 -->
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>