一文搞懂Spring依赖注入

在这里插入图片描述

在官网中,咱们发现它的核心技术之一:Dependency Injection,简称:DI ,翻译过来就是依赖注入。今天咱们就来盘一盘它。java

在本文中,咱们将深刻研究 Spring 框架 DI背后的故事,包括 Spring Inversion of Control(控制反转)、 DIApplicationContext 接口。 基于这些基本概念,咱们将研究如何使用基于 java 和基于 XML 的配置来 建立Spring 应用程序。 最后,咱们将探讨在建立 Spring 应用程序时遇到的一些常见问题,包括 bean冲突循环依赖性算法

一 控制反转(Inversion of Control)

在学习DI以前,咱们先学习一下 IoC控制反转),接下来的一段可能读起来会让你感受比较啰嗦,可是要细细体会每一次改变的意图,和咱们的解决方案,对于理解控制反转很是重要。spring

首先来了解下咱们一般实例化一个对象的方式。 在 平时,咱们使用 new 关键字实例化一个对象。 例如,若是有一个 Car 类,咱们可使用如下方法实例化一个对象 Carexpress

Car car = new Car();
复制代码

由于汽车有不少零部件组成,咱们定义Engine接口来模拟汽车引擎,而后将engine对象做为成员变量放在Carmarkdown

public interface Engine {
     void turnOn();
}

public class Car {

    private Engine engine;
    
    public Car() {}
    
    public void start() {

        engine.turnOn();

    }

}
复制代码

如今,咱们能够调用start()方法吗?显然是不行的,一眼能够看出会报NullPointerException (NPE),由于咱们没有在Car的构造函数中初始化engine。一般咱们采用的方案就是在Car的构造函数中以为使用Engine接口的哪一个实现,并直接将该实现分配给engine字段;框架

如今,咱们来首先建立Engine接口的实现类dom

public class ElectricEngine implements Engine {
    @Override
    public void turnOn() {
        System.out.println("电动引擎启动");
    }
}

public class CombustionEngine implements Engine {
    @Override
    public void turnOn() {
        System.out.println("燃油引擎启动");
    }
}
复制代码

咱们修改Car的构造函数,使用ElectricEngine实现,将咱们的engine字段分配给一个实例化的ElectricEngine对象ide

public class Car {

    private Engine engine;

    public Car() {
        this.engine = new ElectricEngine();
    }

    public void start() {

        engine.turnOn();

    }

    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}
复制代码

如今咱们执行start()方法,咱们会看到以下输出:函数

在这里插入图片描述

大功告成,咱们成功解决了 NPE(空指针)问题,可是咱们胜利了吗?哈哈哈,显然没有!学习

在解决问题的同时,咱们又引入了另外一个问题。尽管咱们经过抽象Engine接口,而后经过不一样的Engine实现类来负责不一样类型引擎的业务逻辑,的确是很好的设计策略。可是细心的伙伴可能已经发现了,咱们Car类的构造函数中将engine声明为CombustionEngine,这将致使全部车都有一个燃油引擎。假如咱们如今要建立不一样的汽车对象,它有一个电动引擎,咱们将不得不改变咱们的设计。比较常见的方法是建立两个独立里的类,各司其职,在他们的构造函数中将engine分配给Engine接口的不一样实现;

例如:

public class CombustionCar {
    
    private Engine engine;
    
    public CombustionCar() {
        this.engine = new CombustionEngine();
    }
    
    public void start() {
        engine.turnOn();
    }

}

public class ElectricCar {
    private Engine engine;

    public ElectricCar() {
        this.engine = new ElectricEngine();
    }
    
    public void start() {
        engine.turnOn();
    }
    
}
复制代码

经过上面的一顿操做,咱们成功的解决了咱们引擎的问题。若是是一个平常需求,咱们已经能够成功交工了。可是这显然不是我写这篇文章的目的。

