Spring是为了解决企业应用开发的复杂性而创建的一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。在这句话中重点有两个,一个是IoC,另一个是AOP。今天我们讲第一个IoC。
一. IoC理论的背景
我们都知道,在采用面向对象方法设计的软件系统中,它的底层实现都是由N个对象组成的,所有的对象通过彼此的合作,最终实现系统的业务逻辑。
如果我们打开机械式手表的后盖,就会看到与上面类似的情形,各个齿轮分别带动时针、分针和秒针顺时针旋转,从而在表盘上产生正确的时间。图1中描述的就是 这样的一个齿轮组,它拥有多个独立的齿轮,这些齿轮相互啮合在一起,协同工作,共同完成某项任务。我们可以看到,在这样的齿轮组中,如果有一个齿轮出了问 题,就可能会影响到整个齿轮组的正常运转。
齿轮组中齿轮之间的啮合关系,与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。现在,伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临 更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。
耦合关系不仅会出现在对象与对象之间,也会出现在软件系统的各模块之间,以及软件系统和硬件系统之间。如何降低系统之间、模块之间和对象之间的耦合度,是软件工程永远追求的目标之一。为了解决对象之间的耦合度过高的问题,软件专家Michael Mattson提出了IOC理论,用来实现对象之间的“解耦”,目前这个理论已经被成功地应用到实践当中,很多的J2EE项目均采用了IOC框架产品Spring。
2. 什么是控制反转(IoC)
IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”,还有些书籍翻译成为“控制反向”或者“控制倒置”。
1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IOC 这个概念。对于面向对象设计及编程的基本思想,前面我们已经讲了很多了,不再赘述,简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依 赖关系的对象之间的解耦,如下图:
大家看到了吧,由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了, 全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合 在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。
我们再来做个试验:把上图中间的IOC容器拿掉,然后再来看看这套系统:
我们现在看到的画面,就是我们要实现整个系统所需要完成的全部内容。这时候,A、B、C、D这4个对象之间已经没有了耦合关系,彼此毫无联系,这样的话, 当你在实现A的时候,根本无须再去考虑B、C和D了,对象之间的依赖关系已经降低到了最低程度。所以,如果真能实现IOC容器,对于系统开发而言,这将是 一件多么美好的事情,参与开发的每一成员只要实现自己的类就可以了,跟别人没有任何关系!
我们再来看看,控制反转(IOC)到底为什么要起这么个名字?我们来对比一下:
软件系统在没有引入IOC容器之前,如图1所示,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
软件系统在引入IOC容器之后,这种情形就完全改变了,如图3所示,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后的对比,我们不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。
3. IOC的别名:依赖注入(DI)
2004年,Martin Fowler探讨了同一个问题,既然IOC是控制反转,那么到底是“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。于是,他给“控制反转”取了一个更合适的名字叫做“依赖 注入(Dependency Injection)”。他的这个答案,实际上给出了实现IOC的方法:注入。所谓依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。
所以,依赖注入(DI)和控制反转(IOC)是从不同的角度的描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。
我们举一个生活中的例子,来帮助理解依赖注入的过程。大家对USB接口和USB设备应该都很熟悉吧,USB为我们使用电脑提供了很大的方便,现在有很多的外部设备都支持USB接口。
现在,我们利用电脑主机和USB接口来实现一个任务:从外部USB设备读取一个文件。
电脑主机读取文件的时候,它一点也不会关心USB接口上连接的是什么外部设备,而且它确实也无须知道。它的任务就是读取USB接口,挂接的外部设备只要符 合USB接口标准即可。所以,如果我给电脑主机连接上一个U盘,那么主机就从U盘上读取文件;如果我给电脑主机连接上一个外置硬盘,那么电脑主机就从外置 硬盘上读取文件。挂接外部设备的权力由我作主,即控制权归我,至于USB接口挂接的是什么设备,电脑主机是决定不了,它只能被动的接受。电脑主机需要外部 设备的时候,根本不用它告诉我,我就会主动帮它挂上它想要的外部设备,你看我的服务是多么的到位。这就是我们生活中常见的一个依赖注入的例子。在这个过程 中,我就起到了IOC容器的作用。
通过这个例子,依赖注入的思路已经非常清楚:当电脑主机读取文件的时候,我就把它所要依赖的外部设备,帮他挂接上。整个外部设备注入的过程和一个被依赖的对象在系统运行时被注入另外一个对象内部的过程完全一样。
我们把依赖注入应用到软件系统中,再来描述一下这个过程:
对象A依赖于对象B,当对象 A需要用到对象B的时候,IOC容器就会立即创建一个对象B送给对象A。IOC容器就是一个对象制造工厂,你需要什么,它会给你送去,你直接使用就行了, 而再也不用去关心你所用的东西是如何制成的,也不用关心最后是怎么被销毁的,这一切全部由IOC容器包办。
在传统的实现中,由程序内部代码来控制组件之间的关系。我们经常使用new关键字来实现两个组件之间关系的组合,这种实现方式会造成组件之间耦合。IOC 很好地解决了该问题,它将实现组件间关系从程序内部提到外部容器,也就是说由容器在运行期将组件间的某种依赖关系动态注入组件中。
4. IOC为我们带来了什么好处
我们还是从USB的例子说起,使用USB外部设备比使用内置硬盘,到底带来什么好处?
第一、USB设备作为电脑主机的外部设备,在插入主机之前,与电脑主机没有任何的关系,只有被我们连接在一起之后,两者才发生联系,具有相关性。所以,无 论两者中的任何一方出现什么的问题,都不会影响另一方的运行。这种特性体现在软件工程中,就是可维护性比较好,非常便于进行单元测试,便于调试程序和诊断 故障。代码中的每一个Class都可以单独测试,彼此之间互不影响,只要保证自身的功能无误即可,这就是组件之间低耦合或者无耦合带来的好处。
第二、USB设备和电脑主机的之间无关性,还带来了另外一个好处,生产USB设备的厂商和生产电脑主机的厂商完全可以是互不相干的人,各干各事,他们之间 唯一需要遵守的就是USB接口标准。这种特性体现在软件开发过程中,好处可是太大了。每个开发团队的成员都只需要关心实现自身的业务逻辑,完全不用去关心 其它的人工作进展,因为你的任务跟别人没有任何关系,你的任务可以单独测试,你的任务也不用依赖于别人的组件,再也不用扯不清责任了。所以,在一个大中型 项目中,团队成员分工明确、责任明晰,很容易将一个大的任务划分为细小的任务,开发效率和产品质量必将得到大幅度的提高。
第三、同一个USB外部设备可以插接到任何支持USB的设备,可以插接到电脑主机,也可以插接到DV机,USB外部设备可以被反复利用。在软件工程中,这 种特性就是可复用性好,我们可以把具有普遍性的常用组件独立出来,反复利用到项目中的其它部分,或者是其它项目,当然这也是面向对象的基本特征。显 然,IOC不仅更好地贯彻了这个原则,提高了模块的可复用性。符合接口标准的实现,都可以插接到支持此标准的模块中。
第四、同USB外部设备一样,模块具有热插拔特性。IOC生成对象的方式转为外置方式,也就是把对象生成放在配置文件里进行定义,这样,当我们更换一个实现子类将会变得很简单,只要修改配置文件就可以了,完全具有热插拨的特性。
以上几点好处,难道还不足以打动我们,让我们在项目开发过程中使用IOC框架吗?
5. IOC容器的技术剖析
IOC中最基本的技术就是“反射(Reflection)”编程,目前.Net C#、Java和PHP5等语言均支持,其中PHP5的技术书籍中,有时候也被翻译成“映射”。有关反射的概念和用法,大家应该都很清楚,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象。这种编程方式可以让对象在生成时才决定到底是哪一种对象。反射的应用是很广泛的,很多的成熟的框架,比如象Java中的Hibernate、Spring框架,.Net中 NHibernate、Spring.Net框架都是把“反射”做为最基本的技术手段。
反射技术其实很早就出现了,但一直被忽略,没有被进一步的利用。当时的反射编程方式相对于正常的对象生成方式要慢至少得10倍。现在的反射技术经过改良优化,已经非常成熟,反射方式生成对象和通常对象生成方式,速度已经相差不大了,大约为1-2倍的差距。
我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看 作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言的的反射编程,根据配置文件中给出的类名生成相应的对象。从实现来 看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护 性。
6. IOC容器的一些产品
Sun ONE技术体系下的IOC容器有:轻量级的有Spring、Guice、Pico Container、Avalon、HiveMind;重量级的有EJB;不轻不重的有JBoss,Jdon等等。Spring框架作为Java开发中 SSH(Struts、Spring、Hibernate)三剑客之一,大中小项目中都有使用,非常成熟,应用广泛,EJB在关键性的工业级项目中也被使 用,比如某些电信业务。
.Net技术体系下的IOC容器有:Spring.Net、Castle等等。Spring.Net是从Java的Spring移植过来的IOC容 器,Castle的IOC容器就是Windsor部分。它们均是轻量级的框架,比较成熟,其中Spring.Net已经被逐渐应用于各种项目中。
7. 使用IOC框架应该注意什么
使用IOC框架产品能够给我们的开发过程带来很大的好处,但是也要充分认识引入IOC框架的缺点,做到心中有数,杜绝滥用框架。
第一、软件系统中由于引入了第三方IOC容器,生成对象的步骤变得有些复杂,本来是两者之间的事情,又凭空多出一道手续,所以,我们在刚开始使用IOC框 架的时候,会感觉系统变得不太直观。所以,引入了一个全新的框架,就会增加团队成员学习和认识的培训成本,并且在以后的运行维护中,还得让新加入者具备同 样的知识体系。
第二、由于IOC容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。
第三、具体到IOC框架产品(比如:Spring)来讲,需要进行大量的配制工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
第四、IOC框架产品本身的成熟度需要进行评估,如果引入一个不成熟的IOC框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。
我们大体可以得出这样的结论:一些工作量不大的项目或者产品,不太适合使用IOC框架产品。另外,如果团队成员的知识能力欠缺,对于IOC框架产品缺乏深 入的理解,也不要贸然引入。最后,特别强调运行效率的项目或者产品,也不太适合引入IOC框架产品,象WEB2.0网站就是这种情况。
IoC概念回顾
控制反转(Inversion of Control)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题。 它还有一个名字叫做依赖注入(Dependency Injection)。IoC不是什么技术,它是一种设计模式。
实例演示
为了更好的说明IoC,我为大家举一个简单的例子,如有这样一个描述:某公司新成立了一个项目组,项目组有若干成员和一个项目组长,项目组成立后第一次开会上,作为项目组长的小李按照惯例首先做了简短的自我介绍。
根据上述的描述,如果我们写出如下代码和类图:
public class Li { public void introduce() {System.out.println("大家好,我是小李");}}public class Team {public void firstMeeting() {Li li = new Li();li.introduce();}}
具体类图如下:
上述的代码,应该说基本完成了相关的需求,但是仔细考虑之后就会发现,上述的代码是根据具体的场景描述进行的,并没有进行抽象,这样就导致我们不能灵活的安排项目组长去做开场,即根据现在的代码,开场自我介绍被绑定给了小李而不能安排给其他人。为了解决上述的问题,我们引入首先引入Leader接口,相关代码和类图如下:
public interface Leader {public void introduce();}public class Li implements Leader {@Overridepublic void introduce() {System.out.println("大家好,我是小李");}}public class Team {public void firstMeeting() {Leader li = new Li();li.introduce();}}
具体类图如下:
虽然上述的代码可以让我们安排给其他成员开场,但是我们可以看出Team类同时依赖Leader接口和Li类,并没有达到我们所期望的Team仅仅依赖于Leader接口的目的,如何解决这个问题呢?当然是引入Boss,由Boss决定具体由谁担任项目组长。具体类图和代码如下:
public interface Leader {public void introduce();}public class Li implements Leader {@Overridepublic void introduce() {System.out.println("大家好,我是小李");}}public class Team {public void firstMetting(Leader leader){leader.introduce();}}public class Boss {public void direct(){Leader leader = new Li();Team team = new Team();team.firstMetting(leader);}}
具体类图如下:
通过以上代码和图示,我们可以看出,通过引入老板类,我们将项目小组和具体由谁担任项目组长进行解耦。
对应上述例子,我们再来讲解一下IoC,IoC从字面上看分为控制和反转,控制在上面的实例中就是具体由谁担任项目组长,而反转就是将决定谁担任项目组长转移到Boss类中。通俗理解就是将接口的具体实现类(Li)的控制权从调用类(Team)中分离转交给第三方(Boss)决定。
到此为止,IoC的概念我们就已经讲完了,具体Spring中如何实现呢?
一、第一个Spring程序
1、建立如下的项目结构
2、在项目的src下创建com.spring包下创建Student.java
1 package com.spring; 2 /** 3 * 4 * @author Holly老师 5 * 6 */ 7 public class Student { 8 private String name; 9 private String sex; 10 public String getName() { 11 return name; 12 } 13 public void setName(String name) { 14 this.name = name; 15 } 16 public String getSex() { 17 return sex; 18 } 19 public void setSex(String sex) { 20 this.sex = sex; 21 } 22 @Override 23 public String toString() { 24 return "Student [name=" + name + ", sex=" + sex + "]"; 25 } 26 27 28 }
3、在src下创建applicationContext.xml文件
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:aop="http://www.springframework.org/schema/aop" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xmlns:tx="http://www.springframework.org/schema/tx" 6 xmlns:mvc="http://www.springframework.org/schema/mvc" 7 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 8 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 9 xsi:schemaLocation="http://www.springframework.org/schema/beans 10 http://www.springframework.org/schema/beans/spring-beans.xsd 11 http://www.springframework.org/schema/aop 12 http://www.springframework.org/schema/aop/spring-aop.xsd 13 http://www.springframework.org/schema/context 14 http://www.springframework.org/schema/context/spring-context.xsd 15 http://www.springframework.org/schema/tx 16 http://www.springframework.org/schema/tx/spring-tx.xsd 17 http://www.springframework.org/schema/mvc 18 http://www.springframework.org/schema/mvc/spring-mvc.xsd 19 "> 20 <!-- IOC依赖注入,给对象注入数据 --> 21 <!-- 1. 22 bean节点相当于:Student stuent=new Student(); 23 bean的id就相当于对象名,class是对象的类型全路径 --> 24 <bean id="student" class="com.spring.Student"> 25 <!-- 1.1 为对象赋值,代理对象通过setter方法赋值 --> 26 <!-- 1.2 27 property属性节点的name值必须和Student类的属性名保持一致 28 相当于 student.setName("扬名"); 29 --> 30 <property name="name" value="扬名"/> 31 <property name="age" value="18"/> 32 </bean> 33 34 <!-- 2.创建HelloSpring的bean节点: 35 HelloSpring helloSpring=new HelloSpring(); 36 --> 37 <bean id="helloSpring" class="com.spring.HelloSpring"> 38 <!-- 2.1 给Student类型的stu对象赋值,helloSpring.setStu(student); --> 39 <property name="stu" ref="student"/> 40 41 <!-- 2.2赋值方式1:给:helloSpring.setWho("Spring") --> 42 <property name="who"> 43 <value>Spring</value> 44 </property> 45 46 <!-- 2.3赋值方式2:给helloSpring.setYou("holly");--> 47 <property name="you" value="holly"/> 48 </bean> 49 </beans>
4、在com.spring包下创建HelloSpring.java
1 package com.spring; 2 3 public class HelloSpring { 4 private Student stu=null; 5 private String who=null; 6 private String you=null; 7 public Student getStu() { 8 return stu; 9 } 10 public void setStu(Student stu) { 11 this.stu = stu; 12 } 13 public String getWho() { 14 return who; 15 } 16 public void setWho(String who) { 17 this.who = who; 18 } 19 public String getYou() { 20 return you; 21 } 22 public void setYou(String you) { 23 this.you = you; 24 } 25 /** 26 * 打印信息方法: 27 */ 28 public void print(){ 29 System.out.println("Hello"+this.getWho()); 30 System.out.println("you是:"+this.getYou()); 31 } 32 33 }
5、在com.test包下创建Test.java 运的效果
1 package com.test; 2 3 import org.springframework.context.ApplicationContext; 4 import org.springframework.context.support.ClassPathXmlApplicationContext; 5 6 import com.spring.HelloSpring; 7 8 public class Test { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 //1.读取xml文件 15 ApplicationContext ac=new ClassPathXmlApplicationContext("applicationContext.xml"); 16 17 /*2.通过ApplicationContext对象的getBean("Xxx") 18 * 获取xml文件中bean的id为Xxx的节点对象 19 */ 20 HelloSpring hs=(HelloSpring) ac.getBean("helloSpring"); 21 22 //3.调用HelloSpring中的print方法 23 hs.print(); 24 25 //4.打印HelloSpring中类型为Student的stu对象的值 26 System.out.println(hs.getStu()); 27 } 28 29 }
二、使用IOC注入Dao
1、创建如下项目结构
2、在com.entity包下创建Classes.java
1 package com.entity; 2 3 public class Classes { 4 private int cid; 5 private String cname; 6 7 public Classes() { 8 } 9 10 public Classes(int cid, String cname) { 11 this.cid = cid; 12 this.cname = cname; 13 } 14 15 public int getCid() { 16 return cid; 17 } 18 19 public void setCid(int cid) { 20 this.cid = cid; 21 } 22 23 public String getCname() { 24 return cname; 25 } 26 27 public void setCname(String cname) { 28 this.cname = cname; 29 } 30 31 @Override 32 public String toString() { 33 return "Classes [cid=" + cid + ", cname=" + cname + "]"; 34 } 35 36 37 }
3、在com.entity包下创建Student.java
1 package com.entity; 2 /** 3 * 学生表 4 * @author Dell 5 * 6 */ 7 public class Student { 8 private String sid; 9 private String sname; 10 private String sex; 11 private Classes cla; 12 13 public Student() { 14 } 15 public Student(String sid, String sname, String sex, Classes cla) { 16 this.sid = sid; 17 this.sname = sname; 18 this.sex = sex; 19 this.cla = cla; 20 } 21 public String getSid() { 22 return sid; 23 } 24 public void setSid(String sid) { 25 this.sid = sid; 26 } 27 public String getSname() { 28 return sname; 29 } 30 public void setSname(String sname) { 31 this.sname = sname; 32 } 33 public String getSex() { 34 return sex; 35 } 36 public void setSex(String sex) { 37 this.sex = sex; 38 } 39 public Classes getCla() { 40 return cla; 41 } 42 public void setCla(Classes cla) { 43 this.cla = cla; 44 } 45 @Override 46 public String toString() { 47 return "Student [cla=" + cla + ", sex=" + sex + ", sid=" + sid 48 + ", sname=" + sname + "]"; 49 } 50 51 52 53 }
4、在src下创建applicationContext.xml文件
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:aop="http://www.springframework.org/schema/aop" 4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans.xsd "> 7 <!-- 定义班级bean --> 8 <bean id="classes" class="com.entity.Classes"> 9 <property name="cid" value="1"/> 10 <property name="cname" value="TB13"/> 11 </bean> 12 13 <!-- 定义学生bean --> 14 <bean id="stu" class="com.entity.Student"> 15 <property name="sid" value="1"/> 16 <property name="sname" value="holly"/> 17 <property name="sex" value="女"/> 18 <!-- 注入班级bean --> 19 <!-- 注入方式1:设置注入 --> 20 <property name="cla" ref="classes"/> 21 </bean> 22 23 </beans>
5、在com.spring包下创建Test.java
1 package com.spring; 2 3 import org.springframework.context.ApplicationContext; 4 import org.springframework.context.support.ClassPathXmlApplicationContext; 5 6 import com.entity.Student; 7 8 public class Test { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 //加载读取xml文件 15 ApplicationContext ac=new ClassPathXmlApplicationContext("applicationContext.xml"); 16 17 //通过配置文件的bean的id获取某个节点内容,注意在要注入classes的student类中一定要有getter和setter方法 18 Student stu=(Student) ac.getBean("stu"); 19 System.out.println(stu); 20 21 } 22 23 }
6、运行效果如下: