
这个问题,几乎所有做过一段时间 Spring Boot 的人都遇到过。
代码能启动,@Autowired 不报错,但运行时的行为就是不对——日志不对、实现不对、配置没生效,甚至感觉像换了一个类在跑。最难受的是:你去debug,它又“看起来”是对的。
这篇文章就专门讲一件事:Bean注入成功,但注入的并不是你以为的那个,到底是怎么发生的?

1 最常见的现象
先看一个很典型的场景。
@Service
public class OrderService {
@Autowired
private PayService payService;
public void pay() {
payService.pay();
}
}
你非常确定 PayService 只有一个实现,而且你也在实现类里打了日志。
但线上日志却告诉你:走的不是你以为的那套逻辑。
这种时候,大概率不是你看错代码,而是——Spring帮你选了另一个Bean。
2 场景一:同接口多个实现,Spring默认选了“另一个”
这是最常见、也最隐蔽的一种。
public interface PayService {
void pay();
}
@Service
public class AliPayService implements PayService { }
@Service
public class WxPayService implements PayService { }
此时再注入:
@Autowired
private PayService payService;
如果你没加任何限定条件,结果只有两种:
- 启动直接报错(运气好)
- 被某个实现“悄悄选中”(运气差)
在某些组合条件下(比如某个被标了 @Primary), Spring 会非常“贴心”地替你做决定。
解决方式(最直接、最稳)
@Autowired
@Qualifier("aliPayService")
private PayService payService;
或者在实现类上明确主实现:
@Primary
@Service
public class AliPayService implements PayService { }
3 场景二:你以为是单例,其实拿到了“新对象”
这个坑,通常和 配置类 / 工厂方法 有关。
@Component
public class PayConfig {
@Bean
public PayService payService() {
return new AliPayService();
}
}
再注入:
@Autowired
private PayService payService;
看起来一切正常。
但如果你在别的地方:
@Autowired
private PayConfig payConfig;
public void test() {
PayService p1 = payConfig.payService();
PayService p2 = payConfig.payService();
}
你以为拿的是同一个Bean,实际上,你可能拿到了 两个不同对象。
原因只有一个:配置类被当成普通组件使用了。
正确写法
@Configuration
public class PayConfig {
@Bean
public PayService payService() {
return new AliPayService();
}
}
@Configuration 会确保:
4 场景三:条件装配导致Bean被“悄悄替换”
这是线上最难排查的一类。
@Bean
@ConditionalOnProperty(name = "pay.type", havingValue = "ali")
public PayService aliPayService() { }
@Bean
@ConditionalOnProperty(name = "pay.type", havingValue = "wx")
public PayService wxPayService() { }
如果你在本地:
pay:
type: ali
但线上配置变成了:
pay:
type: wx
结果就是:
这类问题只看代码是永远看不出来的。
实际排查方式
- 打启动日志,看哪些Bean被加载
- 明确哪些是条件装配
- 不要假设“线上配置和本地一样”
5 场景四:包扫描范围不一致
@SpringBootApplication
@ComponentScan("com.example")
而你的实现类在:
com.demo.pay.impl
此时可能出现两种情况:
- 你以为的Bean根本没被扫描
- Spring找到了“另一个能用的Bean”
然后你就开始怀疑人生。
解决方式
- 确认启动类扫描路径
- 不要过度拆包后忘记扫描范围
- 尽量保持 启动类在最顶层包
6 场景五:测试环境和正式环境不一致
很多人都踩过这个坑。
@Profile("test")
@Service
public class MockPayService implements PayService { }
@Profile("prod")
@Service
public class AliPayService implements PayService { }
如果你没注意当前激活的profile,你看到的注入结果,可能完全不是你写的那个。
7 如何快速确认“我到底注入了谁”
这是一个非常实用的排查技巧。
@PostConstruct
public void init() {
log.info("PayService 实现类:{}", payService.getClass().getName());
}
比你盯着IDEA看半天更有效。
因为Spring的设计目标是:尽量让系统跑起来,而不是尽量让你意识到选错了。
只要满足条件:
Spring就会帮你兜底。但业务正确与否,它不负责。
写在最后
Bean注入成功,只能说明一件事:容器里有对象。 但并不保证:
- 是你期望的实现
- 是你理解中的生命周期
- 是你测试过的那一套逻辑
当你发现“行为不对但代码没问题”时,第一反应不该是怀疑业务逻辑,而是先问一句:我现在用的,到底是哪一个Bean?
理解这些场景和排查方法,是掌握Spring依赖注入原理的关键一步。想了解更多类似的实践技巧和深度解析,可以关注云栈社区的技术专栏。