从设计的角度来讲,目前的代码是糟糕的,有如下两点缘由:

  1. 在两个不一样的类中,存在重复的start()方法;
  2. 咱们须要为每一个新的Engine 实现类建立一个新的类;

尤为后一个问题更加难以解决,由于咱们不控制Engine的实现,随着开发人员不断的建立本身的实现类,这个问题会更加恶化;

带着上面的问题,咱们继续思考.....................

咱们能够建立一个父类Car,将公共代码抽取到父类中,能够轻松解决第一个问题。因为Engine字段是私有的,咱们在父类Car的构造函数中接收Engine对象,而且进行赋值。

public class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.turnOn();
    }
}

public class CombustionCar extends Car{

    public CombustionCar() {
        super(new CombustionEngine());
    }

}

public class ElectricCar extends Car {

    public ElectricCar() {
        super(new ElectricEngine());
    }

}

复制代码

经过这种方法,咱们成功的解决了代码重复的问题,咱们来测试一下:

public class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.turnOn();
    }

    public static void main(String[] args) {

        CombustionCar combustionCar1 = new CombustionCar();
        combustionCar1.start();
        ElectricCar electricCar1 = new ElectricCar();
        electricCar1.start();
    }
}
复制代码

在这里插入图片描述

那么咱们该如何解决咱们提出的第二个问题那?

其实这个问题咱们能够换个角度看:为何咱们要去关注CombustionCarElectricCar,咱们如今将关注点回到咱们的Car,咱们如今已经容许客户端实例化Car对象时候将Engine对象做为构造函数的参数传入,其实已经消除了为每一个Engine对象建立新Car的问题。由于如今Car类依赖于Engine接口,并不知道任何Engine的实现;

经过带有Engine参数的构造函数,咱们已将要使用哪一个Engine实现的决定从Car类自己(最初由CombustionEngine决定)更改成实例化Car类的客户端。 决策过程的这种逆转称为IoC原则。 如今,由客户端控制使用哪一种实现,而不是由Car类自己控制使用哪一种Engine实现。

有点绕,你们结合下面的示例代码,细细琢磨

public class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.turnOn();
    }

    public static void main(String[] args) {

        /** * 老法子 * 为每一类型发送机的车建立类,而后实现父类car,而后在构造函数传入本身的引擎,而后调用start() */
        CombustionCar combustionCar1 = new CombustionCar();
        combustionCar1.start();
        ElectricCar electricCar1 = new ElectricCar();
        electricCar1.start();

        /** * 控制反转思想 * 把本身看做实例化car的客户端,须要什么引擎,直接传入相关对象 */
        CombustionEngine combustionEngine = new CombustionEngine();
        Car combustionCar = new Car(combustionEngine);
        combustionCar.start();
        ElectricEngine electricEngine = new ElectricEngine();
        Car electricCar = new Car(electricEngine);
        electricCar.start();
    }
}
复制代码

执行上面的代码,咱们发现均可以得到咱们想要的结果:

在这里插入图片描述

从上面的例子咱们能够看到,实例化Car类的客户端能够控制所使用的Engine实现,而且取决于将哪一个Engine实现传递给Car构造函数,Car对象的行为发生巨大变化。为何这么说,接着看下面

二 依赖注入(Dependency Injection)

在上面控制反转的知识点,咱们已经解决了由谁决定使用哪一种Engine实现的问题,可是不可避免,咱们也更改了实例化一个Car对象的步骤;

最开始,咱们实例化Car不须要参数,由于在它的构造函数里面已经为咱们newEngine对象。使用IoC方法以后,咱们要求在实例化一个Car以前,咱们须要先建立一个Engine对象,并做为参数传递给Car构造对象。换句话说,最初,咱们首先实例化Car对象,而后实例化Engine对象。可是,使用IoC以后,咱们首先实例化Engine对象,而后实例化Car对象;

