三种依赖注入方式
在 Spring boot 中 Bean 的依赖注入通常会使用 @Autowired
注解,该注解用于成员变量 、setter()
方法以及构造方法上,显式地声明依赖。
在最新的文档中依赖注入方式有两大类:
- 基于构造方法的依赖注入(推荐使用)
- 基于
setter()
方法的依赖注入
但是通常认为还有一种是基于 成员变量 的依赖注入(spring framerwork 4.0 后不推荐使用)
基于构造方法的依赖注入
用法如下
@Service
public class A {
private B b;
@Autowired // 这里的 @Autowired 可以加也可以不加
public A(B b) {
this.b = b;
}
}
基于构造方法的依赖注入的缺点
无法解决循环依赖,例如
@Service
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
@Service
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
A 依赖 B,B 又依赖 A,这种相互依赖无法通过构造函数实例话,这也是基于构造方法的依赖注入最大的缺点,如果涉及到循环依赖,只能使用 基于 setter()
方法的依赖注入和基于成员变量的依赖注入来解决
基于 setter()
方法的依赖注入
用法如下
@Service
public class A {
private B b;
@Autowired // 也可以换成 @Resource
public void setB(B b) {
this.b = b;
}
}
基于成员变量的依赖注入
基于 成员变量 的依赖注入,也有人称为基于 属性 的依赖注入或基于 字段 的依赖注入,它是通过反射机制进行依赖注入的
用法如下
@Service
public class A {
@Autowired // 也可以换成 @Resource
private B b;
}
使用这种方式的依赖注入,idea 会在 @Autowired
处有一个黄色的波浪线提示 "Field injection is not recommended"(不推荐使用字段注入)
至于为什么不推荐字段注入,原因如下
基于成员变量的依赖注入的缺点
无法注入 final 修饰的变量
final 修饰的变量,在实例化后就不可变,因此只能使用基于构造函数的依赖注入的方式,这个是基于成员变量的依赖注入和基于 setter()
方法的依赖注入做不到
掩盖了单一职责的设计思想
在 OOP 的设计当中有一个单一职责思想,如果你采用的是基于构造函数的依赖注入的方式来使用 Spring 的 IOC 的时候,当你注入的太多依赖的时候,这个构造方法的参数就会很庞大,例如
@Service
public class A {
private B b;
private C c;
private D d;
private E e;
private F f;
public A(B b, C c, D d, E e, F f) {
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
}
}
当你看到这个类的构造方法那么多参数的时候,你自然而然的会想到 这个类是不是违反了单一职责思想?
但是使用基于成员变量的依赖注入不会让你察觉,你会很沉浸在 @Autowire
当中
与 Spring 的 IOC 机制紧密耦合
使用基于字段的依赖注入方式的时候,确实可以省略构造方法和 setter()
方法,但是,你把控制权全给 Spring 的 IOC 了,别的类想重新设置下某个属性,都没法处理 (当然反射可以做到),你的 Bean 脱离 Spring IOC 都很难使用,想换框架时,可移植性极差
而本身 Spring 的目的本来就是解藕和依赖反转,结果通过再次与类注入器耦合,失去了通过自动装配类字段而实现的对类的解耦,从而使类在 Spring 容器之外无效
隐藏依赖性
当你使用 Spring 的 IOC 的时候,被注入的类应当使用一些 public
类型,如构造方法、setter()
方法,来向外界表达:我需要什么依赖。
但是基于字段的依赖注入的方式,基本都是 private 形式的,private 把属性都给封印到 class 当中了
三种依赖注入方式使用总结
通过上面,我们可以看到,基于成员变量的依赖注入方式有很多缺点,我们应当避免使用
推荐使用基于构造函数和基于 setter 的依赖注入。
- 对于必需的依赖项,建议使用基于构造函数的依赖注入
- 对于可选的依赖项,建议使用基于
setter()
方法的依赖注入 - 对于需要解决循环依赖时,建议使用基于
setter()
方法的依赖注入
两种依赖查找方式
依赖注入是一个过程,查找依赖是其中的一个步骤,
查找依赖有两种策略
- byType :按照 Bean 的类型查找,
@Autowired
注解使用的是该策略 - byName : 按照 Bean 的名称查找,
@Resource
注解使用的是该策略
byName
在 Spring 中,按照 Bean 的名称查找依赖,最终会调用
Object bean = BeanFactory.getBean("beanName")
拿到依赖对象
byType
在 Spring 中,@Autowired
注解是按照类型查找(注入)依赖,例如
@Service
public class A {
@Autowired
private B b;
}
在为 A 注入依赖 B 时,会调用
String[] beanNames = ListableBeanFactory.getBeanNamesForType(B.class)
拿到所有类型为 B.class
的 Bean
的名称,然后调用
Object bean = BeanFactory.getBean("beanName")
拿到依赖对象
也就是说,当类型为 B.class
的 Bean
只有一个时,那么使用 @Autowired
没有问题,但如果类型为 B.class
的 Bean
有多个时,A 就不知道该注入哪一个 B 了
例如
@Controller
public class AController {
private BService b;
@Autowired
public AController(BService b) {
this.b = b;
}
}
interface BService {}
@Service
class BServiceImpl1 implements BService {}
@Service
class BServiceImpl2 implements BService {}
BService 有两个实现类 BServiceImpl1 和 BServiceImpl2,AController 在使用 @Autowired
进行依赖注入时,如果不指明使用哪个实现类就会报错
怎么解决?IDEA 已经提示你了,添加 @Qualifier
注解,指明具体要使用的 bean 的名称
例如
@Controller
public class AController {
private BService b;
public void setB(@Qualifier("BServiceImpl1") BService b) {
this.b = b;
}
}
或者使用 @Resource
注解指明要使用的 bean 的名称
@Controller
public class AController {
private BService b;
@Resource(name = "BServiceImpl1")
public void setB(BService b) {
this.b = b;
}
}