1.Spring简介
(1)Spring是什么
Spring是轻量级的J2EE应用程序开源框架。它由Rod Johnson创建。它是为了解决企业应用开发的复杂性而创建的。Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。
Spring的核心是个轻量级容器(container),实现了IoC(Inversion of Control)模式的容器,Spring的目标是实现一个全方位的整合框架,在Spring框架下实现多个子框架的组合,这些子框架之间彼此可以独立,也可以使用其它的框架方案加以替代,Spring希望提供one-stop shop的框架整合方案
Spring不会特别去提出一些子框架来与现有的OpenSource框架竞争,除非它觉得所提出的框架够新够好,例如Spring有自己的 MVC框架方案,因为它觉得现有的MVC方案有很多可以改进的地方,但它不强迫您使用它提供的方案,您可以选用您所希望的框架来取代其子框架,例如您仍可以在Spring中整合您的Struts框架。
Spring的核心概念是IoC,IoC的抽象概念是「依赖关系的转移」,像是「高层模块不应该依赖低层模块,而是模块都必须依赖于抽象」是IoC的一种表现,「实现必须依赖抽象,而不是抽象依赖实现」也是IoC的一种表现,「应用程序不应依赖于容器,而是容器服务于应用程序」也是IoC的一种表现。回想一下面向对象的设计原则:OCP原则和DIP原则。
Spring的核心即是个IoC/DI的容器,它可以帮程序设计人员完成组件(类别们)之间的依赖关系注入(连结),使得组件(类别们)之间的依赖达到最小,进而提高组件的重用性,Spring是个低侵入性(invasive)的框架,Spring中的组件并不会意识到它正置身于Spring中,这使得组件可以轻易的从框架中脱离,而几乎不用任何的修改,反过来说,组件也可以简单的方式加入至框架中,使得组件甚至框架的整合变得容易。
Spring最为人重视的另一方面是支持AOP(Aspect-Oriented Programming),然而AOP框架只是Spring支持的一个子框架,说Spring框架是AOP框架并不是一件适当的描述,人们对于新奇的 AOP关注映射至Spring上,使得人们对于Spring的关注集中在它的AOP框架上,虽然有所误解,但也突显了Spring的另一个令人关注的特色。
Spring也提供MVC Web框架的解决方案,但您也可以将自己所熟悉的MVC Web框架与Spring解合,像是Struts、Webwork等等,都可以与Spring整合而成为适用于自己的解决方案。
Spring也提供其它方面的整合,像是持久层的整合如JDBC、O/R Mapping工具(Hibernate、iBATIS)、事务处理等等,Spring作了对多方面整合的努力,故说Spring是个全方位的应用程序框架。
Spring框架由七个定义明确的模块组成:
图1 Spring框架概览图
总的来说,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
轻量??从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。此外,Spring是非侵入式的:典型地,Spring应用中的对象不依赖于Spring的特定类。
控制反转??Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反??不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
面向切面??Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务()管理)进行内聚性的开发。应用对象只实现它们应该做的??完成业务逻辑??仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
容器??Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建??基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例??以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。
框架??Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。
所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。
(2)Spring的历史
Spring的基础架构起源于2000年早期,它是Rod Johnson在一些成功的商业项目中构建的基础设施。
在2002后期,Rod Johnson发布了《Expert One-on-One J2EE Design and Development》一书,并随书提供了一个初步的开发框架实现??interface21开发包,interface21就是书中阐述的思想的具体实现。后来,Rod Johnson 在interface21 开发包的基础之上,进行了进一步的改造和扩充,使其发展为一个更加开放、清晰、全面、高效的开发框架??Spring。
2003年2月Spring框架正式成为一个开源项目,并发布于SourceForge中。
(3)Spring的使命
J2EE应该更加容易使用。
面向对象的设计比任何实现技术(比如J2EE)都重要。
面向接口编程,而不是针对类编程。Spring将使用接口的复杂度降低到零。(面向接口编程有哪些复杂度?)
代码应该易于测试。Spring框架会帮助你,使代码的测试更加简单。
JavaBean提供了应用程序配置的最好方法。
在Java中,已检查异常(Checked exception)被过度使用。框架不应该迫使你捕获不能恢复的异常。
2.控制反转IoC
IoC全名Inversion of Control,如果中文硬要翻译过来的话,就是「控制反转」。初看IoC,从字面上不容易了解其意义,我觉得要了解IoC,最好先从Dependency Inversion开始了解,也就是依赖关系的反转。
Dependency Inversion在面向对象的设计原则之依赖倒置原则(DIP,Dependence Inversion Principle)中有着清楚的解?。
简单的说,在模块设计时,高层的抽象模块通常是与业务相关的模块,它应该具有重用性,而不依赖于低层的实现模块,例如如果低层模块原先是软盘存取模式,而高层模块是个存盘备份的需求,如果高层模块直接叫用低层模块的函式,则就对其产生了依赖关系。请看下面的例子:
void Copy(){
int c;
while ((c = ReadKeyboard()) != EOF)
WritePrinter(c);
}
这是僵化和不易改动的例子,为什么呢?很显然,如果我还要将内容输出到磁盘上(如下图所示),那么我们必须改动Copy的内容,并进行重新的测试和编译。
改动后的程序如下所示:
enum OutputDevice {printer, disk};
void Copy(OutputDevice dev){
int c;
while((c = ReadKeyboard())!= EOF)
if(dev == printer)
WritePrinter(c);
else
WriteDisk(c);
}
如果要继续添加别的输入或输出方式,该程序还是无法重用,要对此程序进行修改才能继续使用。
利用依赖倒置原则(DIP),可以解决这个问题。DIP原则,可以从2点来解读:
第1点:高层模块不依赖底层模块,两者都依赖抽象
第2点:抽象不应该依赖于细节,细节应该依赖于抽象
上面所讲的例子如果用DIP原则,结果如下
class Reader {
public:
virtual int read()=0;
};
class Writer {
public:
virtual void write(int)=0;
};
void Copy(Reader& r, Writer& w){
int c;
while((c = r.read()) != EOF)
w.write(c);
}
这样一来,如果要添加新的输入或输出设备时只要改动相应的类(class Reader,Writer,利用多态来解决上面的问题)就可以了,而其它的程序都不用改动。这就是依赖倒置原则的基本内涵。
在软件设计和构建中我们要遵循“高内聚、低偶合”的原则。那么,依赖对于我们来说究竟是好事还是坏事呢?
首先应该明白的是,类之间如果是零偶合的状态是不能够构建应用程序的,只能构建类库。
但是由于人类的理解力和可控制的范围有限,大多数人难以理解和把握过于复杂的系统。把软件系统划分成多个模块,可以有效控制模块的复杂度,使每个模块都易于理解和维护。但在这种情况下,模块之间就必须以某种方式交换信息,也就是必然要发生某种耦合关系。如果某个模块和其它模块没有任何关联(哪怕只是潜在的或隐含的依赖关系),我们就几乎可以断定,该模块不属于此软件系统,应该从系统中剔除。如果所有模块之间都没有任何耦合关系,其结果必然是:整个软件不过是多个互不相干的系统的简单堆积,对每个系统而言,所有功能还是要在一个模块中实现,这等于没有做任何模块的分解。
因此,模块之间必定会有这样或那样的依赖关系,永远不要幻想消除所有依赖。但是,过强的耦合关系(如一个模块的变化会造成一个或多个其他模块也同时发生变化的依赖关系)会对软件系统的质量造成很大的危害。特别是当需求发生变化时,代码的维护成本将非常高。所以,我们必须想尽办法来控制和消解不必要的耦合,特别是那种会导致其它模块发生不可控变化的依赖关系。依赖倒置、控制反转就是人们在和依赖关系进行艰苦卓绝的斗争过程中不断产生和发展起来的。
我们下面来继续一步一步的说明这个问题。
看下面的程序:
#include <floppy.h>
....
void save() {
....
saveToFloppy()
}
}
由于save()程序依赖于saveToFloppy(),如果今天要更换低层的存储模块为Usb碟,则这个程序没有办法重用,必须加以修改才行,低层模块的更动造成了高层模块也必须跟着更动,这不是一个好的设计方式,我们希望模块都依赖于模块的抽象,这样才可以重用高层的业务设计。
如果以面向对象的方式来设计,依赖倒置(Dependency Inversion)的解释变为程序不应依赖实现,而是依赖于抽象,实现必须依赖于抽象。我们来看看下面这个Java程序:
BusinessObject.java
public class BusinessObject {
private FloppyWriter writer = new FloppyWriter();
....
public void save() {
...
writer.saveToFloppy();
}
}
public class FloppyWriter {
.... //相应的写盘的代码
}
在这个程序中,BusinessObject的存盘依赖于实际的FloppyWriter,如果今天我们想要将存盘改为存至Usb碟,我们必须修改或继承BusinessObject进行扩展,而无法直接使用BusinessObject。
浩劫只是刚刚开始,这时,你一定和我一样在期待着救世主的早日降临??接口(Interface)。你一定会说,面向对象的设计原则已经告诉我们了啊,“要针对接口编程”,你为什么不用呢?好,我们采用这个原则进行编程:
什么是接口?
接口定义了行为的协议,这些行为在继承接口的类中实现。
接口定义了很多方法,但是没有实现它们。类履行接口协议并实现所有定义在接口中的方法。
接口是一种只有声明没有实现的特殊类。
使用接口的优点:
Client不必知道其使用对象的具体所属类。
一个对象可以很容易地被(实现了相同接口的)的另一个对象所替换。
对象间的连接不必硬绑定(hardwire)到一个具体类的对象上,因此增加了灵活性。
松散藕合(loosens coupling)。
增加了重用的可能性。
通过面向接口编程,可以改?此一情?,例如:
public interface IDeviceWriter {
public void saveToDevice();
}
public class BusinessObject {
private IDeviceWriter writer;
public void setDeviceWriter(IDeviceWriter writer) {
this.writer = writer;
}
public void save() {
....
writer.saveToDevice();
}
}
这样一来,BusinessObject就是可重用的,如果今天我有存储至Floppy或Usb碟的需求,我只要实现IDeviceWriter即可,而不用修改BusinessObject:
public class FloppyWriter implement IDeviceWriter {
public void saveToDevice() {
....
// 实际储存至Floppy的程序代码
}
}
public class UsbDiskWriter implement IDeviceWriter {
public void saveToDevice() {
....
// 实际储存至UsbDisk的程序代码
}
}
从这个角度来看,Dependency Inversion的意思即是程序不依赖于实现,而是程序与实现都要依赖于抽象。
哦,这样一来,一切都OK了吗?还没有没有问题呢,你可能会有疑问,我要根据不同的情况来使用软盘、USB碟或者其它的存储设备,怎么办呢?在程序包中,BusinessObject不是还是和FloppyWriter或者UsbDiskWriter绑定吗,如果系统发布后,要将FloppyWriter替换为UsbDiskWriter不是还要去修改IDeviceWriter的实现吗?修改就意味着可能会带来错误,就要带来修改代码、进行测试、编译、维护等的工作量。还有更好的方法吗?IoC/DI就是解决之道。
IoC的Control是控制的意思,其实其背后的意义也是一种依赖关系的转移,如果A依赖于B,其意义即是B拥有控制权,我们要转移这种关系,所以依赖关系的反转即是控制关系的反转,藉由控制关系的转移,我们可以获得组件的可重用性,在上面的Java程序中,整个控制权从实际的 FloppyWriter转移至抽象的IDeviceWriter接口上,使得BusinessObject、FloppyWriter、 UsbDiskWriter这几个实现依赖于抽象的IDeviceWriter接口。
使用IoC,对象是被动的接受依赖类,而不是自己主动的去找。容器在实例化的时候主动将它的依赖类注入给它。可以这样理解:控制反转将类的主动权转移到接口上,依赖注入通过xml配置文件在类实例化时将其依赖类注入。
使用IoC,对象是被动的接受依赖类,而不是自己主动的去找。容器在实例化的时候主动将它的依赖类注入给它。可以这样理解:控制反转将类的主动权转移到接口上,依赖注入通过xml配置文件在类实例化时将其依赖类注入。
“控制反转(Inversion of Control)与依赖倒置原则(Dependency Inversion Principle)是一个同义原则。虽然“依赖倒置”和“控制反转”在设计层面上都是消解模块耦合的有效方法,也都是试图令具体的、易变的模块依赖于抽象的、稳定的模块的基本原则,但二者在使用语境和关注点上存在差异:“依赖倒置”强调的是对于传统的、源于面向过程设计思想的层次概念的“倒置”,而“控制反转”强调的是对程序流程控制权的反转。“依赖倒置”的使用范围更为宽泛一些。
IoC在容器的角度,可以用这么一句好莱坞名言来代表(著名的好莱坞原则):"Don't call me, I'll call you."(不要打电话给我们,我们会通知你的)。好莱坞的演员门(包括大牌)都会在好莱坞进行登记,他们不能够直接打电话给制片人或者导演要求演出摸个角色或者参加某个片子的演出,而是由好莱坞根据需要去通知他们(前提是他们已经在好莱坞做过登记)。在这里好莱坞就相当于容器。
以程序的术语来说的话,就是「不要向容器要求您所需要的(对象)资源,容器会自动将这些对象给您!」。IoC要求的是容器不侵入应用程序本身,应用程序本身提供好接口,容器可以透过这些接口将所需的资源注至程序中,应用程序不向容器主动要求资源,故而不会依赖于容器的组件,应用程序本身不会意识到正被容器使用,可以随时从容器中脱离转移而不用作任何的修改,而这个特性正是一些业务逻辑中间件最需要的。
3.依?注入DI
IoC模式基本上是一个高层的概念,在Martin Fowler的Inversion of Control Containers and the Dependency Injection pattern中谈到,实现IoC有两种方式:Dependency Injection与Service Locator。
Spring所采用的是Dependency Injection来实现IoC(多数容器都是采取这种方式的),中文翻译为依赖注入,依赖注入的意义是:保留抽象接口,让组件依赖于抽象接口,当组件要与其它实际的对象发生依赖关系时,藉过抽象接口来注入依赖的实际对象。
回锅头来再仔细研读一下我们在上面给出的例子:
public interface IDeviceWriter {
public void saveToDevice();
}
public class BusinessObject {
private IDeviceWriter writer;
public void setDeviceWriter(IDeviceWriter writer) {
this.writer = writer;
}
public void save() {
....
writer.saveToDevice();
}
}
public class FloppyWriter implement IDeviceWriter {
public void saveToDevice() {
....
// ???存至Floppy的程式?
}
}
public class UsbDiskWriter implement IDeviceWriter {
public void saveToDevice() {
....
// ???存至UsbDisk的程式?
}
}
为了让BusinessObject获得重用性,我们不让BusinessObject依赖于实际的FloppyWriter,而是依赖于抽象的接口。
在此代码中,首先IDeviceWriter的变量writer可以接收任何IDeviceWriter的实例,另外,FloppyWriter或UsbDiskWrite的实例不是通过BusinessObject来获得,而是通过setter(也可以用构造器)来由外部传给它。这似乎跟我们往常的代码没什么不同(回想一下Javabean的setter/getter),但这已经是一个良好的设计。现在的关键问题是:FloppyWriter或UsbDiskWrite的实例如何从外部注入给BusinessObject呢?这就要通过xml来实现了(相当于演员们在好莱坞登记)。
Spring的配置文件applicationContext.xml代码片断如下:
<beans>
<bean id = "FloppyWriter" class = "iocfirst.business.write" />
<bean
id = "BusinessObject"
class = "iocfirst.business.BusinessObject"
/>
<property name = " FloppyWriter">
<ref bean = " FloppyWriter " />
</property>
</bean>
</beans>
如果什么时候想将UsbDiskWrite注入,则修改applicationContext.xm即可。
单有了这个xml文件还不够,加载该xml文件呢?spring提供了现成的API,在加载上面的xml的时候,就进行了如下工作:实例化FloppyWriter或UsbDiskWrite类,实例化BusinessObject类,并将FloppyWriter或UsbDiskWrite的实例作为参数赋给了BusinessObject实例的setDeviceWriter方法。
BusinessObject依赖于抽象接口,在需要建立依赖关系时,我们就是通过抽象接口注入依赖的实际对象。
依赖注入在Martin Fowler的文章中谈到了三种实现方式:interface injection、setter injection与constructor injection。并分别称其为type 1 IoC、type 2 IoC与type 3 IoC。
Type1-接口注入(Interface Injection)
它是在一个接口中定义需要注入的信息,并通过接口完成注入。Apache Avalon是一个较为典型的Type1型IOC容器,WebWork框架的IoC容器也是Type1型。
Type2-设值方法注入(Setter Injection)
在各种类型的依赖注入模式中,设值注入模式在实际开发中得到了最广泛的应用(其中很大一部分得力于Spring框架的影响)。
基于设置模式的依赖注入机制更加直观、也更加自然。上面的BusinessObject所实现的是type 2 IoC,透过setter注入依赖关系。
Type3-构造子注入(Constructor Injection)
构造子注入,即通过构造函数完成依赖关系的设定。
目前我们只要了解到有这个3种方式就可以了,具体情况将在以后的章节中进行介绍。
4.开始spring之旅
任何需要交给spring管理的对象,都必须在配置文件中注册,这个过程被称为wiring,下面做一个最简单的演示,
第一步:下载Spring,Spring官方网站是 http://www.springframework.org/ 我们这里下载的是2.0.1版,spring-framework-2.0.1-with-dependencies.zip是该版本的压缩包,参见图2。
图 2
将压缩包的内容解压到一个目录中,D:/spring-framework-2.0.1是我这里使用的存放目录。
第二步:使用Eclipse的【new project】向导,新建一个名为spring-demo的工程。参见图3。
图3
第三步:新建一个名为HelloTalker的类,package选择com.spring.demo,如“图4”所示。
图 4
第四步:把所需要使用的Spring项目jar的文件加入到当前工程的Libraries中,本例中只使用Spring的简单IOC功能,只需要spring-beans.jar、spring-core.jar以及commons-logging.jar三个包即可。
右键点击工程名称,查看工程属性,选择工程属性中的【Java Builder Path】,然后选择【Libraries】选项,通过点击【add external jar】按钮来把外部的jar文件添加到工程项目中。参见图5所示:
图 5
第五步,录入HelloTalker.java的完整源代码。其中HelloTalker这个类有一个msg属性,为该属性编写set方法,该方法必须严格遵守javabean的命名规则即有一个setMsg方法用于设置msg属性的值。另外,我们直接在HelloTalker的main方法中使用Spring的Bean工厂来从配置文件中加载一个名为helloBean,类型为com.spring.demo.HelloTalker的Bean。然后调用这个Bean的sayHello方法。全部源代码如下所示:
package com.spring.demo;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
public class HelloTalker {
/**
* @param args
*/
private String msg;
public void setMsg(String msg)
{
this.msg=msg;
}
public void sayHello()
{
System.out.print(msg);
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Resource res=new ClassPathResource("com/spring/demo/springConfig.xml");
BeanFactory factory=new XmlBeanFactory(res);
HelloTalker hello=(HelloTalker)factory.getBean("helloBean");
hello.sayHello();
}
}
第六步,任何需要交给spring管理的对象,都必须在配置文件中注册,这个过程被称为wiring。因此,在与HelloTalker.java同级的目录下,建一个名为springConfig.xml(文件名可以任意定)的Spring配置文件,在这个Bean配置文件中定义helloBean,并通过依赖注入设置helloBean的msg属性值,其内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="helloBean" class="com.spring.demo.HelloTalker">
<property name="msg" value="Hello World的Spring示例!"/>
</bean>
</beans>
参见图6:
图6,工程完成后的情况
第七步:运行程序,在HelloTalker.java上点右键,选择【Run As】下面的【Java Application】。即可运行这个使用了Spring的程序,该程序将输出helloBean中的msg属性的值:“Hwllo World的Spring示例!”,如图7所示。
图7 程序运行的结果
至此为止,我们就完成了一个最简单的Spring应用实践。更多的内容将在后面的部分逐步给出。
4.注入方式
前面已经提到依赖注入方式由种,Spring鼓励的是setter injection,但也允许您使用constructor injection,使用setter或constructor来注入依赖关系视您的需求而定,使用constructor的好处之一是,您可以在建构对象的同时一并完成依赖关系的建立,然而如果要建立的对象关系很多,则会在建构函式上留下一长串的参数,这时使用setter会是个不错的选择,另一方面, setter可以有明确的名称可以了解注入的对象会是什么,像是setXXX()这样的名称会比记忆constructor上某个参数位置代表某个对象来得好。
前面讲的例子采用的是setter injection。Type 1 IoC是interface injection,使用type 1 IoC时会要求实现接口,这个接口是为容器所用的,容器知道接口上所规定的方法,它可以呼叫实现接口的对象来完成依赖关系的注入,例如:
public interface IDependencyInjection {
public void createDependency(Map dependObjects);
}
public class BusinessObject implement IDependencyInjection {
private Map dependObjects;
public void createDependency(Map dependObjects) {
this.dependObject = dependObjects;
// 在这边实现与BusinessObject的依赖关系
......
}
public void save() {
....
writer.saveToDevice();
}
}
如果要完成依赖关系注入的对象,必须实现IDependencyInjection接口,并交由容器管理,容器会呼叫被管理对象的createDependency()方法来完成依赖关系的建立。
在上面的例子中,type 1 IoC要求BusinessObject实现特定的接口,这就使得BusinessObject依赖于容器,如果日后BusinessObject要脱离目前这个容器,就必须修改程序,想想在更复杂的依赖关系中产生更多复杂的接口,组件与容器(框架)的依赖会更加复杂,最后使得组件无法从容器中脱离。
所以type 1 IoC具有强的侵入性,使用它来实现依赖注入会使得组件相依于容器(框架),降低组件的重用性。
Spring的核心是个IoC容器,您可以用setter或constructor的方式来实现您的业务对象,至于对象与对象之间的关系建立,则透过组态设定,让Spring在执行时期根据组态档的设定来为您建立对象之间的依赖关系。
那么,如何使用construtor injection呢?首先看看HelloBean:
HelloBean.java
package onlyfun.caterpillar;
public class HelloBean {
private String helloWord = "hello";
private String user = "NoBody";
public HelloBean(String helloWord, String user) {
this.helloWord = helloWord;
this.user = user;
}
public String sayHelloToUser() {
return helloWord + "!" + user + "!";
}
}
为了突显构造函式,我们这个HelloBean设计的简陋,只提供了构造函式与必要的sayHelloToUser(),我们来看看bean的定义档案:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="helloBean" class="onlyfun.caterpillar.HelloBean">
<constructor-arg index="0"><value>Greeting</value></constructor-arg>
<constructor-arg index="1"><value>Justin</value></constructor-arg>
</bean>
</beans>
在bean的定义档案中,我们使用<constructor-arg>来表示我们将使用constructor injection,由于使用constructor injection时并不如setter injection时拥有setXXX()这样易懂的名称,所以我们必须指定参数的位置索引,index属性就是用于指定我们的对象将注入至构造函式中的哪一个参数,索引值从0开始,符合Java索引的惯例。
来看看测试程序:
Test.java
package onlyfun.caterpillar;
import java.io.*;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = new FileInputStream("bean.xml");
BeanFactory factory = new XmlBeanFactory(is);
HelloBean hello = (HelloBean) factory.getBean("helloBean");
System.out.println(hello.sayHelloToUser());
}
}
几种依赖注入模式的特点如下:
接口注入模式因为历史较为悠久,在很多容器中都已经得到应用。但由于其在灵活性、易用性上不如
其他两种注入模式,因而在IOC的专题世界内并不被看好。
Type2和Type3型的依赖注入实现则是目前主流的IOC实现模式。这两种实现方式各有特点,也各具优势。
Type2 设值注入的优势
1. 对于习惯了传统JavaBean开发的程序员而言,通过setter方法设定依赖关系显得更加直观,更加自然。
2. 如果依赖关系(或继承关系)较为复杂,那么Type3模式的构造函数也会相当庞大(我们需要在构造函数中设定所有依赖关系),此时Type2模式往往更为简洁。
3. 对于某些第三方类库而言,可能要求我们的组件必须提供一个默认的构造函数(如Struts中的Action),此时Type3类型的依赖注入机制就体现出其局限性,难以完成我们期望的功能。
Type3 构造子注入的优势:
1. “在构造期即创建一个完整、合法的对象”,对于这条Java设计原则,Type3无疑是最好的响应者。
2. 避免了繁琐的setter方法的编写,所有依赖关系均在构造函数中设定,依赖关系集中呈现,更加易读。
3. 由于没有setter方法,依赖关系在构造时由容器一次性设定,因此组件在被创建之后即处于相对“不变”的稳定状态,无需担心上层代码在调用过程中执行setter方法对组件依赖关系产生破坏,特别是对于Singleton模式的组件而言,这可能对整个系统产生重大的影响。
4. 同样,由于关联关系仅在构造函数中表达,只有组件创建者需要关心组件内部的依赖关系。对调用者而言,组件中的依赖关系处于黑盒之中。对上层屏蔽不必要的信息,也为系统的层次清晰性提供了保证。
5. 通过构造子注入,意味着我们可以在构造函数中决定依赖关系的注入顺序,对于一个大量依赖外部服务的组件而言,依赖关系的获得顺序可能非常重要,比如某个依赖关系注入的先决条件是组件的UserDao及相关资源已经被设定。
可见,Type3和Type2模式各有千秋,而Spring、PicoContainer都对Type3和Type2类型的依赖注入机制提供了良好支持。这也就为我们提供了更多的选择余地。理论上,以Type3类型为主,辅之以Type2类型机制作为补充,可以达到最好的依赖注入效果,不过对于基于Spring Framework开发的应用而言,Type2使用更加广泛。
(1)Spring是什么
Spring是轻量级的J2EE应用程序开源框架。它由Rod Johnson创建。它是为了解决企业应用开发的复杂性而创建的。Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。
Spring的核心是个轻量级容器(container),实现了IoC(Inversion of Control)模式的容器,Spring的目标是实现一个全方位的整合框架,在Spring框架下实现多个子框架的组合,这些子框架之间彼此可以独立,也可以使用其它的框架方案加以替代,Spring希望提供one-stop shop的框架整合方案
Spring不会特别去提出一些子框架来与现有的OpenSource框架竞争,除非它觉得所提出的框架够新够好,例如Spring有自己的 MVC框架方案,因为它觉得现有的MVC方案有很多可以改进的地方,但它不强迫您使用它提供的方案,您可以选用您所希望的框架来取代其子框架,例如您仍可以在Spring中整合您的Struts框架。
Spring的核心概念是IoC,IoC的抽象概念是「依赖关系的转移」,像是「高层模块不应该依赖低层模块,而是模块都必须依赖于抽象」是IoC的一种表现,「实现必须依赖抽象,而不是抽象依赖实现」也是IoC的一种表现,「应用程序不应依赖于容器,而是容器服务于应用程序」也是IoC的一种表现。回想一下面向对象的设计原则:OCP原则和DIP原则。
Spring的核心即是个IoC/DI的容器,它可以帮程序设计人员完成组件(类别们)之间的依赖关系注入(连结),使得组件(类别们)之间的依赖达到最小,进而提高组件的重用性,Spring是个低侵入性(invasive)的框架,Spring中的组件并不会意识到它正置身于Spring中,这使得组件可以轻易的从框架中脱离,而几乎不用任何的修改,反过来说,组件也可以简单的方式加入至框架中,使得组件甚至框架的整合变得容易。
Spring最为人重视的另一方面是支持AOP(Aspect-Oriented Programming),然而AOP框架只是Spring支持的一个子框架,说Spring框架是AOP框架并不是一件适当的描述,人们对于新奇的 AOP关注映射至Spring上,使得人们对于Spring的关注集中在它的AOP框架上,虽然有所误解,但也突显了Spring的另一个令人关注的特色。
Spring也提供MVC Web框架的解决方案,但您也可以将自己所熟悉的MVC Web框架与Spring解合,像是Struts、Webwork等等,都可以与Spring整合而成为适用于自己的解决方案。
Spring也提供其它方面的整合,像是持久层的整合如JDBC、O/R Mapping工具(Hibernate、iBATIS)、事务处理等等,Spring作了对多方面整合的努力,故说Spring是个全方位的应用程序框架。
Spring框架由七个定义明确的模块组成:
图1 Spring框架概览图
总的来说,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
轻量??从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。此外,Spring是非侵入式的:典型地,Spring应用中的对象不依赖于Spring的特定类。
控制反转??Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反??不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
面向切面??Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务()管理)进行内聚性的开发。应用对象只实现它们应该做的??完成业务逻辑??仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
容器??Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建??基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例??以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。
框架??Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。
所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。
(2)Spring的历史
Spring的基础架构起源于2000年早期,它是Rod Johnson在一些成功的商业项目中构建的基础设施。
在2002后期,Rod Johnson发布了《Expert One-on-One J2EE Design and Development》一书,并随书提供了一个初步的开发框架实现??interface21开发包,interface21就是书中阐述的思想的具体实现。后来,Rod Johnson 在interface21 开发包的基础之上,进行了进一步的改造和扩充,使其发展为一个更加开放、清晰、全面、高效的开发框架??Spring。
2003年2月Spring框架正式成为一个开源项目,并发布于SourceForge中。
(3)Spring的使命
J2EE应该更加容易使用。
面向对象的设计比任何实现技术(比如J2EE)都重要。
面向接口编程,而不是针对类编程。Spring将使用接口的复杂度降低到零。(面向接口编程有哪些复杂度?)
代码应该易于测试。Spring框架会帮助你,使代码的测试更加简单。
JavaBean提供了应用程序配置的最好方法。
在Java中,已检查异常(Checked exception)被过度使用。框架不应该迫使你捕获不能恢复的异常。
2.控制反转IoC
IoC全名Inversion of Control,如果中文硬要翻译过来的话,就是「控制反转」。初看IoC,从字面上不容易了解其意义,我觉得要了解IoC,最好先从Dependency Inversion开始了解,也就是依赖关系的反转。
Dependency Inversion在面向对象的设计原则之依赖倒置原则(DIP,Dependence Inversion Principle)中有着清楚的解?。
简单的说,在模块设计时,高层的抽象模块通常是与业务相关的模块,它应该具有重用性,而不依赖于低层的实现模块,例如如果低层模块原先是软盘存取模式,而高层模块是个存盘备份的需求,如果高层模块直接叫用低层模块的函式,则就对其产生了依赖关系。请看下面的例子:
void Copy(){
int c;
while ((c = ReadKeyboard()) != EOF)
WritePrinter(c);
}
这是僵化和不易改动的例子,为什么呢?很显然,如果我还要将内容输出到磁盘上(如下图所示),那么我们必须改动Copy的内容,并进行重新的测试和编译。
改动后的程序如下所示:
enum OutputDevice {printer, disk};
void Copy(OutputDevice dev){
int c;
while((c = ReadKeyboard())!= EOF)
if(dev == printer)
WritePrinter(c);
else
WriteDisk(c);
}
如果要继续添加别的输入或输出方式,该程序还是无法重用,要对此程序进行修改才能继续使用。
利用依赖倒置原则(DIP),可以解决这个问题。DIP原则,可以从2点来解读:
第1点:高层模块不依赖底层模块,两者都依赖抽象
第2点:抽象不应该依赖于细节,细节应该依赖于抽象
上面所讲的例子如果用DIP原则,结果如下
class Reader {
public:
virtual int read()=0;
};
class Writer {
public:
virtual void write(int)=0;
};
void Copy(Reader& r, Writer& w){
int c;
while((c = r.read()) != EOF)
w.write(c);
}
这样一来,如果要添加新的输入或输出设备时只要改动相应的类(class Reader,Writer,利用多态来解决上面的问题)就可以了,而其它的程序都不用改动。这就是依赖倒置原则的基本内涵。
在软件设计和构建中我们要遵循“高内聚、低偶合”的原则。那么,依赖对于我们来说究竟是好事还是坏事呢?
首先应该明白的是,类之间如果是零偶合的状态是不能够构建应用程序的,只能构建类库。
但是由于人类的理解力和可控制的范围有限,大多数人难以理解和把握过于复杂的系统。把软件系统划分成多个模块,可以有效控制模块的复杂度,使每个模块都易于理解和维护。但在这种情况下,模块之间就必须以某种方式交换信息,也就是必然要发生某种耦合关系。如果某个模块和其它模块没有任何关联(哪怕只是潜在的或隐含的依赖关系),我们就几乎可以断定,该模块不属于此软件系统,应该从系统中剔除。如果所有模块之间都没有任何耦合关系,其结果必然是:整个软件不过是多个互不相干的系统的简单堆积,对每个系统而言,所有功能还是要在一个模块中实现,这等于没有做任何模块的分解。
因此,模块之间必定会有这样或那样的依赖关系,永远不要幻想消除所有依赖。但是,过强的耦合关系(如一个模块的变化会造成一个或多个其他模块也同时发生变化的依赖关系)会对软件系统的质量造成很大的危害。特别是当需求发生变化时,代码的维护成本将非常高。所以,我们必须想尽办法来控制和消解不必要的耦合,特别是那种会导致其它模块发生不可控变化的依赖关系。依赖倒置、控制反转就是人们在和依赖关系进行艰苦卓绝的斗争过程中不断产生和发展起来的。
我们下面来继续一步一步的说明这个问题。
看下面的程序:
#include <floppy.h>
....
void save() {
....
saveToFloppy()
}
}
由于save()程序依赖于saveToFloppy(),如果今天要更换低层的存储模块为Usb碟,则这个程序没有办法重用,必须加以修改才行,低层模块的更动造成了高层模块也必须跟着更动,这不是一个好的设计方式,我们希望模块都依赖于模块的抽象,这样才可以重用高层的业务设计。
如果以面向对象的方式来设计,依赖倒置(Dependency Inversion)的解释变为程序不应依赖实现,而是依赖于抽象,实现必须依赖于抽象。我们来看看下面这个Java程序:
BusinessObject.java
public class BusinessObject {
private FloppyWriter writer = new FloppyWriter();
....
public void save() {
...
writer.saveToFloppy();
}
}
public class FloppyWriter {
.... //相应的写盘的代码
}
在这个程序中,BusinessObject的存盘依赖于实际的FloppyWriter,如果今天我们想要将存盘改为存至Usb碟,我们必须修改或继承BusinessObject进行扩展,而无法直接使用BusinessObject。
浩劫只是刚刚开始,这时,你一定和我一样在期待着救世主的早日降临??接口(Interface)。你一定会说,面向对象的设计原则已经告诉我们了啊,“要针对接口编程”,你为什么不用呢?好,我们采用这个原则进行编程:
什么是接口?
接口定义了行为的协议,这些行为在继承接口的类中实现。
接口定义了很多方法,但是没有实现它们。类履行接口协议并实现所有定义在接口中的方法。
接口是一种只有声明没有实现的特殊类。
使用接口的优点:
Client不必知道其使用对象的具体所属类。
一个对象可以很容易地被(实现了相同接口的)的另一个对象所替换。
对象间的连接不必硬绑定(hardwire)到一个具体类的对象上,因此增加了灵活性。
松散藕合(loosens coupling)。
增加了重用的可能性。
通过面向接口编程,可以改?此一情?,例如:
public interface IDeviceWriter {
public void saveToDevice();
}
public class BusinessObject {
private IDeviceWriter writer;
public void setDeviceWriter(IDeviceWriter writer) {
this.writer = writer;
}
public void save() {
....
writer.saveToDevice();
}
}
这样一来,BusinessObject就是可重用的,如果今天我有存储至Floppy或Usb碟的需求,我只要实现IDeviceWriter即可,而不用修改BusinessObject:
public class FloppyWriter implement IDeviceWriter {
public void saveToDevice() {
....
// 实际储存至Floppy的程序代码
}
}
public class UsbDiskWriter implement IDeviceWriter {
public void saveToDevice() {
....
// 实际储存至UsbDisk的程序代码
}
}
从这个角度来看,Dependency Inversion的意思即是程序不依赖于实现,而是程序与实现都要依赖于抽象。
哦,这样一来,一切都OK了吗?还没有没有问题呢,你可能会有疑问,我要根据不同的情况来使用软盘、USB碟或者其它的存储设备,怎么办呢?在程序包中,BusinessObject不是还是和FloppyWriter或者UsbDiskWriter绑定吗,如果系统发布后,要将FloppyWriter替换为UsbDiskWriter不是还要去修改IDeviceWriter的实现吗?修改就意味着可能会带来错误,就要带来修改代码、进行测试、编译、维护等的工作量。还有更好的方法吗?IoC/DI就是解决之道。
IoC的Control是控制的意思,其实其背后的意义也是一种依赖关系的转移,如果A依赖于B,其意义即是B拥有控制权,我们要转移这种关系,所以依赖关系的反转即是控制关系的反转,藉由控制关系的转移,我们可以获得组件的可重用性,在上面的Java程序中,整个控制权从实际的 FloppyWriter转移至抽象的IDeviceWriter接口上,使得BusinessObject、FloppyWriter、 UsbDiskWriter这几个实现依赖于抽象的IDeviceWriter接口。
使用IoC,对象是被动的接受依赖类,而不是自己主动的去找。容器在实例化的时候主动将它的依赖类注入给它。可以这样理解:控制反转将类的主动权转移到接口上,依赖注入通过xml配置文件在类实例化时将其依赖类注入。
使用IoC,对象是被动的接受依赖类,而不是自己主动的去找。容器在实例化的时候主动将它的依赖类注入给它。可以这样理解:控制反转将类的主动权转移到接口上,依赖注入通过xml配置文件在类实例化时将其依赖类注入。
“控制反转(Inversion of Control)与依赖倒置原则(Dependency Inversion Principle)是一个同义原则。虽然“依赖倒置”和“控制反转”在设计层面上都是消解模块耦合的有效方法,也都是试图令具体的、易变的模块依赖于抽象的、稳定的模块的基本原则,但二者在使用语境和关注点上存在差异:“依赖倒置”强调的是对于传统的、源于面向过程设计思想的层次概念的“倒置”,而“控制反转”强调的是对程序流程控制权的反转。“依赖倒置”的使用范围更为宽泛一些。
IoC在容器的角度,可以用这么一句好莱坞名言来代表(著名的好莱坞原则):"Don't call me, I'll call you."(不要打电话给我们,我们会通知你的)。好莱坞的演员门(包括大牌)都会在好莱坞进行登记,他们不能够直接打电话给制片人或者导演要求演出摸个角色或者参加某个片子的演出,而是由好莱坞根据需要去通知他们(前提是他们已经在好莱坞做过登记)。在这里好莱坞就相当于容器。
以程序的术语来说的话,就是「不要向容器要求您所需要的(对象)资源,容器会自动将这些对象给您!」。IoC要求的是容器不侵入应用程序本身,应用程序本身提供好接口,容器可以透过这些接口将所需的资源注至程序中,应用程序不向容器主动要求资源,故而不会依赖于容器的组件,应用程序本身不会意识到正被容器使用,可以随时从容器中脱离转移而不用作任何的修改,而这个特性正是一些业务逻辑中间件最需要的。
3.依?注入DI
IoC模式基本上是一个高层的概念,在Martin Fowler的Inversion of Control Containers and the Dependency Injection pattern中谈到,实现IoC有两种方式:Dependency Injection与Service Locator。
Spring所采用的是Dependency Injection来实现IoC(多数容器都是采取这种方式的),中文翻译为依赖注入,依赖注入的意义是:保留抽象接口,让组件依赖于抽象接口,当组件要与其它实际的对象发生依赖关系时,藉过抽象接口来注入依赖的实际对象。
回锅头来再仔细研读一下我们在上面给出的例子:
public interface IDeviceWriter {
public void saveToDevice();
}
public class BusinessObject {
private IDeviceWriter writer;
public void setDeviceWriter(IDeviceWriter writer) {
this.writer = writer;
}
public void save() {
....
writer.saveToDevice();
}
}
public class FloppyWriter implement IDeviceWriter {
public void saveToDevice() {
....
// ???存至Floppy的程式?
}
}
public class UsbDiskWriter implement IDeviceWriter {
public void saveToDevice() {
....
// ???存至UsbDisk的程式?
}
}
为了让BusinessObject获得重用性,我们不让BusinessObject依赖于实际的FloppyWriter,而是依赖于抽象的接口。
在此代码中,首先IDeviceWriter的变量writer可以接收任何IDeviceWriter的实例,另外,FloppyWriter或UsbDiskWrite的实例不是通过BusinessObject来获得,而是通过setter(也可以用构造器)来由外部传给它。这似乎跟我们往常的代码没什么不同(回想一下Javabean的setter/getter),但这已经是一个良好的设计。现在的关键问题是:FloppyWriter或UsbDiskWrite的实例如何从外部注入给BusinessObject呢?这就要通过xml来实现了(相当于演员们在好莱坞登记)。
Spring的配置文件applicationContext.xml代码片断如下:
<beans>
<bean id = "FloppyWriter" class = "iocfirst.business.write" />
<bean
id = "BusinessObject"
class = "iocfirst.business.BusinessObject"
/>
<property name = " FloppyWriter">
<ref bean = " FloppyWriter " />
</property>
</bean>
</beans>
如果什么时候想将UsbDiskWrite注入,则修改applicationContext.xm即可。
单有了这个xml文件还不够,加载该xml文件呢?spring提供了现成的API,在加载上面的xml的时候,就进行了如下工作:实例化FloppyWriter或UsbDiskWrite类,实例化BusinessObject类,并将FloppyWriter或UsbDiskWrite的实例作为参数赋给了BusinessObject实例的setDeviceWriter方法。
BusinessObject依赖于抽象接口,在需要建立依赖关系时,我们就是通过抽象接口注入依赖的实际对象。
依赖注入在Martin Fowler的文章中谈到了三种实现方式:interface injection、setter injection与constructor injection。并分别称其为type 1 IoC、type 2 IoC与type 3 IoC。
Type1-接口注入(Interface Injection)
它是在一个接口中定义需要注入的信息,并通过接口完成注入。Apache Avalon是一个较为典型的Type1型IOC容器,WebWork框架的IoC容器也是Type1型。
Type2-设值方法注入(Setter Injection)
在各种类型的依赖注入模式中,设值注入模式在实际开发中得到了最广泛的应用(其中很大一部分得力于Spring框架的影响)。
基于设置模式的依赖注入机制更加直观、也更加自然。上面的BusinessObject所实现的是type 2 IoC,透过setter注入依赖关系。
Type3-构造子注入(Constructor Injection)
构造子注入,即通过构造函数完成依赖关系的设定。
目前我们只要了解到有这个3种方式就可以了,具体情况将在以后的章节中进行介绍。
4.开始spring之旅
任何需要交给spring管理的对象,都必须在配置文件中注册,这个过程被称为wiring,下面做一个最简单的演示,
第一步:下载Spring,Spring官方网站是 http://www.springframework.org/ 我们这里下载的是2.0.1版,spring-framework-2.0.1-with-dependencies.zip是该版本的压缩包,参见图2。
图 2
将压缩包的内容解压到一个目录中,D:/spring-framework-2.0.1是我这里使用的存放目录。
第二步:使用Eclipse的【new project】向导,新建一个名为spring-demo的工程。参见图3。
图3
第三步:新建一个名为HelloTalker的类,package选择com.spring.demo,如“图4”所示。
图 4
第四步:把所需要使用的Spring项目jar的文件加入到当前工程的Libraries中,本例中只使用Spring的简单IOC功能,只需要spring-beans.jar、spring-core.jar以及commons-logging.jar三个包即可。
右键点击工程名称,查看工程属性,选择工程属性中的【Java Builder Path】,然后选择【Libraries】选项,通过点击【add external jar】按钮来把外部的jar文件添加到工程项目中。参见图5所示:
图 5
第五步,录入HelloTalker.java的完整源代码。其中HelloTalker这个类有一个msg属性,为该属性编写set方法,该方法必须严格遵守javabean的命名规则即有一个setMsg方法用于设置msg属性的值。另外,我们直接在HelloTalker的main方法中使用Spring的Bean工厂来从配置文件中加载一个名为helloBean,类型为com.spring.demo.HelloTalker的Bean。然后调用这个Bean的sayHello方法。全部源代码如下所示:
package com.spring.demo;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
public class HelloTalker {
/**
* @param args
*/
private String msg;
public void setMsg(String msg)
{
this.msg=msg;
}
public void sayHello()
{
System.out.print(msg);
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Resource res=new ClassPathResource("com/spring/demo/springConfig.xml");
BeanFactory factory=new XmlBeanFactory(res);
HelloTalker hello=(HelloTalker)factory.getBean("helloBean");
hello.sayHello();
}
}
第六步,任何需要交给spring管理的对象,都必须在配置文件中注册,这个过程被称为wiring。因此,在与HelloTalker.java同级的目录下,建一个名为springConfig.xml(文件名可以任意定)的Spring配置文件,在这个Bean配置文件中定义helloBean,并通过依赖注入设置helloBean的msg属性值,其内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="helloBean" class="com.spring.demo.HelloTalker">
<property name="msg" value="Hello World的Spring示例!"/>
</bean>
</beans>
参见图6:
图6,工程完成后的情况
第七步:运行程序,在HelloTalker.java上点右键,选择【Run As】下面的【Java Application】。即可运行这个使用了Spring的程序,该程序将输出helloBean中的msg属性的值:“Hwllo World的Spring示例!”,如图7所示。
图7 程序运行的结果
至此为止,我们就完成了一个最简单的Spring应用实践。更多的内容将在后面的部分逐步给出。
4.注入方式
前面已经提到依赖注入方式由种,Spring鼓励的是setter injection,但也允许您使用constructor injection,使用setter或constructor来注入依赖关系视您的需求而定,使用constructor的好处之一是,您可以在建构对象的同时一并完成依赖关系的建立,然而如果要建立的对象关系很多,则会在建构函式上留下一长串的参数,这时使用setter会是个不错的选择,另一方面, setter可以有明确的名称可以了解注入的对象会是什么,像是setXXX()这样的名称会比记忆constructor上某个参数位置代表某个对象来得好。
前面讲的例子采用的是setter injection。Type 1 IoC是interface injection,使用type 1 IoC时会要求实现接口,这个接口是为容器所用的,容器知道接口上所规定的方法,它可以呼叫实现接口的对象来完成依赖关系的注入,例如:
public interface IDependencyInjection {
public void createDependency(Map dependObjects);
}
public class BusinessObject implement IDependencyInjection {
private Map dependObjects;
public void createDependency(Map dependObjects) {
this.dependObject = dependObjects;
// 在这边实现与BusinessObject的依赖关系
......
}
public void save() {
....
writer.saveToDevice();
}
}
如果要完成依赖关系注入的对象,必须实现IDependencyInjection接口,并交由容器管理,容器会呼叫被管理对象的createDependency()方法来完成依赖关系的建立。
在上面的例子中,type 1 IoC要求BusinessObject实现特定的接口,这就使得BusinessObject依赖于容器,如果日后BusinessObject要脱离目前这个容器,就必须修改程序,想想在更复杂的依赖关系中产生更多复杂的接口,组件与容器(框架)的依赖会更加复杂,最后使得组件无法从容器中脱离。
所以type 1 IoC具有强的侵入性,使用它来实现依赖注入会使得组件相依于容器(框架),降低组件的重用性。
Spring的核心是个IoC容器,您可以用setter或constructor的方式来实现您的业务对象,至于对象与对象之间的关系建立,则透过组态设定,让Spring在执行时期根据组态档的设定来为您建立对象之间的依赖关系。
那么,如何使用construtor injection呢?首先看看HelloBean:
HelloBean.java
package onlyfun.caterpillar;
public class HelloBean {
private String helloWord = "hello";
private String user = "NoBody";
public HelloBean(String helloWord, String user) {
this.helloWord = helloWord;
this.user = user;
}
public String sayHelloToUser() {
return helloWord + "!" + user + "!";
}
}
为了突显构造函式,我们这个HelloBean设计的简陋,只提供了构造函式与必要的sayHelloToUser(),我们来看看bean的定义档案:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING/DTD BEAN/EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="helloBean" class="onlyfun.caterpillar.HelloBean">
<constructor-arg index="0"><value>Greeting</value></constructor-arg>
<constructor-arg index="1"><value>Justin</value></constructor-arg>
</bean>
</beans>
在bean的定义档案中,我们使用<constructor-arg>来表示我们将使用constructor injection,由于使用constructor injection时并不如setter injection时拥有setXXX()这样易懂的名称,所以我们必须指定参数的位置索引,index属性就是用于指定我们的对象将注入至构造函式中的哪一个参数,索引值从0开始,符合Java索引的惯例。
来看看测试程序:
Test.java
package onlyfun.caterpillar;
import java.io.*;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = new FileInputStream("bean.xml");
BeanFactory factory = new XmlBeanFactory(is);
HelloBean hello = (HelloBean) factory.getBean("helloBean");
System.out.println(hello.sayHelloToUser());
}
}
几种依赖注入模式的特点如下:
接口注入模式因为历史较为悠久,在很多容器中都已经得到应用。但由于其在灵活性、易用性上不如
其他两种注入模式,因而在IOC的专题世界内并不被看好。
Type2和Type3型的依赖注入实现则是目前主流的IOC实现模式。这两种实现方式各有特点,也各具优势。
Type2 设值注入的优势
1. 对于习惯了传统JavaBean开发的程序员而言,通过setter方法设定依赖关系显得更加直观,更加自然。
2. 如果依赖关系(或继承关系)较为复杂,那么Type3模式的构造函数也会相当庞大(我们需要在构造函数中设定所有依赖关系),此时Type2模式往往更为简洁。
3. 对于某些第三方类库而言,可能要求我们的组件必须提供一个默认的构造函数(如Struts中的Action),此时Type3类型的依赖注入机制就体现出其局限性,难以完成我们期望的功能。
Type3 构造子注入的优势:
1. “在构造期即创建一个完整、合法的对象”,对于这条Java设计原则,Type3无疑是最好的响应者。
2. 避免了繁琐的setter方法的编写,所有依赖关系均在构造函数中设定,依赖关系集中呈现,更加易读。
3. 由于没有setter方法,依赖关系在构造时由容器一次性设定,因此组件在被创建之后即处于相对“不变”的稳定状态,无需担心上层代码在调用过程中执行setter方法对组件依赖关系产生破坏,特别是对于Singleton模式的组件而言,这可能对整个系统产生重大的影响。
4. 同样,由于关联关系仅在构造函数中表达,只有组件创建者需要关心组件内部的依赖关系。对调用者而言,组件中的依赖关系处于黑盒之中。对上层屏蔽不必要的信息,也为系统的层次清晰性提供了保证。
5. 通过构造子注入,意味着我们可以在构造函数中决定依赖关系的注入顺序,对于一个大量依赖外部服务的组件而言,依赖关系的获得顺序可能非常重要,比如某个依赖关系注入的先决条件是组件的UserDao及相关资源已经被设定。
可见,Type3和Type2模式各有千秋,而Spring、PicoContainer都对Type3和Type2类型的依赖注入机制提供了良好支持。这也就为我们提供了更多的选择余地。理论上,以Type3类型为主,辅之以Type2类型机制作为补充,可以达到最好的依赖注入效果,不过对于基于Spring Framework开发的应用而言,Type2使用更加广泛。