所以,咱们在上面的过程当中建立了一个依赖关系。不过这种依赖关系不是指编译时候Car类对Engine接口的依赖关系,相反,咱们引入了一个运行时依赖关系。在运行时,实例化Car对象以前,必须首先实例化Engine对象。

2.1 依赖关系树

某一个具体的依赖对象你们能够理解为Spring中的bean,对于两个有依赖关系的bean,其中被依赖的那个bean,咱们把它称为依赖对象

咱们用图形化的方式来看看它们之间的依赖关系,其中图形的节点表明对象,箭头表明依赖关系(箭头指向依赖对象)。对于咱们个人Car类,依赖关系树很是简单:

若是依赖关系树的终端结点还有本身的附加依赖关系,那么这个依赖关系树将变得更加复杂。如今再看咱们上面的例子,若是CombustionEngine 还有其余依赖对象,咱们首先须要建立CombustionEngine的依赖对象,而后才能实例化一个CombustionEngine对象。这样在建立Car对象时候,才能将CombustionEngine传递给Car的构造函数;

//凸轮轴 
public class Camshaft {}
//机轴
public class Crankshaft {}

public class CombustionEngine implements Engine {

  //凸轮轴
  private Camshaft camshaft;

  //机轴
  private Crankshaft crankshaft;

  public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {

      this.camshaft = camshaft;

      this.crankshaft = crankshaft;
  }

  @Override

  public void turnOn() {

      System.out.println("燃油引擎启动");

  }

}

复制代码

通过咱们改造,咱们如今的依赖关系树变为下面的样子

2.2 依赖注入框架

随着咱们不断引入更多的依赖关系,这种复杂性将继续增加。为了解决这个复杂问题,咱们须要基于依赖关系树抽取对象的建立过程。这就是依赖注入框架

通常来讲,咱们能够把这个过程分为三个部分:

  1. 声明须要建立的对象须要哪些依赖对象
  2. 注册建立这些依赖对象所须要的类
  3. 提供一种使用1和2两点思想建立对象的机制

经过反射,咱们能够查看 Car 类的构造函数,而且知道它须要一个 Engine 参数。所以为了建立Car对象,咱们必须建立至少一个Engine接口的实现类用做依赖项来使用。在这里,咱们建立一个CombustionEngine 对象(为了方便,暂时当作只有一个实现类,bean冲突问题待会再说)来声明它做为依赖项来使用,就知足Car对象建立时的需求.

其实,这个过程是递归的,由于CombustionEngine 依赖于其余对象,咱们须要不断重复第一个过程,直到把全部依赖对象声明完毕,而后注册建立这些依赖对象所须要的类。

第三点其实就是将前面两点思想付诸实施,从而造成一种建立对象的机制

举个例子:好比咱们须要一个Car对象,咱们必须遍历依赖关系树并检查是否存在至少一个符合条件的类来知足全部依赖关系。 例如,声明CombustionEngine类可知足Engine节点要求。 若是存在这种依赖关系,咱们将实例化该依赖关系,而后移至下一个节点。

若是有一个以上的类知足所需的依赖关系,那么咱们必须显式声明应该选择哪种依赖关系。 稍后咱们将讨论 Spring 是如何作到这一点的。

一旦咱们肯定全部的依赖关系都准备好了,咱们就能够从终端节点开始建立依赖对象。 对于 Car 对象,咱们首先实例化 CamshaftCrankshaftーー由于这些对象没有依赖关系ーー而后将这些对象传递给 CombustionEngine 构造函数,以实例化 CombunstionEngine 对象。 最后,咱们将 CombunstionEngine 对象传递给 Car 构造函数,以实例化所需的 Car 对象。

了解了 DI 的基本原理以后,咱们如今能够继续讨论 Spring 如何执行 DI

2.3 Spring的依赖注入

Spring的核心是一个DI框架,它能够将DI配置转换为Java应用程序。

