Spring Boot 启动失败:BeanNotOfRequiredTypeException+nacosGracefulShutdownDelegate连环报错分析
一、错误现象
应用启动时直接失败,控制台打印大量异常堆栈,主要包括两类错误:
1. 核心错误(导致启动失败)
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'assessmentIndicatorSyncJob': Injection of resource dependencies failed Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'assessmentIndicatorCalcServiceImpl': Injection of resource dependencies failed Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'tenantFrameworkService' is expected to be of type '...TenantCommonApi' but was actually of type '...TenantFrameworkServiceImpl'2. 次生错误(Context 关闭时反复出现)
org.springframework.beans.factory.BeanCreationNotAllowedException: Error creating bean with name 'nacosGracefulShutdownDelegate': Singleton bean creation not allowed while singletons of this factory are in destruction该错误重复出现5~6 次,随后应用退出。
二、错误根因分析
2.1 定位问题代码
@ServicepublicclassAssessmentIndicatorCalcServiceImplimplementsAssessmentIndicatorCalcService{@ResourceprivateTenantCommonApitenantFrameworkService;// ← 问题在这里!// ...}2.2@Resource注入机制
@Resource的查找顺序是:
- 先按 bean name(字段名)匹配
- 匹配不到再按类型匹配
因为字段名叫tenantFrameworkService,Spring 就去容器中找名为tenantFrameworkService的 Bean。
2.3 容器中实际存在什么?
容器中确实有一个 Bean 叫tenantFrameworkService,但它来自yudao-spring-boot-starter-biz-tenant模块:
// TenantFrameworkServiceImpl.java@RequiredArgsConstructorpublicclassTenantFrameworkServiceImplimplementsTenantFrameworkService{// 它实现的是 TenantFrameworkService 接口privatefinalTenantCommonApitenantApi;// 内部包装了 TenantCommonApi}而代码中声明的类型是:
// TenantCommonApi.java — 这是一个 Feign 客户端接口@FeignClient(name=RpcConstants.SYSTEM_NAME)publicinterfaceTenantCommonApi{// RPC 远程调用接口CommonResult<TenantRespDTO>getTenant(@RequestParam("id")Longid);}2.4 类型关系
| 类/接口 | 类型 | 说明 |
|---|---|---|
TenantCommonApi | Feign 客户端接口 | RPC 远程调用,通过 HTTP 调用 system-server |
TenantFrameworkService | 本地服务接口 | 定义本地租户服务方法 |
TenantFrameworkServiceImpl | 本地服务实现类 | 实现TenantFrameworkService,内部包装了TenantCommonApi并加了 Guava 缓存 |
TenantFrameworkServiceImpl不实现TenantCommonApi!它们是两个完全不同的类型体系。
2.5 结论
| 字段名 | 声明的类型 | 容器中实际匹配到的 Bean | 结果 |
|---|---|---|---|
tenantFrameworkService | TenantCommonApi | TenantFrameworkServiceImpl(类型:TenantFrameworkService) | ❌BeanNotOfRequiredTypeException |
三、Nacos 报错为什么出现?
3.1 事件链
① BeanNotOfRequiredTypeException (tenantFrameworkService 类型不匹配) ↓ ② ApplicationContext.refresh() 失败 → 调用 doClose() 清理 ↓ ③ doClose() 发布 ContextClosedEvent ↓ ④ Nacos 的 nacosGracefulShutdownDelegate 监听器收到事件 ↓ ⑤ 该监听器尝试 getBean() → 但 BeanFactory 已在销毁状态 ↓ ⑥ BeanCreationNotAllowedException: "Singleton bean creation not allowed..."3.2 为什么重复 5~6 次?
看堆栈中间:
NamedContextFactory.destroy() ← Spring Cloud LoadBalancer 的子容器销毁 → AbstractApplicationContext.close() → doClose() 发布 ContextClosedEvent → nacosGracefulShutdownDelegate 再次尝试 getBean()NamedContextFactory(Spring Cloud LoadBalancer)会为每个服务创建独立的子 ApplicationContext。销毁时,每个子 Context关闭都会发布ContextClosedEvent,Nacos 监听器又注册在每个子 Context 中,所以触发多次。
3.3 错误性质
| 错误 | 性质 | 原因 |
|---|---|---|
BeanNotOfRequiredTypeException | 根因 | @Resource字段名导致按名称匹配到错误的 Bean |
BeanCreationNotAllowedException | 次生 | 启动失败后关闭 Context 时,Nacos 在销毁阶段试图 getBean |
| Nacos 线程池/HttpClient 销毁日志 | 正常清理 | 容器销毁时的正常善后动作 |
四、解决方案
4.1 修复方法
将字段名改为不与任何 Bean 名冲突,让@Resource退化为按类型匹配:
// 修改前@ResourceprivateTenantCommonApitenantFrameworkService;// ❌ 按名称匹配到 TenantFrameworkServiceImpl// 修改后@ResourceprivateTenantCommonApitenantCommonApi;// ✅ 按类型匹配到 TenantCommonApi 的 Feign Bean同时删除无用的 import:
// 删除这行(从未使用)importcn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;4.2 修复后的调用
// 使用处同步修改CommonResult<TenantRespDTO>commonResult=tenantCommonApi.getTenant(TenantContextHolder.getTenantId());4.3 原理说明
字段名改为tenantCommonApi后,Spring 找不到名为tenantCommonApi的 Bean,于是退回到按类型匹配,正确匹配到 Feign 框架自动创建的TenantCommonApi代理 Bean,注入成功。
五、教训与最佳实践
5.1@Resourcevs@Autowired
| 注解 | 默认匹配方式 | 注意事项 |
|---|---|---|
@Resource | 先按 bean name,再按 type | 字段名会影响注入结果 |
@Autowired | 先按 type,再按 name | 字段名一般不影响注入(除非有多个同类型 Bean) |
5.2 字段命名建议
- 字段名不要与其他已知的 Bean 名相同,否则
@Resource会按名称匹配 - 字段名应该反映声明的类型,而不是"想要的功能"
- 例如:
TenantCommonApi类型的字段就叫tenantCommonApi,而不是tenantFrameworkService
5.3 遇到类似错误时的排查思路
- 找到最底层的
Caused by(而非最上面的报错) - 看
BeanNotOfRequiredTypeException中的三个关键信息:- Bean 名:
'tenantFrameworkService' - 期望类型:
TenantCommonApi - 实际类型:
TenantFrameworkServiceImpl
- Bean 名:
- 检查字段声明是否与 Bean 名冲突
- 上层的 Nacos 报错通常是次生灾害,忽略即可
六、参考
- Spring @Resource Annotation
- Spring Cloud Nacos Graceful Shutdown
- DefaultSingletonBeanRegistry 源码