上篇文章中我们介绍了准备Spring源码阅读环境的两种姿势,接下来,我们就要开始探寻这个著名框架背后的原理。Spring提供的最基本最底层的功能是bean容器,这其实是对IoC思想的应用,在学习Spring容器的实现原理之前,我们有必要先来了解一下什么是IoC,这就是本文的重点。
1. IoC
IoC是随着近年来轻量级容器(Lightweight Container)的兴起而逐渐被很多人提起的一个名词,它的全称为Inversion of Control,中文通常译为控制反转。好莱坞原则"Don’t call us, we will call you."恰如其分地表达了“反转”的意味,是用来形容IoC最多的一句话。
那么,为什么需要IoC?IoC的具体意义是什么?它到底有什么独到之处?让我们带着这些疑问开始我们的IoC之旅吧。
为了更好的阐述IoC模式的概念,我们引入一下简单场景: 在经典的MVC项目中,Controller一般依赖多个Service来完成不同的业务逻辑,这里假设为serviceA和serviceB,如下代码所示:
// 示例代码1
public class ControllerA{
private ServiceA serviceA;
private ServiceB serviceB; public void doSomething(){ serviceA.doSomething(); serviceB.doSomething(); }
}
通常情况下,我们可以直接在构造函数中构造这两个类,如下所示:
// 示例代码2
public ControllerA{
serviceA = new ServiceA();
serviceB = new ServiceB();
}
如果我们依赖于某个类或服务,最简单而有效的方式就是直接在类的构造函数中新建相应的依赖类。我们是自己主动地去获取依赖的对象!
可是回头想想,我们自己每次用到什么依赖对象都要主动地去获取,这是否真的必要?我们最终所要做的,其实就是直接调用依赖对象所提供的某项服务而已。只要用到这个依赖对象的时候,它能够准备就绪,我们完全可以不管这个对象是自己找来的还是别人送过来的。如果有人能够在我们需要时将某个依赖对象送过来,为什么还要大费周折地自己去折腾?
实际上,IoC就是为了帮助我们避免之前的“大费周折”,而提供了更加轻松简洁的方式。它的反转,就反转在让你从原来的事必躬亲,转变为现在的享受服务。最简单的一种方式,体现在代码中,可以如下所示:
// 示例代码3
public ControllerA(ServiceA serviceA,ServiceB serviceB){
this.serviceA = serviceA;
this.serviceB = serviceB;
}
通常情况下,被注入对象(ControllerA)会直接依赖于被依赖对象(ServiceA、ServiceB),也就是如上示例代码2中在构造函数中写死的情况。但是, 在IoC的场景中,二者之间是通过一个IoC Service Provider来打交道,所有的被注入对象和依赖对象由IoC Service Provider统一管理。体现在示例代码3中,是通过构造器传参来完成注入的(实际还可以通过setter、注解等方式完成注入)。
如果增加新的需求,我们只需要增加一个实现了ServiceA的类就可以了,而不用重新增加一个Controller,而对于示例代码2,我们就不得不重写之前的所有代码了。这就是IoC的好处!
如果要用一句话来概括IoC可以带给我们什么,那么我希望是,IoC是一种可以帮助我们解耦各业务对象间依赖关系的对象绑定方式!
控制反转(IoC),体现在哪里,我觉得可以理解为:
如果我们需要一个对象,从我们自己new出来转变成从IoC容器直接获取已经创建好的;
某个对象依赖于别的对象,从对象在自己的构造器中new依赖的对象(示例代码2所示)转变成由IoC容器来注入依赖的对象(示例代码3所示);
也许你会问,我也可以让IoC容器来调用示例代码2中的构造器来完成帮我注入啊,嗯,陈独秀同学,你挡到后面的同学了!
2. DI
上一节讲了IoC的基本思想,在IoC模式中,依赖对象是需要通过IoC容器来帮助注入的,这就是依赖注入(Dependency Injection,简称DI)。而被注入对象总要通过某种方式来通知IoC容器为其提供相应服务吧,这里总结一下:
- 构造方法注入;
- setter方法注入;
- 接口注入;
2.1 构造方法注入
顾名思义,构造方法注入就是被注入对象可以通过在其构造方法中声明依赖对象的参数列表,让外部(通常是IoC容器)知道它需要哪些依赖对象,具体例子参考如上示例代码3。
构造方法注入比较直观,对象被构造完成后即可进入就绪状态,可以马上使用。
2.2 setter方法注入
对于JavaBean对象来说,通常会通过setXXX()和getXXX()方法来访问对应属性。这些setXXX()方法统称为setter方法,通过setter方法可以更改相应的对象属性。所以,某个对象只要为其依赖对所对应的属性添加setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中,如下简单示例:
// 示例代码1
public class ControllerA{
private ServiceA serviceA;
private ServiceB serviceB; public void setA(ServiceA serviceA){
this.serviceA = serviceA;
} public void setB(ServiceB serviceB){
this.serviceB = serviceB;
}
}
2.3 接口注入
相对于前两种注入方式来说,接口注入没有那么简单明了。被注入对象如果想要IoC容器为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。IoC容器最终通过这些接口来了解应该为被注入对象注入什么依赖对象,最典型的要数如BeanNameAware、BeanFactoryAware等接口了,客户对象实现了这些接口之后,容器会自动为其注入相应的资源如beanName、beanFactory。
2.4 三种注入方式的比较
构造方法注入。这种注入方式的优点是,对象在构造完成之后就已进入就绪状态,可以马上使用。缺点就是,当依赖对象较多时,构造方法的参数列表会比较长。而通过反射构造对象的时候,对相同类型的参数的处理会比较困难(这个在后面阅读源码时就能够体会到),维护和使用上也比较麻烦。而且在Java中,构造方法无法被继承,无法设置默认值。对于非必须的依赖处理,可能需要引入多个构造方法,而参数数量的变动也可能造成维护上的不便。
setter方法注入。setter方法可以被继承,而且有良好的IDE支持。缺点是对象无法在构造完成后马上进入就绪状态。
接口注入。从注入方式的使用上来说,接口注入是现在不甚提倡的一种方式,因为它强制被注入对象实现不必要的接口,带有侵入性,而构造方法注入和setter方法注入则不需要如此。
综上,构造方法注入和setter方法注入因为其侵入性较弱,且易于理解和使用,所以是现在主流的注入方式;而接口注入因为侵入性较强,目前已经不流行了,仅仅Spring框架保留了部分接口注入,如BeanNameAware接口等。
3. IoC 容器
虽然业务对象可以通过IOC方式声明相应的依赖,但是最终仍然需要通过某种角色或者服务将这些相互依赖的对象绑定到一起,而IoC容器(IoC Service Provider)就是对于IoC场景中的这一角色。
IoC Service Provider只是一个抽象出来的概念,它可以指代任何将IoC场景中的业务对象绑定到一起的实现方式,可以是一段代码,也可以是一组相关的类,还可以是比较通用的IoC框架或者IoC容器实现,常见的比如Spring。
IoC容器的职责相对来说比较简单,主要有两个:业务对象的构建管理和业务对象之间的依赖绑定。
业务对象的构建管理。在IoC场景中,业务对象无需关心所依赖的对象如何构建如何取得,但这部分工作始终需要完成,所以,IoC容器就承担了这个任务,从而避免这部分逻辑污染业务对象的实现。
业务对象间的依赖绑定。对于IoC容器来说,这个职责是最艰巨也是最重要的,这是它的使命所在。IoC容器通过结合之前构建和管理的所有业务对象,以及各个业务对象间可以识别的依赖关系,将这些对象所依赖对象注入绑定,从而保证每个业务对象在使用的时候可以处于就绪状态。
4. 总结
本文主要介绍了IoC(控制反转)和DI(依赖注入)的概念,并探索验证了IoC所带给我们的部分"附加值"。在此基础上介绍了常见IoC容器Spring的基本职责,从而对IoC即IoC容器有了最基本认识。