Spring Webflux优雅关闭踩坑经验分享
时间:2026-06-15 15:44
SpringWebFlux在SpringBoot2 1 5与ReactorNetty0 8 8中优雅关闭失效,因disposeLater()仅调用subscribe()未阻塞,导致未完成请求被中断。通过反射获取LoopResources并注册关闭钩子,显式调用block()等待最多20秒,确保请求完成。
原始文章内容(含 HTML 与图片)已按 SEO 优化要求重写,仅修改纯文本部分,保持所有标签、属性、结构不变。
### 背景
近期在实际项目中体验 Spring WebFlux 的“优雅关闭”功能时,发现官方文档的描述与真实表现存在偏差。实验环境基于 Spring Boot 2.1.5 与 Reactor Netty 0.8.8,当服务器仍有未完成的请求(例如一个 sleep 10 秒的接口)时,直接返回了 `Empty reply` 错误,未能实现预期的平滑关闭。

### 根因与解决方案
跳过冗长的分析过程,直接给出结论:Netty 本身确实提供了 graceful shutdown 机制,并且在关闭时也的确被调用了。问题根源在于 Reactor Netty 的内部调用方式。其源码处理如下:
```ja va
//reactor.netty.resources.LoopResources#dispose
@Overide
default void dispose() {
//noop default
disposeLater().subscribe();
}
```
关键问题就在这里:`disposeLater()` 返回的是一个 `Publisher` 对象,后续直接通过 `subscribe()` 触发,并未调用 `block()` 等待其完成。因此当 Spring 的 shutdown 流程推进到后续阶段时,该异步操作被强制中断,导致未处理完的请求直接断开连接。
针对 WebFlux 体系还不算特别熟悉的情况下,暂时未找到优雅等待所有 `subscribe` 完成的标准方法,因此采用了一种相对“直接”的处理方式:在启动后通过反射获取内部的 `HttpResources`,并注册一个关闭钩子,在其中显式调用 `block()` 方法,最长阻塞等待 20 秒,从而确保剩余请求有机会正常返回。
```ja va
/**
* @author Lambda.J
* @version $Id: GracefulShutdown.ja va, v 0.1 2019-05-27
*/
@Component
public class GracefulShutdown {
@Autowired
ReactorResourceFactory reactorResourceFactory;
LoopResources loopResources;
// SpringBoot 2.1.5 reactor.netty.resources.LoopResources#dispose 只 subscribe 没有 block 造成没有等待关闭,
// 这边手工调用,后面如果修复了直接删除就好
@PostConstruct
void init() throws Exception {
Field field = TcpResources.class.getDeclaredField("defaultLoops");
field.setAccessible(true);
loopResources = (LoopResources) field.get(reactorResourceFactory.getLoopResources());
field.setAccessible(false);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Graceful: block long to 20s before real shutdown!");
loopResources.disposeLater().block(Duration.ofSeconds(20));
}));
}
}
```
### 相关知识
#### Spring 关闭流程
Spring 启动时会在 `org.springframework.context.support.AbstractApplicationContext#registerShutdownHook` 注册一个关闭钩子。

实际的关闭流程在 `org.springframework.context.support.AbstractApplicationContext#doClose` 中实现:

大致执行顺序如下:
1. 发布上下文关闭事件
2. 关闭 lifecycle beans
3. 关闭 beanFactory 生成的单例 beans(NettyServer 的关闭正是在这一步骤中发生)

4. 关闭 BeanFactory
5. 关闭 DisposableServer
6. 移除监听器,设置上下文状态为 inactive
测试中发现,在关闭 bean 的过程中,当 `reactorServerResourceFactory` 关闭(即 `org.springframework.http.client.reactive.ReactorResourceFactory#destroy`)后,端口实际上已经关闭,但尚未响应的请求仍然可以继续响应。因此还有一个较为 hack 的方式:通过类复写改写这里的关闭逻辑,在关闭 reactorServer 之后等待一段时间。不过这种做法过于“暴力”,除非万不得已不建议采用。

### 参考资料
- Netty 优雅退出机制和原理
- Spring Boot 2.1.5 源码
- Reactor Netty 0.8.8 源码
- Netty 4.1.36 源码