Spring控制多个Bean加载的陷阱
直接看下结论
不要用“同名重载 @Bean 方法”来通过条件注解生成 bean 的加载。
如果你希望“按配置二选一加载”,请把方法名改成不同,再用 @Bean("同一个名字") 导出统一 Bean 名称,或直接用不同 Bean 名 + @Qualifier 注入。
1. 先看一个示例
下面这个例子看起来很合理,但会踩坑:
@Configuration
public class NotifyAutoConfiguration {
// 期望: notify.version=1 时加载这个
@Bean
@ConditionalOnProperty(name = "notify.version", havingValue = "1", matchIfMissing = true)
public NotifyClient notifyClient(SmtpService smtpService) {
return new EmailNotifyClient(smtpService);
}
// 期望: notify.version=2 时加载这个
@Bean
@ConditionalOnProperty(name = "notify.version", havingValue = "2")
public NotifyClient notifyClient(SmsGateway smsGateway, TemplateEngine templateEngine) {
return new SmsNotifyClient(smsGateway, templateEngine);
}
@Bean
public NotifyClient auditNotifyClient() {
return new AuditNotifyClient();
}
}
业务代码:
@RestController
public class NotifyController {
@Resource
private NotifyClient notifyClient;
}
很多人会以为:
notify.version=1时,加载第一个notifyClient。notify.version=2时,加载第二个notifyClient。
但真实运行中可能出现:
notifyClient这个目标 Bean 没按预期注册。@Resource按名字找不到后回退按类型。- 容器里
NotifyClient类型有多个(比如auditNotifyClient等)。 - 最终报
required a single bean, but X were found。
2. 为什么会这样
2.1 默认 Bean 名来自方法名
@Bean 不写 name/value 时,默认 beanName 就是方法名。
上面两个方法都叫 notifyClient,默认都指向同一个 beanName:notifyClient。
2.2 同名重载 @Bean 不是“两个独立 Bean 定义”
虽然 Java 语法里它们是重载方法,但在 Spring 配置解析里,它们会被视为同一 beanName 的“重载工厂方法”场景,而不是两个完全独立、互不影响的 Bean 定义。
这会导致一个关键问题:
- 条件注解不会稳定地按你期待的“两个方法独立评估后再二选一”来工作。
- 在重载场景里,条件判断与方法选择不是“配置驱动的显式分支”,而是“同名 Bean 定义解析过程中的重载决策”。
- 结果就是:你以为
havingValue="2"会命中第二个方法,但实际并不可靠,甚至可能没有得到期望的notifyClient。
3. 为什么错误会显示“找到了多个 Bean”
以 @Resource private NotifyClient notifyClient; 为例,典型链路是:
- 先按名字找 Bean:
notifyClient。 - 若按名字没找到,再按类型找
NotifyClient。 - 按类型发现多个候选(例如
auditNotifyClient、emailClient、smsClient...)。 - 无法决定注入哪个,抛出“需要单个 Bean,但找到了多个”。
所以日志里看到“4 个、5 个同类型 Bean 冲突”,本质上常常是前一步“按名字目标 Bean 丢失”导致的连锁反应。
4. 正确写法(推荐)
核心思路:
- 方法名不要重载(改成不同方法名)。
- 条件注解各自独立判断。
- 如果业务层要统一注入名,可以显式导出同一个 beanName。
@Configuration
public class NotifyAutoConfiguration {
@Bean("notifyClient")
@ConditionalOnProperty(name = "notify.version", havingValue = "1", matchIfMissing = true)
public NotifyClient notifyClientV1(SmtpService smtpService) {
return new EmailNotifyClient(smtpService);
}
@Bean("notifyClient")
@ConditionalOnProperty(name = "notify.version", havingValue = "2")
public NotifyClient notifyClientV2(SmsGateway smsGateway, TemplateEngine templateEngine) {
return new SmsNotifyClient(smsGateway, templateEngine);
}
}
注入方:
@RestController
public class NotifyController {
@Resource
private NotifyClient notifyClient;
}
这个写法稳定的原因:
notifyClientV1和notifyClientV2是两个独立@Bean定义。- 条件分别生效,且
havingValue=1/2互斥。 - 最终只会有一个名为
notifyClient的 Bean 存在。
5. 另一种可维护写法(也推荐)
如果不需要统一 Bean 名,也可以让每个 Bean 保持独立名字,然后在注入点明确指定:
@Bean
@ConditionalOnProperty(name = "notify.version", havingValue = "1", matchIfMissing = true)
public NotifyClient emailNotifyClient(...) { ... }
@Bean
@ConditionalOnProperty(name = "notify.version", havingValue = "2")
public NotifyClient smsNotifyClient(...) { ... }
注入时:
@Resource(name = "smsNotifyClient")
private NotifyClient notifyClient;
或:
@Autowired
@Qualifier("smsNotifyClient")
private NotifyClient notifyClient;
6. 常见误区
误区 1
“条件注解失效了。”
更准确是:
在同名重载 @Bean 场景下,你不能把条件注解当作“方法级 switch/case”来可靠分流。
误区 2
“既然最终 Bean 名相同,为啥方法名不同就可以?”
因为关键冲突点不是“最终 Bean 名相同”,而是“是否处于同名方法重载解析模式”。
方法名不同后,Spring 会把它们当作两个独立 Bean 定义,再由条件决定哪个被注册。
误区 3
“加 @Primary 就彻底解决了。”
@Primary 只能解决“按类型注入时多候选冲突”。
它不能修复“你期望的目标 Bean 没被正确注册”这个根因。
所以它更像兜底,不是根本方案。
7. 实用的排查步骤
- 看是否存在同名重载
@Bean方法。 - 看
@Bean是否显式指定了 name/value。 - 看注入点是否使用了
@Resource(name=...)或@Qualifier。 - 打开启动日志,确认目标 Bean 名是否真的注册成功。
- 如果报“found X beans”,优先回溯“目标命名 Bean 是否缺失”。
8. 对于这种多个 bean 加载的设计建议
- 禁止用同名重载
@Bean+ 条件注解做版本切换。 - 条件分流统一采用“不同方法名 + 显式 Bean 名”。
- 所有关键依赖(如
ChatClient、DataSource、ObjectMapper)在注入点明确名称/限定符,不依赖隐式回退。 - 在配置模块增加一个小型启动测试,分别验证
version=1、version=2时的 Bean 存在性。