Spring Boot WebClient:基于响应体将200状态判定为错误的处理
解决Spring WebClient声明式客户端的非常规API错误处理问题
核心问题拆解
你遇到的两个关键痛点:
- HTTP响应体是一次性订阅的Flux,在
defaultStatusHandler中读取后,后续调用者无法再获取内容 - 第三方API的错误逻辑不符合REST规范:401(密钥不存在)需抛出中断,200但
successful=false(无资源权限)也需抛出异常
最优解决方案:全局ExchangeFilterFunction(支持多项目复用)
自定义全局过滤器统一处理所有请求的响应,既解决响应体重复读取问题,又能复用错误逻辑,适合多项目场景。
步骤1:定义统一响应体DTO
先定义匹配第三方API响应的通用DTO(示例为ApiResponse<T>):
public class ApiResponse<T> { private boolean successful; private T data; private String errorMessage; // getter、setter、构造器省略 }
步骤2:实现全局ExchangeFilterFunction
通过exchangeToMono缓存响应体,避免重复读取问题,同时处理两种错误场景:
import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import reactor.core.publisher.Mono; public class ApiErrorHandlingFilter implements ExchangeFilterFunction { @Override public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { return next.exchange(request) .flatMap(clientResponse -> { // 缓存响应体,支持多次订阅 return clientResponse.bodyToMono(ApiResponse.class) .cache() .flatMap(apiResponse -> { // 处理401状态码:抛出未授权异常 if (clientResponse.statusCode().value() == 401) { return Mono.error(new UnauthorizedException("API密钥不存在或无效")); } // 处理200但successful=false的情况:抛出权限不足异常 if (clientResponse.statusCode().is2xxSuccessful() && !apiResponse.isSuccessful()) { return Mono.error(new AccessDeniedException(apiResponse.getErrorMessage())); } // 正常情况:重新包装ClientResponse,传递缓存的响应体 return Mono.just(clientResponse.mutate() .body(Mono.just(apiResponse)) .build()); }); }); } }
步骤3:配置WebClient并绑定声明式客户端
将过滤器添加到WebClient.Builder,声明式客户端使用该Builder:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.support.WebClientAdapter; import org.springframework.web.service.invoker.HttpServiceProxyFactory; @Configuration public class WebClientConfig { @Bean public WebClient apiWebClient() { return WebClient.builder() .baseUrl("第三方API基础地址") .filter(new ApiErrorHandlingFilter()) .build(); } @Bean public YourDeclarativeApiClient declarativeApiClient(WebClient apiWebClient) { HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(apiWebClient)).build(); return factory.createClient(YourDeclarativeApiClient.class); } }
步骤4:声明式客户端接口示例
import org.springframework.web.service.annotation.GetExchange; import reactor.core.publisher.Mono; public interface YourDeclarativeApiClient { @GetExchange("/resource/{id}") Mono<ApiResponse<ResourceDTO>> getResource(@PathVariable("id") String id); }
关键细节说明
- 响应体缓存:使用
.cache()操作符缓存响应体Mono,确保后续订阅(比如业务代码读取响应)能拿到数据 - 全局复用:将
ApiErrorHandlingFilter和WebClientConfig封装到公共组件Jar中,多项目直接引入即可复用错误逻辑 - 异常类型:自定义
UnauthorizedException和AccessDeniedException,方便业务层统一捕获处理
替代方案:接口级别局部处理(适合特殊接口)
如果仅需针对个别接口处理,可在声明式方法中直接处理响应:
@GetExchange("/resource/{id}") default Mono<ResourceDTO> getResourceWithErrorHandling(@PathVariable("id") String id) { return getResource(id) .flatMap(response -> { if (!response.isSuccessful()) { return Mono.error(new AccessDeniedException(response.getErrorMessage())); } return Mono.just(response.getData()); }); }
内容的提问来源于stack exchange,提问作者Nándor Holozsnyák
相关产品推荐
相关产品推荐

