如何实现WebClient POST重试时动态修改请求头的逻辑
问题根因
你遇到的retry/retryWhen无法携带新token的问题,本质不是重试API不支持修改请求,而是请求组装逻辑把authToken的值提前固定死了:.header("authToken", authenticationRepository.getUserkey()) 这行在你调用send()方法组装Mono的瞬间就会执行取值,第一次调用取到null,后续不管重试多少次,请求头用的都是组装时拿到的固定值,不会重新读取repository里更新后的token。
实现方案
核心是延迟请求的构造时机,让每次实际发起请求(包括首次请求、重试请求)时才实时读取最新token,同时增加单飞机制避免并发场景下重复调用登录接口,不需要递归,也不需要手动在重试逻辑里修改请求内容。
完整实现代码:
// 缓存正在执行/有效的登录请求Mono,避免并发场景重复触发登录 private final AtomicReference<Mono<String>> cachedLoginMono = new AtomicReference<>(); private <T extends APIResponse<?>> Mono<T> send(RequestData postRequest, Class<T> responseType) { // 用Mono.defer包裹整个请求链,每次订阅(含重试)都会重新构造请求、读取最新token return Mono.defer(() -> webClient .post() .uri(url) // 这里的header取值会在每次发请求时实时执行,不会固定为初始值 .header("authToken", authenticationRepository.getUserkey()) .retrieve() .bodyToMono(responseType) // 直接在flatMap里做未授权校验,比doOnSuccess抛异常更符合响应式流语义 .flatMap(resp -> { if (isUnauthorized(resp)) { // 替换成你原来的未授权判断逻辑 return Mono.error(new RequestUnauthorized()); } return Mono.just(resp); }) ) // 最多重试1次,避免登录异常时无限循环 .onErrorResume(RequestUnauthorized.class, ex -> refreshToken().then(send(postRequest, responseType))) .checkpoint("api request with auth"); } // 单飞实现的token刷新:并发场景下多个请求同时触发401时,只会发起1次登录请求 private Mono<String> refreshToken() { return cachedLoginMono.updateAndGet(existingMono -> { if (existingMono != null) { return existingMono; } // 实际登录逻辑 return loginUsecase.login() .map(LoginResponse::getToken) .doOnNext(authenticationRepository::setUserkey) // 登录完成(成功/失败)后清空缓存,下次401可重新触发登录 .doOnTerminate(() -> cachedLoginMono.set(null)) // 缓存登录结果,给所有等待token的并发请求复用 .cache(); }).flatMap(Function.identity()); }
关键说明
Mono.defer()是整个实现的核心:原来的写法在调用send()时就把请求头的值固定了,加了defer之后,每次订阅这个Mono(第一次请求、重试、多调用方订阅)都会重新执行请求构造逻辑,实时读取repository里的最新token,不需要手动修改重试时的请求内容。- 单飞机制解决了并发场景的重复登录问题:如果同时有10个请求因为token过期返回未授权,不会发起10次登录请求,所有请求都会共享同一次登录的结果,等拿到新token后再各自重试,完全避免不必要的登录开销。
- 去掉了原来
doOnSuccess里抛异常的逻辑,改成在flatMap里校验响应后主动抛异常,符合响应式流的异常传播规则,不会出现上下文丢失的问题。 - 如果你偏好使用
retryWhen实现重试,逻辑完全一致,只要请求链被Mono.defer()包裹,重试时就会自动拿到最新token,不需要额外处理请求头。注意不要在retry的回调里调用block()方法,避免阻塞响应式线程。 - 建议给重试加最大次数限制(这里默认重试1次),避免登录接口异常时出现无限重试的死循环,重试后仍然未授权的话直接抛出异常交给上层业务处理即可。
补充优化:如果对接的API返回的token有明确的过期时间,可以在存储token时同步记录过期时间戳,在token即将过期时提前触发刷新,比等请求返回未授权再刷新的用户体验更好。
内容的提问来源于stack exchange,提问作者m.antkowicz
相关产品推荐
相关产品推荐