在这里咱们要阐述一个问题:那就是库和框架的区别。库只是类定义的集合。背后的缘由仅仅是代码重用,即获取其余开发人员已经编写的代码。这些类和方法一般在域特定区域中定义特定操做。例如,有一些数学库可以让开发人员仅调用函数而无需重作算法工做原理的实现。

框架一般被认为是一个骨架,咱们在其中插入代码以建立应用程序。 许多框架保留了特定于应用程序的部分,并要求咱们开发人员提供适合框架的代码。 在实践中,这意味着编写接口的实现,而后在框架中注册实现。

2.4 ApplicationContext

Spring 中,框架围绕 ApplicationContext 接口实现上一节中概述的三个 DI 职责。一般这个接口表明了一个上下文。 所以,咱们经过基于 java 或基于 xml 的配置向 ApplicationContext 注册合适的类,并从 ApplicationContext 请求建立 bean 对象。 而后 ApplicationContext 构建一个依赖关系树并遍历它以建立所需的 bean对象

Applicationcontext 中包含的逻辑一般被称为 Spring 容器。 一般,一个 Spring 应用程序能够有多个 ApplicationContext,每一个 ApplicationContext 能够有单独的配置。 例如,一个 ApplicationContext 可能被配置为使用 CombustionEngine 做为其引擎实现,而另外一个容器可能被配置为使用 ElectricEngine 做为其实现。

在本文中,咱们将重点讨论每一个应用程序的单个 ApplicationContext,可是下面描述的概念即便在一个应用程序有多个 ApplicationContext 实例时也适用。

三 基于 java 的配置

Spring为咱们提供了两种基于 java 的配置方式

  1. 基本配置
  2. 自动配置

3.1 基于 java 的基本配置

基于java的基本配置的核心,实际上是下面两个注解:

  1. @Configuration: 定义配置类
  2. @Bean: 建立一个bean

例如,给出咱们以前定义的 Car, CombustionEngine, Camshaft, 和Crankshaft 类,咱们能够建立一个下面 的配置类:

/** * @author milogenius * @date 2020/5/17 20:52 */
@Configuration
public class AnnotationConfig {
    
    @Bean
    public Car car(Engine engine) {
        return new Car(engine);
    }

    @Bean
    public Engine engine(Camshaft camshaft, Crankshaft crankshaft) {
        return new CombustionEngine(camshaft, crankshaft);
    }

    @Bean
    public Camshaft camshaft() {
        return new Camshaft();
    }

    @Bean
    public Crankshaft crankshaft() {
        return new Crankshaft();
    }
}

复制代码

接下来,咱们建立一个 ApplicationContext 对象,从 ApplicationContext 对象获取一个 Car 对象,而后在建立的 Car 对象上调用 start 方法:

ApplicationContext context = 

    new AnnotationConfigApplicationContext(AnnotationConfig.class);

Car car = context.getBean(Car.class);

car.start();

复制代码

执行结果以下:

Started combustion engine

复制代码

虽然@Configuration@Bean 注解的组合为 Spring 提供了足够的信息来执行依赖注入,但咱们仍然须要手动手动定义每一个将被注入的 bean,并显式地声明它们的依赖关系。 为了减小配置 DI 框架所需的开销,Spring 提供了基于java的自动配置。

3.2 基于 java 的自动配置

为了支持基于 java 的自动配置,Spring 提供了额外的注解。 虽然咱们平时可能加过不少这种类型的注解,可是有三个最基本的注解:

  1. @Component: 注册为由 Spring 管理的类
  2. @Autowired: 指示 Spring 注入一个依赖对象
  3. @ComponentScan: 指示Spring在何处查找带有@Component注解的类

3.2.1 构造函数注入

@Autowired注解用来指导 Spring ,咱们打算在使用注解的位置注入一个依赖对象。 例如,在 Car 构造函数中,咱们指望注入一个 Engine 对象,所以,咱们给 Car 构造函数添加@Autowired注解。 经过使用@Component@Autowired注解改造咱们Car类,以下所示:

