前言
提起Spring
,大家肯定不陌生,它是每一个Java开发者绕不过去的坎。Spring
框架为基于 java
的企业应用程序提供了一整套解决方案,方便开发人员在框架基础快速进行业务开发。
在官网中,我们发现它的核心技术之一:Dependency Injection
,简称:DI
,翻译过来就是依赖注入。今天我们就来盘一盘它。
在本文中,我们将深入研究 Spring
框架 DI
背后的故事,包括 Spring Inversion of Control
(控制反转)、 DI
和 ApplicationContext
接口。基于这些基本概念,我们将研究如何使用基于 java
和基于 XML
的配置来 创建Spring
应用程序。最后,我们将探讨在创建 Spring
应用程序时遇到的一些常见问题,包括 bean冲突和循环依赖性。
一 控制反转(Inversion of Control)
在学习DI
之前,我们先学习一下 IoC
(控制反转),接下来的一段可能读起来会让你感觉比较啰嗦,但是要细细体会每一次改变的意图,和我们的解决方案,对于理解控制反转非常重要。
首先来了解下我们通常实例化一个对象的方式。在 平时,我们使用 new
关键字实例化一个对象。例如,如果有一个 Car
类,我们可以使用以下方法实例化一个对象 Car
Car car = new Car();
因为汽车有很多零部件组成,我们定义Engine
接口来模拟汽车引擎,然后将engine
对象作为成员变量放在Car
类
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
接口的实现类
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
对象
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(); } }
通过上面的一顿骚操作,我们成功的解决了我们引擎的问题。如果是一个日常需求,我们已经可以成功交工了。但是这显然不是我写这篇文章的目的。
从设计的角度来说,目前的代码是糟糕的,有以下两点原因:
- 在两个不同的类中,存在重复的
start()
方法;
- 我们需要为每个新的
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(); } }
电动引擎启动
燃油引擎启动
那么我们该如何解决我们提出的第二个问题那?
其实这个问题我们可以换个角度看:为什么我们要去关注CombustionCar
和ElectricCar
,我们现在将关注点回到我们的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
不需要参数,因为在它的构造函数里面已经为我们new
了Engine
对象。使用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("燃油引擎启动"); } }