Skip to content

WebClient 同步使用指南:从响应式到阻塞式的优雅转换 🔄

概述:为什么需要同步使用 WebClient?

WebClient 作为 Spring 5 引入的响应式 HTTP 客户端,天生就是为异步、非阻塞的响应式编程而设计的。但在实际开发中,我们有时需要在传统的同步代码中使用它,或者需要等待 HTTP 调用的结果才能继续执行后续逻辑。

IMPORTANT

WebClient 的同步使用本质上是将响应式流转换为阻塞式调用,这在某些场景下是必要的,但需要谨慎使用以避免性能问题。

核心概念:阻塞 vs 非阻塞

在深入代码之前,让我们理解一下关键概念:

基本同步使用方式

单个请求的同步处理

kotlin
@Service
class UserService(private val webClient: WebClient) {
    
    // 获取单个用户信息
    fun getUserById(userId: Long): User? {
        return runBlocking { 
            webClient.get()
                .uri("/users/{id}", userId)
                .retrieve()
                .awaitBody<User>() 
        }
    }
    
    // 获取用户列表
    fun getAllUsers(): List<User> {
        return runBlocking { 
            webClient.get()
                .uri("/users")
                .retrieve()
                .bodyToFlow<User>() 
                .toList() 
        }
    }
}
java
@Service
public class UserService {
    
    private final WebClient webClient;
    
    public UserService(WebClient webClient) {
        this.webClient = webClient;
    }
    
    // 获取单个用户信息
    public User getUserById(Long userId) {
        return webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .bodyToMono(User.class)
            .block(); 
    }
    
    // 获取用户列表
    public List<User> getAllUsers() {
        return webClient.get()
            .uri("/users")
            .retrieve()
            .bodyToFlux(User.class)
            .collectList() 
            .block(); 
    }
}

TIP

在 Kotlin 中,使用 runBlocking 和协程扩展函数(如 awaitBody())比 Java 的 .block() 更加优雅和高效。

高效的多请求并发处理

当需要同时发起多个 HTTP 请求时,避免逐个阻塞是关键!

❌ 低效的串行方式

kotlin
// 不推荐:串行执行,效率低下
fun getUserWithHobbiesSerial(userId: Long): UserWithHobbies {
    return runBlocking {
        // 先获取用户信息
        val user = webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .awaitBody<User>()
        
        // 再获取爱好信息(等待上一个请求完成)
        val hobbies = webClient.get() 
            .uri("/users/{id}/hobbies", userId)
            .retrieve()
            .bodyToFlow<Hobby>()
            .toList()
        
        UserWithHobbies(user, hobbies)
    }
}

✅ 高效的并发方式

kotlin
@Service
class UserService(private val webClient: WebClient) {
    
    fun getUserWithHobbies(userId: Long): UserWithHobbies {
        return runBlocking {
            // 使用 async 实现并发请求
            val userDeferred = async { 
                webClient.get()
                    .uri("/users/{id}", userId)
                    .retrieve()
                    .awaitBody<User>()
            }
            
            val hobbiesDeferred = async { 
                webClient.get()
                    .uri("/users/{id}/hobbies", userId)
                    .retrieve()
                    .bodyToFlow<Hobby>()
                    .toList()
            }
            
            // 等待所有请求完成
            UserWithHobbies(
                user = userDeferred.await(), 
                hobbies = hobbiesDeferred.await() 
            )
        }
    }
}
java
@Service
public class UserService {
    
    public UserWithHobbies getUserWithHobbies(Long userId) {
        // 创建两个独立的 Mono
        Mono<User> userMono = webClient.get() 
            .uri("/users/{id}", userId)
            .retrieve()
            .bodyToMono(User.class);
        
        Mono<List<Hobby>> hobbiesMono = webClient.get() 
            .uri("/users/{id}/hobbies", userId)
            .retrieve()
            .bodyToFlux(Hobby.class)
            .collectList();
        
        // 使用 Mono.zip 并发执行并合并结果
        return Mono.zip(userMono, hobbiesMono, 
                (user, hobbies) -> new UserWithHobbies(user, hobbies))
            .block(); 
    }
}

实际业务场景示例

让我们看一个更贴近实际的电商场景:

