Spring IoC注入与循环依赖

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/programmer_at/article/details/82389221

Spring Core经典内容极少变动,不管Spring发展到什么版本,本文都有利于你理解Spring IoC

什么是IoC

Spring IoC容器负责对象的生命周期和对象之间的(依赖)关系。
在创建新的Bean时,IoC容器会自动注入新Bean的所依赖的其他Bean,而无须自己手动创建。
这里写图片描述

  • IoC容器自动完成对象的初始化,避免在开发过程中写一大段初始化代码。
  • 创建实例的时候不需要了解细节。

    优点:
    不会对业务代码构成很强的侵入性,对象具有更好的可测试性,可重用性和可拓展性。

    优点看起来很抽象,可以结合实例来理解:参见王福强的《Spring揭秘》2.3节IoC的附加值

IoC与控制反转

IoC 全称为 InversionofControl,翻译为 “控制反转”.
1. 谁控制谁:在传统的开发模式下,我们都是采用直接 new 一个对象的方式来创建对象,也就是说你依赖的对象直接由你自己控制,但是有了 IOC 容器后,则直接由 IoC 容器来控制。所以“谁控制谁”,当然是 IoC 容器控制对象。
2. 控制什么:控制对象。
3. 为何是反转:没有 IoC 的时候我们都是在自己对象中主动去创建被依赖的对象,这是正转。但是有了 IoC 后,所依赖的对象直接由 IoC 容器创建后注入到被注入的对象中,依赖的对象由原来的主动获取变成被动接受,所以是反转。
4. 哪些方面反转了:所依赖对象的获取被反转了。

普遍认为(误,下面会讲),IoC还有一个别名为 DI( DependencyInjection),即依赖注入。“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。
理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:
1. 谁依赖于谁:“被注入的对象”依赖于“依赖对象”。举个例子,对象A依赖B,那么IoC容器在注入A对象之前,需要先注入B对象;对象A依赖于IoC容器;对象B被注入到对象A中,所以A是被注入对象的对象,B是依赖对象,A依赖B。
2. 为什么需要依赖:容器管理对象需要IoC容器来提供对象需要的外部资源;
3. 谁注入谁:很明显是IoC容器注入某个对象,也就是注入“依赖对象”;
4. 注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

二者的关系
控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。

如何理解IoC容器

我们自己手动创建一个车instance时候,是从底层往上层new的:
这里写图片描述
每一个类的构造函数都直接调用了底层代码的构造函数。这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new/注入。底层的改动都会牵动所有上层的改动。如果每次修改一个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。

而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历)。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。
这里写图片描述
这里IoC Container可以直接隐藏具体的创建实例的细节,在我们来看它就像一个工厂:
这里写图片描述
我们就像是工厂的客户。我们只需要向工厂请求一个Car实例,然后它就给我们按照Config创建了一个Car实例。我们完全不用管这个Car实例是怎么一步一步被创建出来。

IoC注入方式

  • 构造器注入
    被注入对象的构造方法的参数列表声明依赖对象的参数列表
private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

@Autowired
public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) {
    this.dependencyA = dependencyA;
    this.dependencyB = dependencyB;
    this.dependencyC = dependencyC;
}

Spring4.3+之后,constructor注入支持非显示注入方式。

private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

// @Autowired
public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) {
    this.dependencyA = dependencyA;
    this.dependencyB = dependencyB;
    this.dependencyC = dependencyC;
}
  • setter方法注入
    被注入对象的setter()方法的参数列表声明依赖对象
private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

@Autowired
public void setDependencyA(DependencyA dependencyA) {
    this.dependencyA = dependencyA;
}

@Autowired
public void setDependencyB(DependencyB dependencyB) {
    this.dependencyB = dependencyB;
}

@Autowired
public void setDependencyC(DependencyC dependencyC) {
    this.dependencyC = dependencyC;
}
  • 接口注入
    比较繁琐。已经极少使用了。
    被注入对象实现的接口方法的参数列表声明依赖对象
  • 字段注入
@Autowired
private DependencyA dependencyA;

@Autowired
private DependencyB dependencyB;

@Autowired
private DependencyC dependencyC;

优缺点比较

注入方式 优点 缺点 实用场景
字段注入 简单,便于添加新的dependency 可能会出现注入失败而出现NullPointedException;在Test和其他Module不可用;不可用于final字段,从而无法保证字段的不变性。 Spring官方不推荐这种写法。
setter注入 灵活性高,便于修改依赖对象 对于仅使用setter注入的依赖对象需要进行非空检查;对象无法在构造完成后马上进入就绪状态 重新注入非默认值(构造器注入)的依赖对象;对于非必需的依赖,建议使用setter注入。
constructor注入 对象在构造完成之后,即已进入就绪状态,可以马上使用。 当依赖对象比较多的时候,构造方法的参数列表会比较长,维护和使用也比较麻烦,根据单一职责原则,此时应该考虑重构了。使用不慎还有可能出现循环依赖。

Spring4.x之后,注入方式应该按需选择setter或constructor注入方式。

