一个接口两个实现,启动直接炸给你看
场景复刻:PayService 接口下面有 AliPayService 和 WechatPayService 两个实现,各自标了 @Service。这时在任何地方注入 PayService,启动瞬间就是 NoUniqueBeanDefinitionException——按类型找到了两个候选,Spring 不知道该注哪个。这不是 bug,是注入规则的必然结果。想把这类问题一网打尽,得先分清两个注入注解的身世。
byType 与 byName:底层分野
@Autowired 是 Spring 家的注解,默认按类型注入:先按类型找 Bean,唯一则直接注入;找到多个候选,就退而求其次把字段名当 Bean 名字去匹配;还匹配不上才抛异常。@Resource 是 JSR-250 标准注解(Boot 3 里在 jakarta.annotation 包下),默认按名称注入:先拿字段名当 Bean 名找,找不到再退回按类型。一句话记住:Autowired 先看类型,Resource 先看名字。
@Service
public class AliPayService implements PayService { }
@Service
public class WechatPayService implements PayService { }
// @Autowired:按类型两个候选 → 按字段名 aliPayService 匹配 → 命中
@Autowired
private PayService aliPayService;
// @Resource:先找名字叫 alipay 的 Bean → 没有 → 退回按类型 → 两个 → 炸
@Resource
private PayService alipay;多实现注入的三种正解
第一种 @Primary:标在其中一个实现类上,它成为同类型候选里的默认人选,其余场景照旧。第二种 @Qualifier:注入时点名,指名道姓不猜谜。第三种 @Resource(name):用 name 属性直接指定。三种都能解决,团队统一一种即可。
// 方案一:谁当默认,谁标 @Primary
@Primary
@Service
public class AliPayService implements PayService { }
// 方案二:@Autowired 配 @Qualifier 点名
@Autowired
@Qualifier("wechatPayService")
private PayService payService;
// 方案三:@Resource 指定 name
@Resource(name = "wechatPayService")
private PayService payService;字段、setter、构造器:官方为什么推构造器
字段注入最省事,代价也清楚:字段没法标 final;脱离容器就没法构造——单测里 new 出来的对象依赖全是 null,只能靠反射硬塞;类和容器焊死。构造器注入是 Spring 官方推荐:依赖可以 final 保证不可变;依赖摆在构造参数里一目了然,参数膨胀到七八个时,代码自己就在喊「职责太重」;单测直接 new 传 mock,零反射。setter 注入留给可选依赖。另外从 Spring 4.3 起,只有一个构造器的类连 @Autowired 注解都能省。
@Service
public class OrderService {
private final PayService payService; // final:构造后不可变
// Spring 4.3+ 单构造器自动注入,注解可省
public OrderService(PayService payService) {
this.payService = payService;
}
}别忘了,注进来的是代理
呼应 AOP 那两篇:有切面、有事务的 Bean,@Autowired 注进来的其实是代理对象。所以别用反射往私有字段里硬塞对象,也别绕开容器自己 new——让容器来管,代理才能站好岗。再送一个冷知识:@Autowired 有个 required = false 属性,容器里没有这个 Bean 也不报错;更优雅的玩法是 ObjectProvider,它下一篇循环依赖里还要出场。
下一篇拆注入的经典事故:循环依赖。Boot 2.6 为什么默认禁止?@Lazy 能救哪些,构造器注入为什么是无解的死结?
评论 (0)