kotlin
@Service
class OrderService(
    private val webClient: WebClient,
    private val logger: Logger = LoggerFactory.getLogger(OrderService::class.java)
) {
    
    /**
     * 获取订单详情(包含用户信息、商品信息、物流信息)
     * 演示高效的并发 HTTP 调用
     */
    fun getOrderDetails(orderId: Long): OrderDetails {
        return runBlocking {
            logger.info("开始获取订单详情: {}", orderId)
            
            // 并发发起多个 HTTP 请求
            val orderDeferred = async {
                logger.debug("请求订单基本信息...")
                webClient.get()
                    .uri("/orders/{id}", orderId)
                    .retrieve()
                    .awaitBody<Order>()
            }
            
            val userDeferred = async {
                logger.debug("请求用户信息...")
                webClient.get()
                    .uri("/users/{id}", orderId) // 假设从订单中获取用户ID
                    .retrieve()
                    .awaitBody<User>()
            }
            
            val productsDeferred = async {
                logger.debug("请求商品信息...")
                webClient.get()
                    .uri("/orders/{id}/products", orderId)
                    .retrieve()
                    .bodyToFlow<Product>()
                    .toList()
            }
            
            val shippingDeferred = async {
                logger.debug("请求物流信息...")
                webClient.get()
                    .uri("/orders/{id}/shipping", orderId)
                    .retrieve()
                    .awaitBody<ShippingInfo>()
            }
            
            // 等待所有请求完成并组装结果
            val order = orderDeferred.await()
            val user = userDeferred.await()
            val products = productsDeferred.await()
            val shipping = shippingDeferred.await()
            
            logger.info("订单详情获取完成: {}", orderId)
            
            OrderDetails(
                order = order,
                user = user,
                products = products,
                shipping = shipping
            )
        }
    }
}

// 数据类定义
data class OrderDetails(
    val order: Order,
    val user: User,
    val products: List<Product>,
    val shipping: ShippingInfo
)

性能对比分析

让我们用数据说话,看看并发请求的威力:

性能提升

通过并发请求,我们将总耗时从 650ms 降低到 300ms,性能提升了 54%

最佳实践与注意事项

1. 何时使用同步方式

WARNING

在 Spring WebFlux 控制器中,永远不要使用 .block()runBlocking

kotlin
@RestController
class UserController(private val userService: UserService) {
    
    // ❌ 错误:在 WebFlux 中阻塞
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: Long): User {
        return runBlocking { 
            userService.getUserAsync(id).await()
        }
    }
    
    // ✅ 正确:返回响应式类型
    @GetMapping("/users/{id}")
    suspend fun getUser(@PathVariable id: Long): User { 
        return userService.getUserAsync(id).await()
    }
    
    // 或者
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: Long): Mono<User> { 
        return userService.getUserReactive(id)
    }
}

2. 超时处理

kotlin
fun getUserWithTimeout(userId: Long): User? {
    return runBlocking {
        try {
            withTimeout(5000) { // 5秒超时
                webClient.get()
                    .uri("/users/{id}", userId)
                    .retrieve()
                    .awaitBody<User>()
            }
        } catch (e: TimeoutCancellationException) {
            logger.warn("获取用户信息超时: {}", userId)
            null
        }
    }
}

3. 错误处理

kotlin
fun getUserSafely(userId: Long): User? {
    return runBlocking {
        try {
            webClient.get()
                .uri("/users/{id}", userId)
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError) { 
                    Mono.error(UserNotFoundException("用户不存在: $userId"))
                }
                .onStatus(HttpStatus::is5xxServerError) { 
                    Mono.error(ServiceUnavailableException("服务暂时不可用"))
                }
                .awaitBody<User>()
        } catch (e: Exception) {
            logger.error("获取用户信息失败: {}", userId, e)
            null
        }
    }
}

总结

WebClient 的同步使用为我们提供了在传统同步代码中使用响应式 HTTP 客户端的能力。关键要点:

核心要点

  1. 单个请求:使用 runBlocking + awaitBody() (Kotlin) 或 .block() (Java)
  2. 多个请求:使用 async/await (Kotlin) 或 Mono.zip (Java) 实现并发
  3. 避免阻塞:在 WebFlux 控制器中返回响应式类型
  4. 错误处理:合理处理超时和异常情况

CAUTION

记住:同步使用 WebClient 会阻塞当前线程,在高并发场景下可能影响性能。优先考虑使用响应式编程模式!

通过合理使用这些技术,你可以在保持代码简洁的同时,获得出色的性能表现。🚀