官方为什么不推荐字段注入
1. 单一职责侵入
添加依赖是很简单的,可能过于简单了。添加六个、十个甚至一堆依赖一点也没有困难,使得我们不易察觉违反单一职责原则的程序。
而使用构造器注入方法时,发现违反单一职责原则的程序则相对容易了。在使用构造器方式注入时,到了某个特定的点,构造器中的参数变得太多以至于很明显地发现something is wrong。拥有太多的依赖通常意味着你的类要承担更多的责任。明显违背了单一职责原则(SRP:Single responsibility principle)。
2. 无法声明不变的字段。
字段注人无法注入final字段,只有构造器注入才能注入final字段
3. 隐藏了依赖关系
使用依赖注入容器意味着类不再对依赖对象负责,获取依赖对象的职责从类中抽离出来,IoC容器会帮你装配。当类不再为依赖对象负责,它应该更明确的使用公有的接口方法或构造器,使用这种方式能很清晰的了解类需要什么,也能明确它是可选的(setter注入)还是强制的(构造器注入)。
4. 依赖注入容器紧耦合
依赖注入框架的核心思想之一就是受容器管理的类不应该去依赖容器所使用的依赖对象。换句话说,这个类应该是一个简单的POJO(Plain Ordinary Java Object)能够被单独实例化并且你也能为它提供它所需的依赖。只有这样,你才能在单元测试中实例化这个类而不必去启动依赖注入容器,实现测试分离(启动容器更多是集成测试)。
然而,当使用变量直接注入时,没有一种方式能直接地实例化这个类并且满足其所有的依赖。这意味着需要手动new出来依赖对象或者只能在IoC Container范围使用。

循环依赖

依赖注入稍不注意就会出现循环依赖:
Bean之间的依赖顺序: BeanA -> BeanB -> BeanA
举个例子:

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public CircularDependencyA(CircularDependencyB circB) {
        this.circB = circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    @Autowired
    public CircularDependencyB(CircularDependencyA circA) {
        this.circA = circA;
    }
}

运行SpringBootApplication:

@SpringBootApplication
@ComponentScan("com.example.circulardependency.constructor")
public class CirculardependencyApplication {

    public static void main(String[] args) {
        SpringApplication.run(CirculardependencyApplication.class, args);
    }
}

会报以下的错:

BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA': Requested bean is currently in creation: Is there an unresolvable circular reference?

还会画出dependency circle:
这里写图片描述

解决办法

出现循环依赖是因为设计问题,最佳处理方法是重新设计

在实际开发中,推倒重来往往是不允许的,所以会有以下几种补救方法。

1.改用setter注入方式(推荐)

与constructor注入不同,setter是按需注入的,并且允许依赖对象为null;

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public void setCircB(CircularDependencyB circB) {
        this.circB = circB;
    }

    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    private String message = "Hi!";

    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }

    public String getMessage() {
        return message;
    }
}

添加单元测试:

@RunWith(SpringRunner.class)
@SpringBootTest
@ComponentScan("com.example.circulardependency.setter")
public class CirculardependencyApplicationTests {
    @Bean
    public CircularDependencyB getDependencyB() {
        return new CircularDependencyB();
    }

    @Bean
    public CircularDependencyA getDependencyA() {
        CircularDependencyA circularDependencyA = new CircularDependencyA();
        circularDependencyA.setCircularDependencyB(getDependencyB());
        return circularDependencyA;
    }

    @Test
    public void contextLoads() {
        System.out.println("Hello world.");
        CircularDependencyA circularDependencyA = getDependencyA();
        System.out.println(circularDependencyA.getCircularDependencyB().getMessage());
    }
}

使用字段注解也可以解决循环依赖,但是字段注解为非官方推荐做法,所以在这里也就不给出实例了。

2. @Lazy注解

@Lazy延迟初始化。在本例中,会先构建 CircularDependencyA完成后, 再构建CircularDependencyB,打破dependency circle。

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}

3. 使用ApplicationContextAware, InitializingBean

ApplicationContextAware获取SpringContext,用于加载bean;InitializingBean定义了设置Bean的property之后的动作。

@Component
public class CircularDependencyA implements InitializingBean, ApplicationContextAware {
    private CircularDependencyB circB;
    private ApplicationContext context;

    @Override
    public void afterPropertiesSet() throws Exception {
        this.circB = context.getBean(CircularDependencyB.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    public CircularDependencyB getCircularDependencyB() {
        return circB;
    }
}

原理:跟Spring Bean的Life cycle有关:先实例化,再调用setApplicationContext(), afterPropertiesSet(); 所以,CircularDependencyB已经加载完毕后,再加载CircularDependencyA。
倘若,CircularDependencyA先实例化, 调用afterPropertiesSet()时发现CircularDependencyB尚未加载,先加载CircularDependencyB(调用构造函数,由于此时CircularDependencyA已经实例化了,所以能够顺利加载),设置属性circB,CircularDependencyA加载完成。

ref:
[1]IoC注入方式: https://www.vojtechruzicka.com/field-dependency-injection-considered-harmful/
[2] 循环依赖: https://www.baeldung.com/circular-dependencies-in-spring
[3] 字段注入: https://blog.marcnuri.com/field-injection-is-not-recommended/
[4] IoC与DI: 概念精准解释版:http://sishuok.com/forum/blogPost/list/2427.html
通俗易懂版:https://www.zhihu.com/question/23277575/answer/169698662

猜你喜欢

转载自blog.csdn.net/programmer_at/article/details/82389221