控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
上层建筑依赖下层建筑
—每一个类的构造函数都直接调用了底层代码的构造函数
Car car = new Car();
System.out.println(car.getDiPan().getLunZi().getSize());
public class Car {
private DiPan diPan;
Car(){
this.diPan = new DiPan();
}
}
public class DiPan {
private LunZi lunZi;
DiPan(){
this.lunZi = new LunZi();
}
}
public class LunZi {
private int size;
LunZi(){
this.size = 30;
}
}
轮子尺寸变更
这种设计需要修改整个上层所有类的构造函数
Car car = new Car(40);
System.out.println(car.getDiPan().getLunZi().getSize());
public class Car {
private DiPan diPan;
Car(int size){
this.diPan = new DiPan(size);
}
}
public class DiPan {
private LunZi lunZi;
DiPan(int size){
this.lunZi = new LunZi(size);
}
}
public class LunZi {
private int size;
LunZi(int size){
this.size = size;
}
}
我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。
LunZi lunZi = new LunZi(40);
DiPan diPan = new DiPan(lunZi);
Car car = new Car(diPan);
System.out.println(car.getDiPan().getLunZi().getSize());
public class Car {
private DiPan diPan;
Car(DiPan diPan){
this.diPan = diPan;
}
}
public class DiPan {
private LunZi lunZi;
DiPan(LunZi lunZi){
this.lunZi = lunZi;
}
}
public class LunZi {
private int size;
LunZi(int size){
this.size = size;
}
}
spring boot 中的 IOC
BeanFactory
IoC容器是一个管理Bean的容器。在Spring的定义在中,它要求所有的IoC容器都需要实现BeanFactory接口,它是一个顶级容器接口。
默认情况下,Spring IoC容器中的Bean都是以单例存在的。
由于BeanFactory的功能不够强大,因此Spring在BeanFactory的基础上,设计了一个更为高级的接口ApplicationContext,它是BeanFactory的子接口之一,在Spring的体系中,BeanFactory和ApplicationContext是最为重要的接口设计,在现实中我们使用的大部分Spring IoC容器是ApplicationContext接口的实现类。
容器管理对象,不用每次都自己取new对象。使用@Service和@Autowired提供和使用服务。
@Service
public class ArBillServiceImpl implements IArBillService {
}
注入方式
构造注入
被注入对象的构造方法的参数列表声明依赖对象的参数列表
Spring4.3+之后,constructor注入支持非显示注入方式。
@Autowired
public ArTplBuildController(IArTplBuildService arTplBuildService) {
this.arTplBuildService = arTplBuildService;
}
字段注入
@Autowired
private IArSysSetupService iArSysSetupService;
setter注入
被注入对象的setter()方法的参数列表声明依赖对象
接口注入
比较繁琐。已经极少使用了。
被注入对象实现的接口方法的参数列表声明依赖对象
方式 | 优点 | 缺点 | 场景 |
---|---|---|---|
字段注入 | 简单,便于添加新的dependency | 可能会出现注入失败而出现NullPointedException;在Test和其他Module不可用;不可用于final字段,从而无法保证字段的不变性。 | Spring官方不推荐这种写法。 |
setter注入 | 灵活性高,便于修改依赖对象 | 对于仅使用setter注入的依赖对象需要进行非空检查;对象无法在构造完成后马上进入就绪状态 | 重新注入非默认值(构造器注入)的依赖对象;对于非必需的依赖,建议使用setter注入 |
构造注入 | 对象在构造完成之后,即已进入就绪状态,可以马上使用。 | 当依赖对象比较多的时候,构造方法的参数列表会比较长,维护和使用也比较麻烦,根据单一职责原则,此时应该考虑重构了。使用不慎还有可能出现循环依赖。 |
Spring4.x之后,注入方式应该按需选择setter或constructor注入方式。
官方为什么不推荐字段注入
- 单一职责侵入
添加依赖是很简单的,可能过于简单了。添加六个、十个甚至一堆依赖一点也没有困难,使得我们不易察觉违反单一职责原则的程序。
而使用构造器注入方法时,发现违反单一职责原则的程序则相对容易了。在使用构造器方式注入时,到了某个特定的点,构造器中的参数变得太多以至于很明显地发现something is wrong。拥有太多的依赖通常意味着你的类要承担更多的责任。明显违背了单一职责原则(SRP:Single responsibility principle)。- 无法声明不变的字段。
字段注人无法注入final字段,只有构造器注入才能注入final字段- 隐藏了依赖关系
使用依赖注入容器意味着类不再对依赖对象负责,获取依赖对象的职责从类中抽离出来,IoC容器会帮你装配。当类不再为依赖对象负责,它应该更明确的使用公有的接口方法或构造器,使用这种方式能很清晰的了解类需要什么,也能明确它是可选的(setter注入)还是强制的(构造器注入)。- 依赖注入容器紧耦合
依赖注入框架的核心思想之一就是受容器管理的类不应该去依赖容器所使用的依赖对象。换句话说,这个类应该是一个简单的POJO(Plain Ordinary Java Object)能够被单独实例化并且你也能为它提供它所需的依赖。只有这样,你才能在单元测试中实例化这个类而不必去启动依赖注入容器,实现测试分离(启动容器更多是集成测试)。
然而,当使用变量直接注入时,没有一种方式能直接地实例化这个类并且满足其所有的依赖。这意味着需要手动new出来依赖对象或者只能在IoC Container范围使用。
自动装配
自动装配是为了将依赖注入“自动化”的一个简化配置的操作。
当一个对象的属性是另一个对象时,实例化时,需要为这个对象属性进行实例化。这就是装配。
如果一个对象只通过接口来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行切换。但是这样会存在一个问题,在传统的依赖注入配置中,我们必须要明确要给属性装配哪一个bean的引用,一旦bean很多,就不好维护了。基于这样的场景,spring使用注解来进行自动装配,解决这个问题。自动装配就是开发人员不必知道具体要装配哪个bean的引用,这个识别的工作会由spring来完成。与自动装配配合的还有“自动检测”,这 个动作会自动识别哪些类需要被配置成bean,进而来进行装配。这样我们就明白了,自动装配是为了将依赖注入“自动化”的一个简化配置的操作。
装配方式
- byName就是会将与属性的名字一样的bean进行装配。
- byType就是将同属性一样类型的bean进行装配。-- constructor就是通过构造器来将类型与参数相同的bean进行装配。
- autodetect是constructor与byType的组合,会先进行constructor,如果不成功,再进行byType。
@Autowired注解是byType类型的,这个注解可以用在属性上面,setter方面上面以及构造器上面。使用这个注解时,就不需要在类中为属性添加setter方法了。但是这个属性是强制性的,也就是说必须得装配上,如果没有找到合适的bean能够装配上,就会抛出异常:`NoSuchBeanDefinitionException,如果required=false时,则不会抛出异常。另一种情况是同时有多个bean是一个类型的,也会抛出这个异常。此时需要进一步明确要装配哪一个Bean,这时可以组合使用@Qualifier注解,值为Bean的名字即可。@Qualifier注解使用byName进行装配,这样可以在多个类型一样的bean中,明确使用哪一个名字的bean来进行装配。@Qualifier注解起到了缩小自动装配候选bean的范围的作用。
循环依赖
org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'arBillServiceImpl': Requested bean is currently in creation:
Is there an unresolvable circular reference?
The dependencies of some of the beans in the application context form a cycle:
arBillController (field private com.ufgov.ar.modules.bill.service.IArBillService com.ufgov.ar.modules.bill.controller.ArBillController.arBillService)
┌─────┐
| arBillServiceImpl defined in file [D:\IdeaProjects\ar-server-8.20\target\classes\com\ufgov\ar\modules\bill\serviceImpl\ArBillServiceImpl.class]
↑ ↓
| arTplBuildServiceImpl (field private com.ufgov.ar.modules.bill.service.IArBillService com.ufgov.ar.modules.tplset.serviceImpl.ArTplBuildServiceImpl.arBillService)
└─────┘
解决方法
- 重新设计
- 改用setter注入方式(推荐)
与constructor注入不同,setter是按需注入的,并且允许依赖对象为null; - @Lazy注解
@Lazy延迟初始化 - 使用ApplicationContextAware, InitializingBean