WebClient优雅停机遇Connection prematurely closed BEFORE response问题
问题根源
应用收到停机信号后,默认的ReactorResourceFactory会优先于业务Bean销毁,导致Netty连接池提前关闭,此时业务中尚未完成的WebClient请求就会触发PrematureCloseException——你的业务Bean的destroyMethod还没来得及执行,连接池就已经被关闭了。
解决方案
1. 自定义ReactorResourceFactory并控制生命周期
创建独立的ReactorResourceFactory Bean,禁用全局资源,这样可以精准控制它的销毁顺序:
@Bean public ReactorResourceFactory reactorResourceFactory() { ReactorResourceFactory factory = new ReactorResourceFactory(); factory.setUseGlobalResources(false); // 禁用全局资源,由当前Bean管理连接池和事件循环 return factory; }
2. 让WebClient绑定自定义资源工厂
修改WebClient的Bean定义,将自定义的ReactorResourceFactory注入,确保所有WebClient请求使用该工厂管理的资源:
@Bean(name = "webClient") public WebClient webClient(WebClient.Builder builder, ReactorResourceFactory resourceFactory) { HttpClient httpClient = HttpClient.create() .responseTimeout(Duration.ofSeconds(5)) .runOn(resourceFactory.getLoopResources()); // 绑定到自定义资源工厂的事件循环 ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); return builder.clientConnector(connector).build(); }
3. 控制业务Bean与资源工厂的销毁顺序
通过依赖关系确保业务Bean先销毁:让业务Bean通过构造函数注入WebClient(间接依赖自定义的ReactorResourceFactory),Spring会遵循“初始化顺序逆序销毁”的规则,保证业务Bean先完成销毁逻辑,再销毁ReactorResourceFactory。如果是通过@Bean定义业务类,可添加@DependsOn("webClient")明确依赖关系。
4. 在业务Bean中等待所有请求完成
在业务Bean的销毁方法中,收集并等待所有未完成的WebClient请求执行完毕:
@Component public class BusinessService { private final WebClient webClient; private final CopyOnWriteArrayList<Mono<?>> pendingRequests = new CopyOnWriteArrayList<>(); public BusinessService(@Qualifier("webClient") WebClient webClient) { this.webClient = webClient; } public Mono<Void> executeHttpRequest() { Mono<Void> requestMono = webClient.put() .uri("http://xxx") .retrieve() .bodyToMono(Void.class) .doFinally(signal -> pendingRequests.remove(requestMono)); pendingRequests.add(requestMono); return requestMono; } @PreDestroy public void gracefulShutdown() { if (!pendingRequests.isEmpty()) { // 等待所有请求完成,设置超时时间避免无限阻塞 Mono.when(pendingRequests).block(Duration.ofSeconds(30)); } } }
5. 配置全局停机超时
在application.yml中设置Spring Boot的停机超时时间,给足够时间处理剩余请求:
spring: lifecycle: timeout-per-shutdown-phase: 30s
验证效果
触发停机时,业务Bean的@PreDestroy方法会优先执行,等待所有pending的WebClient请求完成后,ReactorResourceFactory才会销毁Netty连接池,从而彻底避免PrematureCloseException。
内容的提问来源于stack exchange,提问作者user143715