@Component
public class Car {

  private Engine engine;

 
  @Autowired

  public Car(Engine engine) {

      this.engine = engine;
  }

  public void start() {

      engine.turnOn();

  }

}

复制代码

咱们能够在其余类中重复这个过程:

@Component

public class Camshaft {}

@Component
public class Crankshaft {}


@Component

public class CombustionEngine implements Engine {

 

 private Camshaft camshaft;

 private Crankshaft crankshaft;


 @Autowired

 public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {

     this.camshaft = camshaft;

     this.crankshaft = crankshaft;

 }


 @Override

 public void turnOn() {

     System.out.println("Started combustion engine");

 }

}

复制代码

改造完成相关类以后,咱们须要建立一个@Configuration 类来指导 Spring 如何自动配置咱们的应用程序。 对于基于 java 的基本配置,咱们明确指示 Spring 如何使用@Bean 注解建立每一个 bean,但在自动配置中,咱们已经经过@Component@Autowired 注解提供了足够的信息,说明如何建立所需的全部 bean。 惟一缺乏的信息是 Spring 应该在哪里寻找咱们的带有@Component注解的 类,并把它注册为对应的bean。

@ Componentscan 注释包含一个参数 basePackages,它容许咱们将包名称指定为一个 StringSpring 将经过递归搜索来查找@Component 类。 在咱们的示例中,包是 com.milo.domain,所以,咱们获得的配置类是:

@Configuration
@ComponentScan(basePackages = "com.milo.domain")
public class AutomatedAnnotationConfig {}

复制代码
ApplicationContext context = 

    new AnnotationConfigApplicationContext(AutomatedAnnotationConfig.class);

Car car = context.getBean(Car.class);   

car.start();

复制代码

执行结果:

Started combustion engine

复制代码

经过和基于java的基础配置比较,咱们发现基于 java 的自动配置方法有两个主要优势:

  1. 所需的配置要简洁得多
  2. 注解直接应用于类,而不是在配置类

因此无特殊状况,自动配置是首选

3.2.2 字段注入

除了构造函数注入,咱们还能够经过字段直接注入。 咱们能够将@Autowired 注解应用到所需的字段来实现这一点:

@Component
public class Car {


  @Autowired

  private Engine engine;

  

  public void start() {

      engine.turnOn();

  }

}

复制代码

这种方法极大地减小了咱们的编码压力,可是它也有一个缺点,就是在使用字段以前,咱们将没法检查自动注入的对象是否为空。

3.2.3 Setter注入

构造函数注入的最后一种替代方法是 setter 注入,其中@Autowired 注解应用于与字段关联的 setter。 例如,咱们能够改变 Car 类,经过 setter 注入得到 Engine 对象,方法是用@Autowired注解 setEngine 方法:

@Component
public class Car {


  private Engine engine;

  

  public void start() {

      engine.turnOn();

  }


  public Engine getEngine() {

      return engine;

  }


  @Autowired

  public void setEngine(Engine engine) {

      this.engine = engine;

  }

}

复制代码

Setter 注入相似于字段注入,但它容许咱们与 注入对象交互。 在有些状况下,setter 注入可能特别有用,例如具备循环依赖关系,但 setter 注入多是三种注入技术中最不常见的,尽量优先使用构造函数注入

四 基于 xml 的配置

另外一种配置方法是基于 xml 的配置。 咱们在 XML 配置文件中定义 bean 以及它们之间的关系,而后指示 Spring 在哪里找到咱们的配置文件。

第一步是定义 bean。 咱们基本遵循与基于 java 的基本配置相同的步骤,但使用 xmlbean 元素代替。 在 XML 的状况下,咱们还必须显式地声明咱们打算使用 constructor-arg 元素注入到其余构造函数中的 bean。 结合 beanconstructor-arg 元素,咱们获得如下 XML 配置:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">


  <bean id="car" class="com.milo.domain.Car">

      <constructor-arg ref="engine" />

  </bean>

  

  <bean id="engine" class="com.milo.CombustionEngine">

      <constructor-arg ref="camshaft" />

      <constructor-arg ref="crankshaft" />

  </bean>

  

  <bean id="camshaft" class="com.milo.Camshaft" />

  <bean id="crankshaft" class="com.milo.Crankshaft" />


