据说,80%的人没有真正理解了Spring的依赖注入(下)

简介: 据说,80%的人没有真正理解了Spring的依赖注入(下)

我们可以在其他类中重复这个过程:


@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. 所需的配置要简洁得多


  1. 注解直接应用于类,而不是在配置类


所以无特殊情况,自动配置是首选


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` 注解方法名)


  1. 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. 依赖对象冲突


  1. 依赖对象间存在循环依赖


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 对象。


image.png


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


@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,希望对大家有所帮助,谢谢大家。


END

目录
打赏
0
0
0
0
23
分享
相关文章
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
本文详细介绍了Spring框架的核心功能,并通过手写自定义Spring框架的方式,深入理解了Spring的IOC(控制反转)和DI(依赖注入)功能,并且学会实际运用设计模式到真实开发中。
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
轻松掌握Spring依赖注入:打造你的登录验证系统
本文以轻松活泼的风格,带领读者走进Spring框架中的依赖注入和登录验证的世界。通过详细的步骤和代码示例,我们从DAO层的创建到Service层的实现,再到Spring配置文件的编写,最后通过测试类验证功能,一步步构建了一个简单的登录验证系统。文章不仅提供了实用的技术指导,还以口语化和生动的语言,让学习变得不再枯燥。
87 2
Spring Boot中的依赖注入和控制反转
Spring Boot中的依赖注入和控制反转
简单了解下Spring中的各种Aware接口实现依赖注入
在Spring框架中,Aware接口是一组用于提供特定资源或环境信息的回调接口。这些接口被设计用来允许Bean获取对Spring容器或其他相关资源的引用,并在需要时进行适当的处理。
83 2
彻底改变你的编程人生!揭秘 Spring 框架依赖注入的神奇魔力,让你的代码瞬间焕然一新!
【8月更文挑战第31天】本文介绍 Spring 框架中的依赖注入(DI),一种降低代码耦合度的设计模式。通过 Spring 的 DI 容器,开发者可专注业务逻辑而非依赖管理。文中详细解释了 DI 的基本概念及其实现方式,如构造器注入、字段注入与 setter 方法注入,并提供示例说明如何在实际项目中应用这些技术。通过 Spring 的 @Configuration 和 @Bean 注解,可轻松定义与管理应用中的组件及其依赖关系,实现更简洁、易维护的代码结构。
132 0
Spring循环依赖问题之Spring不支持构造器内的强依赖注入如何解决
Spring循环依赖问题之Spring不支持构造器内的强依赖注入如何解决
103 2
简单了解下Spring中的各种Aware接口实现依赖注入
【8月更文挑战第21天】在Spring框架中,Aware接口系列是一种特殊的机制,它允许Bean在初始化过程中获取到Spring容器或容器中的特定资源,从而实现了更加灵活和强大的依赖注入方式。本文将围绕Spring中的各种Aware接口,详细探讨它们如何帮助开发者在工作和学习中更好地实现依赖注入。
205 0
深入理解Spring中的依赖注入原理
深入理解Spring中的依赖注入原理
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等