Spring的控制反转、依赖注入、循环依赖

转载于:https://blog.csdn.net/qq_42192693/article/details/112967959


1,IoC(Spring的核心和基础)

1.1,IoC原理

纵观所有的Java应用(从基于Applet的小应用到多层结构的企业级应用),这些应用中大量存在A对象调用B对象方法的情况,这种情况被Spring称为依赖,即A对象依赖B对象。对于Java应用而言,它们总是由一些相互调用的对象构成的,Spring把这种相互调用的关系称为依赖关系。

Spring框架的核心功能有两个:

  • Spring容器作为超级大工厂,负责创建、管理所有的Java对象,这些Java对象被称为Bean。
  • Spring容器管理容器中的Bean之间的依赖关系,Spring使用一种被称为“依赖注入”的方式来管理Bean之间的依赖关系。

IOC(控制反转):IoC 不是一种技术,只是一种思想并非 Spring 特有,其思想是反转资源获取的方向。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

简单的说之前我们在代码中创建一个对象是通过 new 关键字,而使用了 Spring 之后,我们不在需要自己去 new 一个对象了,而是直接通过容器里面去取出来,再将其自动注入到我们需要的对象之中,也就说创建对象的控制权不在我们程序员手上了,全部交由 Spring 进行管理。交给 Spring 管理的也称为 Bean,所有的 Bean 都被存储在一个 Map 集合中,这个 Map 集合也称为 IoC 容器。

将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。比如说,在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能每次都要搞清这个 Service 所有底层类的构造函数,这显然过于繁琐。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

DI(依赖注入): IoC 的另一种表述方式:即组件以一些预先定义好的方式(例如: setter 方法)接受来自如容器的资源注入. 相对于 IOC 而言,这种表述更直接。所谓依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。

因此,不管是依赖注入还是控制反转,其含义是完全相同的。当某个Java对象(调用者)需要调用另一个Java对象(被依赖对象)的方法时,在传统模式下通常有如下两个做法

  • 原始做法:调用者主动创建被依赖对象,然后再调用被依赖对象的方法。由于调用者需要通过“new 被调用对象构造器()”的代码创建安对象,因此,必然导致调用者与被依赖对象实现类的硬编码耦合,非常不利于项目和维护。
  • 简单工厂模式:调用者先找到被依赖对象的工厂,然后主动通过工厂去获取被依赖对象,最后再调用被依赖对象的方法。大致需要需要把握三点:(1)调用者向被依赖对象的接口编程;(2)将被依赖对象的创建交给工厂完成;(3)调用者通过工厂来获得被依赖组件。通过这三点,可以保证调用者只需要与被依赖对象的接口耦合,这就避免了类层次的硬编码耦合。这个方法的唯一缺点是,调用组件需要主动通过工厂去获取被依赖对象,这就会带来调用组件与被依赖对象工厂的耦合。

Spring框架的IOC原理:调用者无需主动获取被依赖对象,调用者只需要被动接受Spring容器为调用者的成员变量赋值即可(只要配置一个<property.../>子元素,Spring就会执行对应的setter方法为调用者的成员变量赋值)。IoC实现的基础是工厂模式,所使用的技术主要是java的反射技术。(工厂模式+反射)

  • 使用Spring框架之后,调用者获取被依赖对象的方式由原来的主动获取,变成了被动接受——也被称为控制反转。
  • 从Spring角度来看,Spring容器负责将被依赖对象赋值给调用者的成员变量——相当于为调用者注入它依赖的实例,因此也被称为依赖注入。
  • 正因为Spring将被依赖对象注入给了调用者,所以调用者无须主动获取被依赖对象,只要被动等待Spring容器注入即可。由此可见,控制反转和依赖注入其实是同一种行为的两种表达,只是描述的角度的问题。

使用依赖注入,不仅可以为Bean注入普通的属性值,还可以注入其他Bean的引用。通过这种依赖注入,JavaEE应用中的各种组件不需要以硬编码的方式耦合在一起,甚至无须使用工厂模式。依赖注入达到的效果,非常类似于“共产主义”,当某个Java实例需要其他Java实例时,系统自动提供所需的实例,无须程序显式获取。

