@Autowired 自动装配与冲突

今天在写代码的时候遇到这个情况

以前也经常这么写,在需要注入的属性上面直接添加 @Autowired 注解,但是今天注意到 @Autowired 是有警告的:Field Injection is not recommended,来看一下 IDEA 给的建议:Create Constructor: AuthProvider(UserService userService),原来是不建议使用属性注入的方式,而是建议使用 构造器注入,那么它们之间有什么区别呢?

@Autowired

我们先来看一下熟悉又陌生的 @Autowired 注解,可以说它是我们使用 Spring 框架的控制反转、依赖注入特性中用的最多的注解,@Autowired 是 Spring 官方提供的注解,在需要注入的地方添加这个注解后,Spring 就会自动为我们装配这个 Bean,这里我们首先要知道,@Autowired 是根据 类型 注入的。举个栗子

声明一个接口如下

1
2
3
public interface Flyable {
void fly();
}

再添加两个实现类,并都将其使用 @Component 注解声明为 Bean

1
2
3
4
5
6
7
@Component
public class A implements Flyable {
@Override
public void fly() {
System.out.println("i can fly, my name is A");
}
}
1
2
3
4
5
6
7
@Component
public class B implements Flyable {
@Override
public void fly() {
System.out.println("i can fly, my name is B");
}
}

这时候我们要根据接口注入实现类,像这样

1
2
3
4
5
@Component
public class Test {
@Autowired
private Flyable xxx;
}

到这里其实大家应该可以想到是有歧义的,因为同时有两个符合条件的 Bean 分别是 A 和 B,结果是

1
Field xxx in lab.zlren.xunwu.base.test.Test required a single bean, but 2 were found:

这里也验证了 @Autowired 是根据类型注入的,否则不会报歧义而应该是找不到

如何解决歧义呢?最简单的方法就是在我们需要的 Bean 上加入 @Primary 注解,这样对应的 Bean 就在同类型的 Bean 中有了更高的优先级,产生歧义的时候由于存在一个优先级更高的 Bean 那么它就会默认被注入

1
2
3
4
5
6
7
8
@Component
@Primary
public class A implements Flyable {
@Override
public void fly() {
System.out.println("i can fly, my name is A");
}
}

可以想到如果在 B 上也加入 @Primary 则又会报歧义的错误。。

有没有其他的方法呢?在 Java Config 之前,也就是 xml 配置的时代,有一个属性叫做 beanId,那么现在 beanId 是什么呢?实际上声明为 Bean 的 类名首字母小写 就是这个 Bean 对应的 beanId,beanId 是唯一的,因此通过它注入是不会有歧义的,这里可以有两种方式通过 beanId 去注入

1 属性名称,这也是比较简单的方式,比如要注入 A,我们可以这样做(首先去掉 A 上面的 @Primary 注解恢复之前的歧义状态)

1
2
3
4
5
@Component
public class Test {
@Autowired
private Flyable a;
}

由于 A 的 beanId 是首字母小写也就是 『a』,所以这里的属性(变量)名直接就是 beanId 就不会有歧义。但是这样一来 A 这个 Bean 如果更改了类名就又会造成注入失败,那么能不能手动指定 beanId 呢?

2 @Qualifier 手动指定,我们也可以通过它手动指定 beanId 来消除歧义

1
2
3
4
5
6
@Component
public class Test {
@Autowired
@Qualifier("a")
private Flyable xxx;
}

上面提到 Bean 的默认 beanId 是首字母小写,其实这里也可以手动指定,同样也是通过 @Qualifier 注解,这样再配合 @Autowired 处的 @Qualifier 就可以实现无歧义注入

三种注入方式

@Autowired 的 3 种使用方法:

  • 属性(Field)注入
  • setter 方法(或者其他方法)注入
  • 构造器注入

属性注入是最简单、最简洁的注入方式,一般情况下很多同学都这么做,但它无法注入 final 对象,与此同时它和所依赖的容器是强耦合的,无法在容器外使用,无法绕过反射进行实例化,更像是集成测试。由于没有写在构造器参数列表中,实际的依赖被隐藏在外面,使用 Spring 外的容器可能会引起空指针异常

使用 setter 方法注入的属性无法被设置为 final,上面提到的 或其他方法 表明不只是通过 setter 方法,实际上 setter 方法注入的背后参数列表,因此完全可以将 @Autowired 写在一个普通方法上,它会根据参数列表进行 Bean 装配

第三种就是构造器注入,这是最推荐的方式,但就是如果需要注入的依赖比较多的话。。参数列表可能比较长。。但是这也从某种程度上也提醒我们这个类需要依赖这么多东西,是不是干的活太多了?有没有违背单一职责的原则?

推荐

Spring 3 建议 setter 注入,Spring 4 建议构造器注入

结论:如果是 final 类型的就使用构造器注入,否则可以使用 setter 方法注入

至于属性注入,还是不要使用的好