说明:学习笔记主要内容来自spring技术手册,如有侵权,请联系[email protected]
参考目录
编号
|
名称
|
出处
|
作者
|
1
|
IoC 容器和Dependency Injection 模式
|
DependencyInjection.pdf
|
Martin Fowler
|
2
|
Spring开发指南
|
Spring开发指南.pdf
|
夏昕
|
3
|
Spring技术手册
|
《Spring技术手册》
|
林信良
|
4
|
Expert One-on-One J2EE Development Without EJB
|
《Expert One-on-One J2EE Development Without EJB》
|
Rod Johnson
Juergen Hoeller
|
5
|
文中示例项目源代码
|
http://zlsunnan.edudisk.cn
|
林信良
|
1. 依赖注入和服务定位器
J2EE
开发者常遇到的一个问题就是如何组装不同的程序元素:如果web控制器体系结构和数据库接口是由不同的团队所开发的,彼此几乎一无所知,应该如何让它们配合工作?很多框架尝试过解决这个问题,有几个框架索性朝这个方向发展,提供了更通用的“组装各层组件”的方案。这样的框架通常被称为“轻量级容器”,PicoContainer和Spring都在此列。在这些容器背后,一些有趣的设计原则发挥着作用。这些原则已经超越了特定容器的范畴,甚至已经超越了Java平台的范畴。
要消除应用程序对插件实现的依赖,可以有两种选择:Dependency Injection和Service Locater模式。我们先来看一个Spring中使用IOC的示例程序:
1.1. spring程序示例(SpringDemo项目)
Action
接口:
public interface
Action {
public
String execute(String str);
}
|
Action
的两个实现
public class
UpperAction
implements
Action {
private
String message;
public
String getMessage() {
return
message;
}
public void
setMessage(String string) {
message = string;
}
public
String execute(String str) {
return
(getMessage() + str).toUpperCase();
}
}
|
public class
LowerAction
implements
Action {
private
String message;
public
String getMessage() {
return
message;
}
public void
setMessage(String string) {
message = string;
}
public
String execute(String str) {
return
(getMessage()+str).toLowerCase();
}
}
|
配置文件bean.xml
<
beans
>
<
description
>
Spring Quick Start
description>
<
bean
id
=
"TheAction"
class
=
"com.raykey.spring.qs.UpperAction"
>
<
property
name
=
"message"
>
<
value
>
HeLLo
value>
property>
bean>
beans>
|
(请确保配置bean.xml位于工作路径之下,注意工作路径并不等同于CLASSPATH ,eclipse的默认工作路径为项目根路径,也就是.project文件所在的目录,而默认输出目录/bin是项目CLASSPATH的一部分,并非工作路径。)
测试代码:
public void
testQuickStart() {
ApplicationContext ctx=
new
FileSystemXmlApplicationContext(
"bean.xml"
);
Action action = (Action) ctx.getBean(
"TheAction"
);
System.out.println(action.execute(
"Rod Johnson"
));
}
|
仔细观察一下上面的代码,可以看到:
1
. 我们的所有程序代码中(除测试代码之外),并没有出现Spring中的任何组件。
2
. UpperAction和LowerAction的Message属性均由Spring通过读取配置文件(bean.xml)动态设置。
3
. 客户代码(这里就是我们的测试代码)仅仅面向接口编程,而无需知道实现类的具体名称。同时,我们可以很简单的通过修改配置文件来切换具体的底层实现类。
上面所说的这些,对于我们的实际开发有何帮助?
Ø
首先,我们的组件并不需要实现框架指定的接口,因此可以轻松的将组件从Spring中脱离,甚至不需要任何修改(这在基于EJB框架实现的应用中是难以想象的)。
Ø
其次,组件间的依赖关系减少,极大改善了代码的可重用性。现在假设我们回到传统的实现模式,应该如何处理?一般的处理办法也就是编写一个Helper类(辅助类),完成配置文件读写功能,然后在各个Action的构造函数中,调用这个Helper类设置message属性值。此时,我们的组件就与这个Helper类库建立了依赖关系,之后我们需要在其他系统中重用这个组件的话,也必须连同这个Helper类库一并移植。实际开发中,依赖关系往往并非如此简单,组件与项目基层代码之间复杂的关联,使得组件重用性大大下降。Spring通过依赖注入模式,将依赖关系从编码中脱离出来,从而大大降低了组件之间的耦合,实现了组件真正意义上的即插即用。这也是Spring最具价值的特性之一。
Ø
面向接口编程
。诚然,即使没有Spring,实现面向接口的设计也不困难。Spring对于面向接口设计的意义,在于它为面向接口编程提供了一个更加自然的平台。基于Spring开发,程序员会自然而然倾向于使用接口来定义不同层次之间的关联关系,这种自发的倾向性,来自于Spring所提供的简单舒适的依赖注入实现。Spring使得接口的定义和使用不再像传统编码过程中那么繁琐(传统编码过程中,引入一个接口,往往也意味着同时要引入一个Factory类,也许还有一个额外的配置文件及其读写代码)。
1.2. 依赖注入
1.2.1. 何为依赖注入?
IoC
,用白话来讲,就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。这也就是所谓“控制反转”的概念所在:控制权由应用代码中转到了外部容器,控制权的转移,是所谓反转。
正在业界为IoC争吵不休时,大师级人物Martin Fowler也站出来发话,以一篇经典文章《Inversion of Control Containers and the Dependency Injection pattern》为IoC正名,至此,IoC又获得了一个新的名字:“依赖注入 (Dependency Injection)”。相对IoC 而言,“依赖注入”的确更加准确的描述了这种古老而又时兴的设计理念。从名字上理解,
所谓依赖注入,即组件之间的依赖关系由容器在运行期决定,形象的来说,即由容器动态的将某种依赖关系注入到组件之中。相信大家对下面图片中的设备不会陌生:
图中三个设备都有一个共同点,都支持USB 接口。当我们需要将数据复制到外围存储设备时,可以根据情况,选择是保存在U盘还是USB硬盘,下面的操作大家也都轻车熟路,无非接通USB接口,然后在资源浏览器中将选定的文件拖放到指定的盘符。
笔记本电脑与外围存储设备通过预先指定的一个接口(USB)相连,对于笔记本而言,只是将用户指定的数据发送到USB接口,而这些数据何去何从,则由当前接入的USB设备决定。在USB备加载之前,笔记本不可能预料用户将在USB接口上接入何种设备,只有USB设备接入之后,这种设备之间的依赖关系才开始形成。
对应上面关于依赖注入机制的描述,在运行时(系统开机,USB 设备加载)由容器(运行在笔记本中的Windows操作系统)将依赖关系(笔记本依赖USB设备进行数据存取)注入到组件中(Windows文件访问组件)。这就是依赖注入模式在现实世界中的一个版本。
在不使用Dependency Injection模式以前,我们的模式的效果如下:
MovieLister
类既依赖于MovieFinder接口,也依赖于具体的实现类。我们当然希望MovieLister 类只依赖于接口,但我们要如何获得一个MovieFinder子类的实例呢?这里的问题就是:如何设计这个连接过程,使MovieLister 类在不知道实现类细节的前提下与其实例协同工作。以下是使用了依赖注入后的效果:
Dependency Injection
的基本思想是:用一个单独的对象(装配器)来获得MovieFinder的一个合适的实现,并将其实例赋给MovieLister类的一个字段。
1.2.2. 实现方式
依赖注入的形式主要有三种,我分别将它们叫做构造子注入(Constructor Injection)、设值方法注入(Setter Injection)和接口注入(Interface Injection)。
1.2.2.1. 构造子注入(PicoContainer)
注入过程:
class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
//
通过另一个类对其关系进行配置
private MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();
Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
pico.registerComponentImplementation(MovieFinder.class,ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
return pico;
}
|
使用过程:
public void testWithPico() {
MutablePicoContainer pico = configureContainer();
MovieLister lister = (MovieLister)
pico.getComponentInstance(MovieLister.class);
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West",movies[0].getTitle());
}
|
尽管在这里我使用了构造子注入,实际上PicoContainer 也支持设值方法注入,不过该项目的开发者更推荐使用构造子注入。
1.2.2.2. 设值方法注入(Spring)
Spring
框架是一个用途广泛的企业级Java 开发框架,其中包括了针对事务、持久化框架、web应用开发和JDBC 等常用功能的抽象。和PicoContainer 一样,它也同时支持构造子注入和设值方法注入,但该项目的开发者更推荐使用设值方法注入——恰好适合这个例子。
注入过程:
class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}
//
通过配置文件将其关联起来
|
使用过程:
public void testWithSpring() throws Exception {
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West",movies[0].getTitle());
}
|
1.2.2.3. 接口注入
除了前面两种注入技术,还可以在接口中定义需要注入的信息,并通过接口完成注入。Avalon框架就使用了类似的技术。
接口注入要求组件实现容器所规定的接口,这就使得组件依赖于容器的API,如果日后组件打算脱离目前这个容器到其它的容器或是框架的时候就必须修改程序,在更复杂的依赖关系中将产生更多复杂的接口,组件和容器(框架)的依赖会更复杂,最后使得组件无法从窗口中脱离。
1.2.2.4. 几种依赖注入模式的对比总结
接口注入模式因为具备侵入性,它要求组件必须与特定的接口相关联,因此并不被看好,实际使用有限。
Type3
构造子注入的优势:
1
.
“在构造期即创建一个完整、合法的对象”,对于这条Java设计原则,Type3无疑是最好的响应者。
2
. 避免了繁琐的setter方法的编写,所有依赖关系均在构造函数中设定,依赖关系集中呈现,更加易读。
3
. 由于没有setter方法,依赖关系在构造时由容器一次性设定,因此组件在被创建之后即处于相对“不变”的稳定状态,无需担心上层代码在调用过程中执行setter方法对组件依赖关系产生破坏,特别是对于Singleton模式的组件而言,这可能对整个系统产生重大的影响。
4
. 同样,由于关联关系仅在构造函数中表达,只有组件创建者需要关心组件内部的依赖关系。对调用者而言,组件中的依赖关系处于黑盒之中。对上层屏蔽不必要的信息,也为系统的层次清晰性提供了保证。
5
. 通过构造子注入,意味着我们可以在构造函数中决定依赖关系的注入顺序,对于一个大量依赖外部服务的组件而言,依赖关系的获得顺序可能非常重要,比如某个依赖关系注入的先决条件是组件的DataSource及相关资源已经被设定。
Type2
设值注入的优势:
1
. 对于习惯了传统JavaBean开发的程序员而言,通过setter方法设定依赖关系显得更加直观,更加自然。
2
. 如果依赖关系(或继承关系)较为复杂,那么Type3模式的构造函数也会相当庞大(我们需要在构造函数中设定所有依赖关系),此时Type2模式往往更为简洁。
3
. 对于某些第三方类库而言,可能要求我们的组件必须提供一个默认的构造函数(如Struts中的Action),此时Type3类型的依赖注入机制就体现出其局限性,难以完成我们期望的功能。
可见,Type2和Type3模式各有千秋,而Spring、PicoContainer都对Type2和Type3类型的依赖注入机制提供了良好支持。这也就为我们提供了更多的选择余地。理论上,以Type3类型为主,辅之以Type2类型机制作为补充,可以达到最好的依赖注入效果,不过对于基于Spring Framework开发的应用而言,Type2使用更加广泛。
1.3. 服务定位器(Service Locator)
依赖注入的最大好处在于:它消除了MovieLister类对具体MovieFinder实现类的依赖。这样一来,我就可以把MovieLister类交给朋友,让他们根据自己的环境插入一个合适的MovieFinder实现即可。不过,Dependency Injection模式并不是打破这层依赖关系的唯一手段,另一种方法是使用Service Locator 模式。这里不对服务定位器的实现形式做进一步的分析,请参阅参考目录是编号为1的资料。