我们可以在其他类中重复这个过程:
@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
,它允许我们将包名称指定为一个 String
,Spring
将通过递归搜索来查找@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
的自动配置方法有两个主要优点:
- 所需的配置要简洁得多
- 注解直接应用于类,而不是在配置类
所以无特殊情况,自动配置是首选
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
。结合 bean
和 constructor-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 元素中,我们必须指定两个属性:
id
: bean 的唯一 ID (相当于带有`@Bean` 注解方法名)
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
,并正确地将所有依赖关系注入到我们的应用程序中,但是我们必须处理两个棘手的问题:
- 依赖对象冲突
- 依赖对象间存在循环依赖
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
接口的两个类ーー即 CombustionEngine
和ElectricEngine
ーー 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
注释添加到类中。例如,我们可以将我们的 Combusttionengine
、 ElectricEngine
和 Car
类更改为:
@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
依赖性,即 CombustionEngine
和 Electricengine
,但 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,从而打破循环依赖链。理解了这一点,我们可以改变 Foo
和 Bar
类:
@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
的基础知识,包括 IoC
、 DI
和 Spring ApplicationContext
。然后,我们介绍了使用基于 java
的配置和基于 xml
的配置创建 Spring
应用程序的基本知识,同时研究了使用 Spring DI
时可能遇到的一些常见问题。虽然这些概念一开始可能晦涩难懂,与 Spring
代码脱节,但是我们可以从基底层认识Spirng
,希望对大家有所帮助,谢谢大家。
END