深入理解 Spring 控制反转与依赖注入

概览

对于 Spring 框架来说,控制反转(Inversion of Control, IoC)和依赖注入(Dependency Injection, DI)是个等同的概念,控制反转是通过依赖注入实现的。在这篇文章中,我们会详细介绍 IoC 和 DI 的概念,然后我们会讨论 Spring 框架中是如何实现 IoC 和 DI 的。

什么是控制反转

IoC 是软件工程的一个设计原则,是指对象的依赖由框架来控制,而不是由对象的提供者来控制。
IoC 可以通过多种机制来实现,例如,策略设计模式,依赖注入(DI)等。本文我们主要介绍如何通过 DI 来实现控制反转。

什么是依赖注入

一般来说,每个 Java 应用程序都有几个对象,这些对象一起工作以提供系统的功能。当实现复杂的 Java 应用程序时,Java 对象之间应尽可能独立以增加这些对象的可复用性。依赖注入有助于把这些对象粘合在一起,同时保持它们的独立。
依赖注入是 IoC 的一种实现方式,为理解依赖注入概念,我们先来看一下传统的对象依赖是如何实现的。
假设我们有一个包含文本编辑器的应用程序,文本编辑器提供拼写检查功能,传统的代码看起来是这样的:

1
2
3
4
5
6
public class TextEditor {
private SpellChecker spellChecker;
public TextEditor() {
spellChecker = new SpellChecker();
}
}

在这里,我们创建了TextEditorSpellChecker的依赖关系。如果使用了 IoC,代码可以写成这样子:

1
2
3
4
5
6
public class TextEditor {
private SpellChecker spellChecker;
public TextEditor(SpellChecker spellChecker) {
this.spellChecker = spellChecker;
}
}

TextEditor实例化时,通过在构造函数对实例变量spellChecker赋值,我们创建了TextEditorSpellChecker的依赖关系,这个依赖关系的建立是通过构造函数注入到TextEditor中的。

1
2
SpellChecker sc = new SpellChecker; // dependency
TextEditor textEditor = new TextEditor(sc);

上面我们采用类的使用者来实现依赖注入,对于 Spring 框架来说,依赖注入是由 Spring IoC 容器来实现的。

Spring IoC 容器

IoC 容器是 Spring 框架最基础的设施,对于 Spring 框架来说,接口ApplicationContext代表 IoC 容器。Spring IoC 容器负责创建 Bean(对象),通过容器将功能类 Bean 注入到你需要的 Bean 中。
Spring 框架提供了多个接口ApplicationContext的实现,包括ClassPathXmlApplicationContextFileSystemXmlApplicationContext,以及WebApplicationContext

Spring IoC 容器使用 xml 配置,或者注解配置来实现 Bean 的创建与注入。这些 xml 配置和注解配置,称为配置元数据,所谓元数据是指描述数据的数据。元数据本身不具备可执行的能力,只能通过外界代码来对这些元数据解析后进行一些有意义的操作。

可以使用下方法可以初始化一个 IoC 容器。

1
2
ApplicationContext context =
new ClassPathXmlApplicationContext("Beans.xml");

Spring 实现依赖注入

Spring 可以通过构造函数,设值函数,等方法来实现依赖注入。

基于构造函数的依赖注入

当容器调用带有一组参数的构造函数时,基于构造函数的 DI 就完成了,其中每个参数代表一个对其他对象的依赖。
我们以一个完整的例子来说明 Spring IoC 容器是如何利用构造函数来实现 DI 的。

使用 IntelliJ IDEA 创建一个 Maven 项目,并添加以下依赖。

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>

添加SpellChecker.java

1
2
3
4
5
6
7
8
9
10
package me.leehao;
public class SpellChecker {
public SpellChecker(){
System.out.println("Inside SpellChecker constructor." );
}
public void checkSpelling() {
System.out.println("Inside checkSpelling." );
}
}

添加TextEditor.java

1
2
3
4
5
6
7
8
9
10
11
12
package me.leehao;
public class TextEditor {
private SpellChecker spellChecker;
public TextEditor(SpellChecker spellChecker) {
System.out.println("Inside TextEditor constructor." );
this.spellChecker = spellChecker;
}
public void spellCheck() {
spellChecker.checkSpelling();
}
}

添加程序入口类MainApp.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package me.leehao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("Beans.xml");
TextEditor te = (TextEditor) context.getBean("textEditor");
te.spellCheck();
}
}

最后我们添加Beans.xml文件,我们使用 xml 配置的方式来实现 Bean 的注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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-3.0.xsd">
<!-- Definition for textEditor bean -->
<bean id="textEditor" class="me.leehao.TextEditor">
<constructor-arg ref="spellChecker"/>
</bean>
<!-- Definition for spellChecker bean -->
<bean id="spellChecker" class="me.leehao.SpellChecker">
</bean>
</beans>

<constructor-arg ref="spellChecker"/>表明textEditor对象通过构造函数来注入spellChecker的依赖。

运行程序,输出:

Inside SpellChecker constructor.
Inside TextEditor constructor.
Inside checkSpelling.

基于设值函数的依赖注入

通过容器在 Bean 上调用设值函数,也可以实现 Bean 的依赖注入。我们以例子来说明。

SpellChecker.java内容如下:

1
2
3
4
5
6
7
8
9
10
package me.leehao;
public class SpellChecker {
public SpellChecker(){
System.out.println("Inside SpellChecker constructor." );
}
public void checkSpelling() {
System.out.println("Inside checkSpelling." );
}
}

TextEditor.java的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package me.leehao;
public class TextEditor {
private SpellChecker spellChecker;
public void setSpellChecker(SpellChecker spellChecker) {
System.out.println("Inside setSpellChecker." );
this.spellChecker = spellChecker;
}
public SpellChecker getSpellChecker() {
return spellChecker;
}
public void spellCheck() {
spellChecker.checkSpelling();
}
}

TextEditor的定义跟上面的例子相比,多了一个setSpellChecker()设值函数。

程序入口MainApp.java的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package me.leehao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("Beans.xml");
TextEditor te = (TextEditor) context.getBean("textEditor");
te.spellCheck();
}
}

Beans.xml的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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-3.0.xsd">
<!-- Definition for textEditor bean -->
<bean id="textEditor" class="me.leehao.TextEditor">
<property name="spellChecker" ref="spellChecker"/>
</bean>
<!-- Definition for spellChecker bean -->
<bean id="spellChecker" class="me.leehao.SpellChecker">
</bean>
</beans>

在基于设值函数的 DI 中,我们改用了property元素。<property name="spellChecker" ref="spellChecker"/>将会调用类TextEditor的设值函数setSpellChecker(),并将 Bean spellChecker注入到 Bean textEditor

运行上面程序,输出:

Inside SpellChecker constructor.
Inside setSpellChecker.
Inside checkSpelling.

小结

本文详细介绍了 IoC 和 DI 的概念,并介绍了在 Spring 框架中是如何通过 DI 来实现 IoC 的,最后以例子说明如何基于构造函数和基于设值函数实现了依赖注入。

参考资料

  1. http://www.baeldung.com/inversion-control-and-dependency-injection-in-spring
  2. https://stackoverflow.com/questions/9403155/what-is-dependency-injection-and-inversion-of-control-in-spring-framework
  3. https://stackoverflow.com/questions/3058/what-is-inversion-of-control?noredirect=1&lq=1
  4. https://www.w3cschool.cn/wkspring/1h9m1h9m.html
  5. Java EE 开发的颠覆者 Spring Boot 实战,汪云飞著,电子工业出版社