前言
提起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("燃油引擎启动"); } }