</beans>

复制代码

在 bean 元素中,咱们必须指定两个属性:

  1. id : bean 的惟一 ID (至关于带有@Bean 注解方法名)
  2. class : 类的全路径(包括包名)

对于 constructor-arg 元素,咱们只须要指定 ref 属性,它是对现有 bean ID 的引用。 例如,元素构造函数 <constructor-arg ref="engine" /> 规定,具备 ID engine(直接定义在 car bean 之下)的 bean 应该被用做注入 car bean 构造函数的 bean。

构造函数参数的顺序由 constructor-arg 元素的顺序决定。 例如,在定义 engine bean 时,传递给 CombustionEngine 构造函数的第一个构造函数参数是 camshaft bean,而第二个参数是 crankshaft bean。

获取ApplicationContext对象,咱们只需修改 ApplicationContext 实现类型。 由于咱们将 XML 配置文件放在类路径上,因此咱们使用 ClassPathXmlApplicationContext:

ApplicationContext context = 

    new ClassPathXmlApplicationContext("basic-config.xml");

Car car = context.getBean(Car.class);

car.start();

复制代码

执行结果:

Started combustion engine

复制代码

五 常见问题

如今,咱们已经摸清了Spring框架如何进行DI,并正确地将全部依赖关系注入到咱们的应用程序中,可是咱们必须处理两个棘手的问题:

  1. 依赖对象冲突
  2. 依赖对象间存在循环依赖

5.1 具备多个符合条件的依赖对象

在基于 java 和基于 xml 的方法中,咱们已经指示 Spring 只使用 CombustionEngine 做为咱们的Engine实现。 若是咱们将ElectricEngine注册为符合 di 标准的部件会发生什么? 为了测试结果,咱们将修改基于 java 的自动配置示例,并用@Component 注解 ElectricEngine 类:

@Component
public class ElectricEngine implements Engine {


  @Override

  public void turnOn() {

      System.out.println("Started electric engine");

  }

}

复制代码

若是咱们从新运行基于 java 的自动配置应用程序,咱们会看到如下错误:

No qualifying bean of type 'com.dzone.albanoj2.spring.di.domain.Engine' available: expected single matching bean but found 2: combustionEngine,electricEngine

复制代码

因为咱们已经注释了用@Component 实现 Engine 接口的两个类ーー即 CombustionEngineElectricEngine ーー spring 如今没法肯定在实例化 Car 对象时应该使用这两个类中的哪个来知足 Engine 依赖性。 为了解决这个问题,咱们必须明确地指示 Spring 使用这两个 bean 中的哪个。

5.1.1 @ Qualifier 注解

一种方法是给咱们的依赖对象命名,并在应用@Autowired注解的地方使用@Qualifier注解来肯定注入哪个依赖对象。 因此,@Qualifier 注解限定了自动注入的 bean,从而将知足需求的 bean 数量减小到一个。 例如,咱们能够命名咱们的CombustionEngine依赖对象:

@Component("defaultEngine")
public class CombustionEngine implements Engine {

    

    // ...代码省略,未改变

}

复制代码

而后咱们能够添加@Qualifier 注解,其名称和咱们想要注入的依赖对象的名称保持一致,这样,咱们Engine 对象在 Car 构造函数中被自动注入

@Component
public class Car {

  

  @Autowired

  public Car(@Qualifier("defaultEngine") Engine engine) {

      this.engine = engine;

  }

  

  // ...existing implementation unchanged...

}

复制代码

若是咱们从新运行咱们的应用程序,咱们再也不报之前的错误:

Started combustion engine

复制代码

注意,若是没有显式申明bean名称的类都有一个默认名称,该默认名称就是类名首字母小写。 例如,咱们的 Combusttionengine 类的默认名称是 combusttionengine

5.1.2 @ Primary 注解

若是咱们知道默认状况下咱们更喜欢一个实现,那么咱们能够放弃@Qualifier 注释,直接将@Primary 注释添加到类中。 例如,咱们能够将咱们的 CombusttionengineElectricEngineCar 类更改成:

@Component
@Primary

public class CombustionEngine implements Engine {

  

   // ...existing implementation unchanged...

}

@Component
public class ElectricEngine implements Engine {

  

    // ...existing implementation unchanged...

}


@Component
public class Car {

 

 @Autowired

 public Car(Engine engine) {

     this.engine = engine;

 }

 

 // ...existing implementation unchanged...

}

复制代码

咱们从新运行咱们的应用程序,咱们会获得如下输出:

Started combustion engine

复制代码

这证实,虽然有两种可能性知足 Engine 依赖性,即 CombustionEngineElectricengine,但 Spring 可以根据@Primary 注释决定两种实现中哪种应该优先使用。

5.2 循环依赖

虽然咱们已经深刻讨论了 Spring DI 的基础知识,可是还有一个主要问题没有解决: 若是依赖关系树有一个循环引用会发生什么? 例如,假设咱们建立了一个 Foo 类,它的构造函数须要一个 Bar 对象,可是 Bar 构造函数须要一个 Foo 对象。

咱们可使用代码实现上面问题:

@Component
public class Foo {



  private Bar bar;


  @Autowired

  public Foo(Bar bar) {

      this.bar = bar;

  }

}


@Component
public class Bar {



 private Foo foo;


 @Autowired

 public Bar(Foo foo) {

     this.foo = foo;

 }

}

复制代码

而后咱们能够定义如下配置:

@Configuration
@ComponentScan(basePackageClasses = Foo.class)
public class Config {}

复制代码

最后,咱们能够建立咱们的 ApplicationContext:

ApplicationContext context = 

    new AnnotationConfigApplicationContext(Config.class);

Foo foo = context.getBean(Foo.class);

复制代码

当咱们执行这个代码片断时,咱们看到如下错误:

Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'bar': Requested bean is currently in creation: Is there an unresolvable circular reference?

复制代码

首先,Spring 尝试建立 Foo 对象。 在这个过程当中,Spring 认识到须要一个 Bar 对象。 为了构造 Bar 对象,须要一个 Foo 对象。 因为 Foo 对象目前正在构建中(这也是建立 Bar 对象的缘由) ,spring 认识到可能发生了循环引用。

这个问题最简单的解决方案之一是在一个类和注入点上使用@Lazy注解。 这指示 Spring 推迟带注解的 bean 和带注释的@Autowired 位置的初始化。 这容许成功地初始化其中一个 bean,从而打破循环依赖链。 理解了这一点,咱们能够改变 FooBar 类:

@Component
public class Foo {

  

  private Bar bar;


  @Autowired

  public Foo(@Lazy Bar bar) {

      this.bar = bar;

  }

}


@Component

@Lazy

public class Bar {


  @Autowired

  public Bar(Foo foo) {}

}

复制代码

若是使用@Lazy 注解后从新运行应用程序,没有发现报告任何错误。

六 总结

在本文中,咱们探讨了 Spring 的基础知识,包括 IoCDISpring ApplicationContext。 而后,咱们介绍了使用基于 java 的配置和基于 xml 的配置建立 Spring 应用程序的基本知识,同时研究了使用 Spring DI 时可能遇到的一些常见问题。 虽然这些概念一开始可能晦涩难懂,与 Spring 代码脱节,可是咱们能够从基底层认识Spirng,但愿对你们有所帮助,谢谢你们。

相关文章
相关标签/搜索