依赖注入是一种非常优秀的解耦方式。依赖注入让Spring的Bean以配置的文件组合在一起,而不是以硬编码的方式耦合在一起。IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。

1.2,理解依赖注入

案例:一个人(Java实例,调用者)需要一把斧头(Java实例,被依赖对象)。

原始做法:需要斧头的人(调用者)只能自己去磨一把斧子(被依赖对象)。这就相当于在Java程序里的调用者自己创建被依赖对象,通常采用new关键字来调用构造器创建一个被依赖对象。

Java实例的调用者创建被调用的Java实例,调用者直接使用new关键字创建被依赖对象,程序高度耦合,效率低下,真是应用极少使用这种方式。

  • 可扩展性差:由于人和斧头实现高度耦合,当程序视图扩展斧头时,人的代码也要随之改变。
  • 各组件职责不清:对于人组件而言,它只需要调用斧头的方法即可,并不关心斧头的创建过程。这种种情况下,人却主动创建斧头,因此职责混乱。

简单工厂模式:斧头不再由普通人完成,而是在工厂里被生产出来,此时需要斧头的人(调用者)找到工厂,购买斧头,无须关心斧头的制作过程。对应简单工厂模式,调用者只需要定位工厂,无须理会被依赖对象的具体实现过程。

调用者无须关心被依赖对象的具体实现过程,只需要找到符合某种标准(接口)的实例,即可使用。此时调用的代码面向接口编程,可以让调用者和被依赖对象的实现类的解耦,专业是工厂模式大量使用的原因。但调用者依然需要主动定位工厂,调用者与工厂耦合在一起。

IoC:需要斧头的人无须定位工厂,坐等社会的提供即可。调用者无需关心被依赖对象的实现,无须理会工厂,等待Spring的依赖注入。

程序完全无须理会被依赖对象的实现,也无须主动定位工厂,这是一种优秀的解耦方式。实例之间的依赖关系由IoC容器负责管理。

使用Spring框架之后的两个主要改变是:

  • 程序无须使用new调用构造器去创建对象。所有的Java对象都可交给Spring容器去创建。
  • 当调用者需要调用被依赖对象的方法时,调用者无须去主动获取被依赖对象,只要等待Spring容器注入即可。

1.3,工厂模式+反射=IoC

Spring 中的 IoC 的实现原理就是工厂模式加反射机制。 示例:

interface Fruit {
    public abstract void eat();
}
 
class Apple implements Fruit {
    public void eat() {
        System.out.println("Apple");
    }
}
 
class Orange implements Fruit {
    public void eat() {
        System.out.println("Orange");
    }
}
 
class Factory {
    public static Fruit getInstance(String ClassName) {
        Fruit f = null;
        try {
            f = (Fruit) Class.forName(ClassName).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
}
 
public class Main {
    public static void main(String[] args) {
        Fruit f = Factory.getInstance("Apple");
        if (f != null) {
            f.eat();
        }
    }
}

2,依赖注入方式

构造注入:就是被注入对象可以在它的构造方法中声明依赖对象的参数列表,让外部知道它需要哪些依赖对象。然后,IoC Service Provider会检查被注入的对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。构造方法注入方式比较直观,对象被构造完成后,即进入就绪状态,可以马上使用。

设值注入:通过setter方法,可以更改相应的对象属性。所以,当前对象只要为其依赖对象所对应的属性添加 setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。setter方法注入虽不 像构造方法注入那样,让对象构造完成后即可使用,但相对来说更宽松一些,可以在对象构造完成后再注入。

接口注入:相对于前两种注入方式来说,接口注入没有那么简单明了。被注入对象如果想要IoC Service Provider为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。IoC Service Provider最终通过这些接口来了解应该为被注入对象注入什么依赖对象。相对于 前两种依赖注入方式,接口注入比较死板和烦琐。

总体来说,构造注入和设值注入因为其侵入性较弱,且易于理解和使用,所以是现在使用最 多的注入方式。而接口注入因为侵入性较强,近年来已经不流行了。

2.1,设值注入

设值注入:是指IoC容器使用setter方法来注入被依赖的实例。通过调用无参构造器或无参static工厂方法实例化bean之后,调用该bean的setter方法,即可实现基于setter的DI。这种方式简单、直观,因而在Spring的依赖注入里大量使用。

Bean与Bean之间的依赖关系由Spring管理,Spring采用setter方法为目标Bean注入所依赖的Bean,这种方式被称为设置注入。

依赖注入以配置文件管理Bean实例之间的耦合,让Bean实例之间的耦合从代码层次分类出来,依赖注入是一种优秀的解耦方式。

使用Spring IoC容器的三个基本要点:

  • 应用程序的各种组件面向接口编程。面向接口编程可以将组件之间的耦合关系提升到接口层次,从而更有利于项目后期的扩展。
  • 应用程序的各组件不再由程序主动创建,而是由Spring容器来负责产生并初始化。
  • Spring采用配置文件或注解来管理Bean的实现类、依赖关系,Spring容器则根据配置文件或注解,利用反射来创建实例,并为之注入依赖关系。

1,定义接口

Spring推荐面向接口编程。不管是调用者,还是被依赖对象,都应该为之定义接口,程序应该面向它们的接口,而不是面向实现类编程,这样便于后期的省级,维护。

package Bean;
public interface Person {
    public void useAxe();
}
package Bean;
public interface Axe {
    public String chop();
}

Spring推荐面向接口编程,这样可以更好地让规范和实现分离,从而提供更好的解耦。对于一个Java EE应用,不管DAO组件,还是业务逻辑组件,都应该先定义一个接口,该接口定义了该组件应该实现的功能,但功能的实现则由其实现类提供。

2,继承接口 

package Bean;
public class Chinese implements Person{
    private Axe axe;
    public void setAxe(Axe axe) {
        this.axe = axe;
    }
    public void useAxe() {
        System.out.println(axe.chop());
    }
}
package Bean;
public class StoneAxe implements Axe{
    public String chop() {
        return "石头斧头砍柴好慢啊";
    }
}

 上面程序实现了Person接口中的useAxe()方法,实现该方法时调用了axe的chop()方法,这就是典型的依赖关系。Java中大部分都是依赖方式,Spring容器的最大作用就是以松耦合的方式来管理这种调用关系。

3,编写XML

到目前为止,程序仍然不知道Chinese类和哪个Axe实例耦合,Spring需要使用XML配置文件来指定实例之间的依赖关系。Spring采用了XML配置文件,从Spring2.0开始,Spring推荐采用XML Schema来定义配置文件的语义约束。当采用XML Schema来定义配置文件的语义约束时,还可利用Spring配置文件的扩展性进一步简化Spring配置。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="chinese" class="Bean.Chinese">
        <property name="axe" ref="stoneAxe"/>
    </bean>
    <bean id="stoneAxe" class="Bean.StoneAxe">
    </bean>
</beans>
  • id:指定该Bean的唯一标识,Spring根据id属性值来管理Bean,程序通过id属性值来访问该Bean实例,Spring也通过Bean的id属性值管理Bean与Bean之间的依赖。
  • class:指定该Bean的实现类,此处不可再用接口,必须使用实现类,Spring容器会使用XML解析器读取该属性值,并利用反射来创建该实现类的实例。

Spring管理Bean很灵巧,Bean与Bean之间的依赖关系放在配置文件里组织,而不是写在代码里。通过配置文件的指定,Spring能精确地为每个Bean的成员变量注入值。

Spring会自动监测每个<bean.../>定义里的<preperty.../>元素定义,Spring会调用默认的构造器创建Bean实例之后,立即调用对应的setter方法为Bean的成员变量注入。

4,编写主程序

package Action;
import Bean.Person;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class Test {
    public static void main(String[] args) throws BeansException {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Person chinese = context.getBean("chinese", Person.class);
        chinese.useAxe();
    }
}

上面代码实现了创建Spring容器,并通过Spring容器来获取Bean实例。Spring容器就是一个巨大工厂,它可以生产出所有类型的Bean实例。程序获取Bean实例的方法是getBean()。一旦通过Spring容器获得了Bean实例之后,如何调用就是按照普通的Java类使用就行了。

主程序调用Person的useAxe()方法时,该方法的方法体内需要使用Axe实例,但程序没有任何地方将特定的Person实例和Axe实例耦合在一起(程序没有为Person实例传入Axe实例,Axe实例由Spring在运行期间注入)。

Person实例不仅不需要了解Axe实例的实现类,甚至无须了解Axe的创建过程。Spring容器根据配置文件,创建Person实例时,不仅创建了Person对象,并为该对象注入它所以来的Axe实例。

5,修改实例

假如系统需要改变Axe的实现——这种改变,对于实际开发是很常见的情形,也许是因为技术的改进,也许是因为性能的优化,也许是因为需求的变化......此时只需要给出Axe的另一个实现,而Person接口、Chinese类的代码无须任何改变。

package Bean;
public class SteelAxe implements Axe{
    public String chop() {
        return "钢斧砍的真快";
    }
}

将修改后的SteelAxe部署在Spring容器中,只需要在Spring配置文件中增加下面代码。

<bean id="steelAxe" class="Bean.SteelAxe"/>

然后修改chinese Bean的配置,将原来传入stoneAxe的地方改为传入steelAxe。

<property name="axe" ref="steelAxe"/>

从上面可以看出,因为chinese实例与具体的Axe实现类没有任何关系,chinese实例仅仅与Axe接口耦合,这就保证了chinese实例与Axe实例之间的松耦合——这也是Spring强调面向接口编程的原因。

2.2,构造注入

构造注入:构造方法注入是指IoC容器使用构造方法来注入被依赖的实例。基于构造器的DI通过调用带参数的构造器来实现,每个参数代表着一个依赖。

构造注入的本质:驱动Spring在底层以反射方式执行带指定参数的构造器,当执行带参数的构造器时,就可利用构造器参数对成员变量执行初始化。

问题:<bean.../>元素默认总是驱动Spring调用无参数的构造器来创建对象,那怎样驱动Spring调用有参的构造参数去创建对象呢?

答案:<constructor-arg.../>子对象,每个<constructor-arg.../>子元素代表一个构造器参数,如果<bean.../>元素包含N个<constructor-arg.../>子元素,就会驱动Spring调用N个参数的构造器来创建对象。

1,继承接口

package Bean;
public class Chinese implements Person{
    private Axe axe;
    public Chinese(Axe axe){
        this.axe = axe;
    }
    public void useAxe() {
        System.out.println(axe.chop());
    }
}

上面Chinese类没有提供设置axe成员变量的setter方法,仅仅提供了一个带Axe参数的构造器,Spring将通过该构造器为chinese注入所依赖的Bean实例。

2,编写XML

构造注入的配置文件也需要简单修改,为了使用构造注入,还需要使用<constructor-arg.../>元素指定构造器的参数。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="chinese" class="Bean.Chinese">
        <constructor-arg ref="steelAxe"/>
    </bean>
    <bean id="stoneAxe" class="Bean.StoneAxe">
    </bean>
    <bean id="steelAxe" class="Bean.SteelAxe"/>
</beans>

代码中使用<constructor-arg.../>元素指定了一个构造器参数,该参数类型Axe,这指定Spring调用Chinese类带一个Axe参数的构造器来创建chinese实例。

配置<constructor-arg.../>元素时可以指定匹配方法:按索引匹配入参、按类型匹配入参。

该实例的执行效果与设置注入steelAxe时执行效果完全一样。区别在于:创建Person实例中Axe属性的时机不同——设值注入是先通过无参数的构造器创建一个Bean实例,然后调用对应的setter方法注入依赖关系;而构造注入则直接调用有参数的构造器,当Bean实例创建完成后,已经完成了依赖关系的注入。

3,构造参数的选择

<bean id="test" class="Bean.test">
    <constructor-arg value="hello"/>
    <constructor-arg value="23"/>
</bean>
package Bean;
public class test {
    public test(String a,String b){
    }
    public test(String a, int b){
    }
}

这就产生了一个问题:xml配置文件的代码会调用test那个构造函数,答案是第一个构造函数。因为Spring只能解析出来一个“23”的字符串,但它到底转换为那种,只能根据test的构造器来尝试转换。

为了更明确地指定数据类型,Spring允许为<constructor-arg.../>元素指定一个type属性。

<bean id="test" class="Bean.test">
    <constructor-arg value="hello" type="java.lang.String"/>
    <constructor-arg value="23" type="int"/>
</bean>

2.3,两种注入方式的对比

不管是设值注入还是构造注入,都视为Bean的依赖,接受Spring容器管理,依赖关系的值要么是一个确定的值,要么是Spring容器中其他Bean的引用。

对于singleton作用域的Bean,如果没有强行取消其预初始化行为,系统会在创建Spring容器时预初始化所有的singleton Bean。与此同时,该Bean所依赖的Bean也被一起实例化。

BeanFactory和ApplicationContext实例化Bean的时机:https://shao12138.blog.csdn.net/article/details/113032492

两种依赖注入方式并没有绝对的好坏,只是适应的场景有所不同。

区别构造注入设值注入
是否有部分注入没有部分注入有部分注入
是否覆盖setter属性不会覆盖setter属性会覆盖setter属性
修改是否创建新的实例任意修改都会创建一个新实例任意修改不会创建一个新实例
场景适用于设置很多属性适用于设置很少属性

设值注入的优点:

  • 与传统的JavaBean的写法更相似,程序开发人员更容易理解、接受。通过setter方法设定依赖关系更显得直观、自然。
  • 对于复杂的依赖关系,如果采用构造注入,会导致构造器过于臃肿,难以阅读。Spring在创建Bean实时时,需要同时实例化其依赖的全部实例,因而导致性能下降。而是用设值注入,则能避免这些问题。
  • 尤其是在某些成员变量可选的情况下,多参数的构造器更加笨重。

构造注入的优势:

  • 构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入。例如,组件中其他依赖关系的注入,常常需要依赖Datasource的注入。采用构造注入,可以在代码中清晰地决定注入顺序。
  • 对于依赖关系无须变化的Bean,构造注入更有用处。因为没有setter方法,所有的依赖关系全部在构造器内设定。因此,无须担心后续的代码对依赖关系产生破坏。
  • 依赖关系只能在构造中设定,则只能在构造器中设定,则只有组件的穿见着才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。

建议采用以设置注入为主,构造注入为辅的注入策略。对依赖关系无须变化的注入,尽量采用构造注入。而其他依赖关系的注入,则考虑采用设值注入。

3,循环依赖

3.1,何为循环依赖

循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:

 循环依赖的种类:

  • 构造器的循环依赖:Spring无法处理,两个或多个Bean通过构造函数注入彼此时,Spring无法实例化任何一个Bean,因为它们都需要其他Bean的实例才能被实例化,直接抛出 BeanCurrentlylnCreationException异常。
  • 单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。
  • 非单例循环依赖:Spring无法处理,Spring在创建Bean时会先创建Bean的实例,然后再注入它的依赖项。但是,在非单例Bean的情况下,如果两个Bean之间存在循环依赖,那么Spring无法确定它们之间的创建顺序,从而导致循环依赖问题的发生。

3.2,Spring解决循环依赖

Spring的单例对象的初始化主要分为三步:

  • createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象。
  • populateBean:填充属性,这一步主要是多bean的依赖属性进行填充。
  • initializeBean:调用spring xml中的init 方法。

从上面讲述的单例bean初始化步骤我们可以知道,循环依赖主要发生在第一、第二步。也就是构造器循环依赖和field循环依赖。

那么我们要解决循环引用也应该从初始化过程着手,对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存

首先我们看源码,三级缓存主要指:

  • 第一级缓存:singletonObjects缓存,它保存了单例Bean的实例。

  • 第二级缓存:earlySingletonObjects缓存,它保存了单例Bean的提前曝光的对象,也就是还未完全实例化的对象。当一个单例Bean被创建时,它会被放入earlySingletonObjects缓存中。

  • 第三级缓存:singletonFactories缓存,它保存了创建单例Bean的工厂方法。当一个单例Bean被创建时,它会被放入singletonFactories缓存中。

当Spring发现两个Bean之间存在循环依赖时,它会先创建Bean的实例,然后将实例放入第二级缓存中。接着,Spring会将创建Bean的工厂方法放入第三级缓存中,以供后续使用。当一个Bean需要注入另一个Bean时,Spring会从第二级缓存中查找提前曝光的对象,如果找到了,就返回该对象。如果没有找到,Spring会从第三级缓存中获取工厂方法来创建对象,创建完毕后放入第二级缓存中,并从第三级缓存中删除该工厂方法。也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值