IoC(Inversion of Control,控制反转)也称为依赖注入(Dependency Injection),作为Spring的一个核心思想,是一种设计对象之间依赖关系的原则及其相关技术。IOC的思想最核心的地方在于,资源不由使用资源的双方管理,而由不使用资源的第三方管理。这可以带来很多好处:第一,资源集中管理,实现资源的可配置和易管理。第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。
IoC理论的背景
在采用面向对象方法设计的软件系统中,它的底层实现都是由N个对象组成的,所有的对象通过彼此的合作,最终实现系统的业务逻辑。
软件系统中耦合的对象 对象之间复杂的依赖关系 齿轮组中齿轮之间的啮合关系,与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。 伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。 耦合关系不仅会出现在对象与对象之间,也会出现在软件系统的各模块之间,以及软件系统和硬件系统之间。如何降低系统之间、模块之间和对象之间的耦合度,是软件工程永远追求的目标之一。为了解决对象之间的耦合度过高的问题,软件专家Michael Mattson提出了IOC理论,用来实现对象之间的“解耦”,目前这个理论已经被成功地应用到实践当中,很多的J2EE项目均采用了IOC框架产品spring。
Iteye 开涛对Ioc的讲解
首先要分享的是Iteye的开涛这位技术牛人对Spring框架的IOC的理解,以下内容全部来自原文。原文地址
IoC是什么
- 谁控制谁,控制什么
传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。- 为何是反转,哪些方面反转了
有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
传统应用程序示意图 有IoC/DI容器后程序结构示意图 IoC能做什么
IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。
IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。IoC和DI
DI—Dependency Injection,即“依赖注入”:是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:
- 谁依赖于谁:当然是应用程序依赖于IoC容器;
- 为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;
- 谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
- 注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
IoC和DI由什么关系呢?其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。
个人理解
IoC
在封建主义社会。当一个家庭(调用者)需要粮食(被调用者)时只有自己去耕种。
对应JAVA中:调用类需要被调用类的实例,只有通过关键字New去创建。该模式下代码高度耦合。会出现扩展性差、各模块职责不清晰等情况。在资本主义社会。粮食(被调用者)可不由家庭(调用者)去生产,而转为农场(第三方容器)统一耕种,家庭的人不需要关心粮食的耕种细节。当需要粮食的时候通过地址去农场购买即可。
在社会主义社会。当家庭(调用者)想要粮食(被调用者),无需主动去找农场(第三方容器),只需通过电话,农场主动派人将粮食送到家庭中。
对应JAVA中:在调用类无需引入工厂类地址,更不用关心调用类的实例化,只需spring的注入即可。该模式下调用者不关心被调者的具体实现,更不用主动定位工厂,实现了最大程度的解耦。DI
电脑主机和USB接口来实现一个任务:从外部USB设备读取一个文件。电脑主机读取文件的时候,它一点也不会关心USB接口上连接的是什么外部设备,而且它确实也无须知道。它的任务就是读取USB接口,挂接的外部设备只要符合USB接口标准即可。所以,如果我给电脑主机连接上一个U盘,那么主机就从U盘上读取文件;如果我给电脑主机连接上一个外置硬盘,那么电脑主机就从外置硬盘上读取文件。挂接外部设备的权力由我作主,即控制权归我,至于USB接口挂接的是什么设备,电脑主机是决定不了,它只能被动的接受。当电脑主机读取文件的时候,我就把它所要依赖的外部设备,帮他挂接上。整个外部设备注入的过程和一个被依赖的对象在系统运行时被注入另外一个对象内部的过程完全一样。
对应JAVA中:对象A依赖于对象B,当对象 A需要用到对象B的时候,IOC容器就会立即创建一个对象B送给对象A。IOC容器就是一个对象制造工厂,你需要什么,它会给你送去,你直接使用就行了,而再也不用去关心你所用的东西是如何制成的,也不用关心最后是怎么被销毁的,这一切全部由IOC容器包办。
注入方式
构造器注入
就是容器实例化Bean时注入那些依赖,通过在在Bean定义中指定构造器参数进行注入依赖,包括实例工厂方法参数注入依赖,但静态工厂方法参数不允许注入依赖;
注入方式 注入配置方式示例 参数索引注入 参数类型注入 参数名注入 测试构造器类HelloImpl3.java,该类只有一个包含两个参数的构造器:
package cn.javass.spring.chapter3.helloworld; public class HelloImpl3 implements HelloApi { private String message; private int index; public HelloImpl3(String message, int index) { this.message = message; this.index = index; } @Override public void sayHello() { System.out.println(index + ":" + message); } }
相关配置:
<bean id="byIndex" class="cn.javass.spring.chapter3.HelloImpl3"> <constructor-arg index="0" value="Hello World!"/> <constructor-arg index="1" value="1"/> </bean> <!-- 通过构造器参数类型方式依赖注入 --> <bean id="byType" class="cn.javass.spring.chapter3.HelloImpl3"> <constructor-arg type="java.lang.String" value="Hello World!"/> <constructor-arg type="int" value="2"/> </bean> <!-- 通过构造器参数名称方式依赖注入 --> <bean id="byName" class="cn.javass.spring.chapter3.HelloImpl3"> <constructor-arg name="message" value="Hello World!"/> <constructor-arg name="index" value="3"/> </bean>
setter注入
setter注入,是通过在通过构造器、静态工厂或实例工厂实例好Bean后,通过调用Bean类的setter方法进行注入依赖
注入方式 注入配置方式示例 setter注入方式 (1)准备测试类HelloImpl4,需要两个setter方法“setMessage”和“setIndex”:
package cn.javass.spring.chapter3; import cn.javass.spring.chapter2.helloworld.HelloApi; public class HelloImpl4 implements HelloApi { private String message; private int index; //setter方法 public void setMessage(String message) { this.message = message; } public void setIndex(int index) { this.index = index; } @Override public void sayHello() { System.out.println(index + ":" + message); } }
配置Bean定义,具体配置文件(resources/chapter3/setterDependencyInject.xml)片段如下:
<!-- 通过setter方式进行依赖注入 --> <bean id="bean" class="cn.javass.spring.chapter3.HelloImpl4"> <property name="message" value="Hello World!"/> <property name="index"> <value>1</value> </property> </bean>
(3)该写测试进行测试一下是否满足能工作了,其实测试代码一点没变,变的是配置:
@Test public void testSetterDependencyInject() { BeanFactory beanFactory = new ClassPathXmlApplicationContext("chapter3/setterDependencyInject.xml"); HelloApi bean = beanFactory.getBean("bean", HelloApi.class); bean.sayHello(); }
两种注入方式的比较
在过去的开发过程中,这两种注入方式都是非常常用的。Spring也同时支持两种依赖注入方式:设值注入和构造注入。 这两种依赖注入的方式,并没有绝对的好坏,只是适应的场景有所不同。
建议:采用以设值注入为主,构造注入为辅的注入策略。对于依赖关系无需变化的注入,尽量采用构造注入;而其他的依赖关系的注入,则考虑采用设值注入。设值注入有如下优点
- 设值注入需要该Bean包含这些属性的setter方法;
- 与传统的JavaBean的写法更相似,程序开发人员更容易理解、接收。通过setter方法设定依赖关系显得更加只管;
- 对于复杂的依赖关系,如果采用构造注入,会导致构造器国语臃肿,难以阅读。Spring在创建Bean实例时,需要同时实例化器依赖的全部实例,因而导致性能下降。而使用设值注入,则能避免这些问题;
- 尤其是在某些属性可选的情况况下,多参数的构造器显得更加笨重;
构造注入有以下优势
- 构造注入需要该Bean包含带有这些属性的构造器;
- 构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入。例如,组件中其他依赖关系的注入,常常要依赖于DataSrouce的注入。采用构造注入,可以在代码中清晰的决定注入顺序;
- 对于依赖关系无需变化的Bean,构造注入更有用处。因为没有Setter方法,所有的依赖关系全部在构造器内设定。因此,无需担心后续的代码对依赖关系产生破坏;
- 依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则;