跳到主要内容

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;
}

很多人会以为:

  1. notify.version=1 时,加载第一个 notifyClient
  2. notify.version=2 时,加载第二个 notifyClient

但真实运行中可能出现:

  1. notifyClient 这个目标 Bean 没按预期注册。
  2. @Resource 按名字找不到后回退按类型。
  3. 容器里 NotifyClient 类型有多个(比如 auditNotifyClient 等)。
  4. 最终报 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 定义。

这会导致一个关键问题:

  1. 条件注解不会稳定地按你期待的“两个方法独立评估后再二选一”来工作。
  2. 在重载场景里,条件判断与方法选择不是“配置驱动的显式分支”,而是“同名 Bean 定义解析过程中的重载决策”。
  3. 结果就是:你以为 havingValue="2" 会命中第二个方法,但实际并不可靠,甚至可能没有得到期望的 notifyClient

3. 为什么错误会显示“找到了多个 Bean”

@Resource private NotifyClient notifyClient; 为例,典型链路是:

  1. 先按名字找 Bean:notifyClient
  2. 若按名字没找到,再按类型找 NotifyClient
  3. 按类型发现多个候选(例如 auditNotifyClientemailClientsmsClient...)。
  4. 无法决定注入哪个,抛出“需要单个 Bean,但找到了多个”。

所以日志里看到“4 个、5 个同类型 Bean 冲突”,本质上常常是前一步“按名字目标 Bean 丢失”导致的连锁反应。

4. 正确写法(推荐)

核心思路:

  1. 方法名不要重载(改成不同方法名)。
  2. 条件注解各自独立判断。
  3. 如果业务层要统一注入名,可以显式导出同一个 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;
}

这个写法稳定的原因:

  1. notifyClientV1notifyClientV2 是两个独立 @Bean 定义。
  2. 条件分别生效,且 havingValue=1/2 互斥。
  3. 最终只会有一个名为 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. 实用的排查步骤

  1. 看是否存在同名重载 @Bean 方法。
  2. @Bean 是否显式指定了 name/value。
  3. 看注入点是否使用了 @Resource(name=...)@Qualifier
  4. 打开启动日志,确认目标 Bean 名是否真的注册成功。
  5. 如果报“found X beans”,优先回溯“目标命名 Bean 是否缺失”。

8. 对于这种多个 bean 加载的设计建议

  1. 禁止用同名重载 @Bean + 条件注解做版本切换。
  2. 条件分流统一采用“不同方法名 + 显式 Bean 名”。
  3. 所有关键依赖(如 ChatClientDataSourceObjectMapper)在注入点明确名称/限定符,不依赖隐式回退。
  4. 在配置模块增加一个小型启动测试,分别验证 version=1version=2 时的 Bean 存在性。
🎁优惠