Spring框架为POJO提供的各种服务共同组成了Spring的生命之树,如图1-1所示。
第2章 IoC的基本概念
2.1
IoC全称为Inversion of Control,中文通常翻译为“控制反转”,它还有一个别名叫做依赖注入(Dependency Injection)
为了更好地阐述IoC模式的概念,我们引入以下简单场景。
FX项目,经常需要近乎实时地为客户提供外汇新闻。通常情况下,都是先从不同的新闻社订阅新闻来源,然后通过批处理程序定时地到指定的新闻服务器抓取最新的外汇新闻,接着将这些新闻存入本地数据库,最后在FX系统的前台界面显示。
其中,FXNewsProvider需要依赖IFXNewsListener来帮助抓取新闻内容,并依赖IFXNewsPersister存储抓取的新闻。
// 代码清单2-1 FXNewsProvider类的实现
public class FXNewsProvider
{
private IFXNewsListener newsListener;
private IFXNewsPersister newPersistener;
public void getAndPersistNews()
{
String[] newsIds = newsListener.getAvailableNewsIds();
if(ArrayUtils.isEmpty(newsIds))
{
return;
}
for(String newsId : newsIds)
{
FXNewsBean newsBean = newsListener.getNewsByPK(newsId);
newPersistener.persistNews(newsBean);
newsListener.postProcessIfNecessary(newsId);
}
}
}
如果我们依赖于某个类或服务,最简单而有效的方式就是直接在类的构造函数中新建相应的依赖类。
// 代码清单2-2 构造IFXNewsProvider类的依赖类
public FXNewsProvider()
{
newsListener = new DowJonesNewsListener();
newPersistener = new DowJonesNewsPersister();
}
我们通常直接调用依赖对象所提供的某项服务,与其依赖对象都要主动地去获取,我们可以让IoC将某个依赖对象送过来
IoC的理念就是,让别人为你服务!在图2-1中,也就是让IoC Service Provider来为你服务!
通常情况下,被注入对象会直接依赖于被依赖对象。但是,在IoC的场景中,二者之间通过IoC ServiceProvider来打交道,所有的被注入对象和依赖对象现在由IoC Service Provider统一管理。
2.2
三种依赖注入的方式,即构造方法注入(constructor injection)、setter方法注入(setter injection)以及接口注入(interface injection)。
2.2.1 构造方法注入
构造方法注入,就是被注入对象可以通过在其构造方法中声明依赖对象的参数列表,让外部(通常是IoC容器)知道它需要哪些依赖对象
FXNewsProvider是被注入对象,newsListner和newsPersister是被依赖对象
// 代码清单2-3 FXNewsProvider构造方法定义
public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister) {
this.newsListener = newsListner;
this.newPersistener = newsPersister;
}
IoC Service Provider会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。同一个对象是不可能被构造两次的,因此,被注入对象的构造乃至其整个生命周期,应该是由IoC Service Provider来管理的。
这就好比你刚进酒吧的门,服务生已经将你喜欢的啤酒摆上了桌面一样。坐下就可马上享受一份清凉与惬意
2.2.2 setter方法注入
对于JavaBean对象来说,通常会通过setXXX()和getXXX()方法来访问对应属性。这些setXXX()方法统称为setter方法,getXXX()当然就称为getter方法。通过setter方法,可以更改相应的对象属性,通过getter方法,可以获得相应属性的状态。所以,当前对象只要为其依赖对象所对应的属性添加setter
方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。
// 代码清单2-4 添加了setter方法声明的FXNewsProvider
public class FXNewsProvider
{
private IFXNewsListener newsListener;
private IFXNewsPersister newPersistener;
public IFXNewsListener getNewsListener() {
return newsListener;
}
public void setNewsListener(IFXNewsListener newsListener) {
this.newsListener = newsListener;
}
public IFXNewsPersister getNewPersistener() {
return newPersistener;
}
public void setNewPersistener(IFXNewsPersister newPersistener) {
this.newPersistener = newPersistener;
}
}
这样,外界就可以通过调用setNewsListener和setNewPersistener方法为FXNewsProvider对象注入所依赖的对象了。
这就好比你可以到酒吧坐下后再决定要点什么啤酒,可以要百威,也可以要大雪,随意性比较强。如果你不急着喝,这种方式当然是最适合你的。
2.2.3 接口注入
被注入对象如果想要IoC ServiceProvider为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。IoC Service Provider最终通过这些接口来了解应该为被注入对象注入什么依赖对象。
图2-3演示了如何使用接口注入为FXNewsProvider注入依赖对象。
FXNewsProvider为了让IoC Service Provider为其注入所依赖的IFXNewsListener,首先需要实现IFXNewsListenerCallable接口,这个接口会声明一个injectNewsListner方法(方法名随意),该方法的参数,就是所依赖对象的类型。这样,InjectionServiceContainer对象,即对应的IoC Service Provider就可以通过这个接口方法将依赖对象注入到被注入对FXNewsProvider当中。
接口中声明方法的参数类型,必须是“被注入对象”所依赖对象的类型。
这就好像你同样在酒吧点啤酒,为了让服务生理解你的意思,你就必须戴上一顶啤酒杯式的帽子
2.2.4 三种注入方式的比较
接口注入。从注入方式的使用上来说,接口注入是现在不甚提倡的一种方式,基本处于“退役状态”。因为它强制被注入对象实现不必要的接口,带有侵入性。而构造方法注入和setter方法注入则不需要如此。
构造方法注入。这种注入方式的优点就是,对象在构造完成之后,即已进入就绪状态,可以 马上使用。缺点就是,当依赖对象比较多的时候,构造方法的参数列表会比较长。而通过反射构造对象的时候,对相同类型的参数的处理会比较困难,维护和使用上也比较麻烦。而且在Java中,构造方法无法被继承,无法设置默认值。对于非必须的依赖处理,可能需要引入多个构造方法,而参数数量的变动可能造成维护上的不便。
setter方法注入。因为方法可以命名,所以setter方法注入在描述性上要比构造方法注入好一些。 另外,setter方法可以被继承,允许设置默认值,而且有良好的IDE支持。缺点当然就是对象无法在构造完成后马上进入就绪状态。
综上所述,构造方法注入和setter方法注入因为其侵入性较弱,且易于理解和使用,所以是现在使用最多的注入方式;而接口注入因为侵入性较强,近年来已经不流行了
2.3 IoC 的附加值
IoC是一种可以帮助我们解耦各业务对象间依赖关系的对象绑定方式!不会对业务对象构成很强的侵入性,使用IoC后,对象具有更好的可测试性、可重用性和可扩展性,等等
如果有其他需求或变动(如多了新闻社叫MarketWin24。),没有用IoC时,对象跟DowJonesNewsListener是绑定的, 我们无法重用这个类了,为了解决问题, 我们可能要重新实现一个继承自FXNewsProvider的MarketWin24NewsProvider,或者干脆重新写一个类似的功能
而使用IoC,却完全可以不做任何改动,处理逻辑实际上应该是一样的:根据
各个公司的连接接口取得新闻,然后将取得的新闻存入数据库
因此,我们只要根据MarketWin24的新闻服务接口,为MarketWin24的FXNewsProvider提供相应的MarketWin24NewsListener注入就可
以了
// 代码清单2-5 构建在IoC之上可重用的FXNewsProvider使用演示
FXNewsProvider dowJonesNewsProvider = ➥
new FXNewsProvider(new DowJonesNewsListener(),new DowJonesNewsPersister());
...
FXNewsPrivider marketWin24NewsProvider = ➥
new FXNewsProvider(new MarketWin24NewsListener(),new DowJonesNewsPersister());
...
TDD(Test Driven Developement ,测试驱动开发)已经成为越来越受重视
的一种开发方式,保证业务对象拥有良好的可测试性,可以为最终交付高质量的软件奠定良好的基础,同时也拉起了产品质量的第一道安全网
设计开发可测试性良好的业务对象是至关重要的。而IoC模式可以让我们更容易达到这个目的
第三章 掌管大局的IoC Service Provider
IoC Service Provider在这里是一个抽象出来的概念,它可以指代任何将IoC场景中的业务对象绑定到一起的实现方式。它可以是一段代码,也可以是一组相关的类,甚至可以是比较通用的IoC框架或者IoC容器实现。
比如,可以通过以下代码(见代码清单3-1)绑定与新闻相关的对象。
// 代码清单3-1 FXNewsProvider相关依赖绑定代码
IFXNewsListener newsListener = new DowJonesNewsListener();
IFXNewsPersister newsPersister = new DowJonesNewsPersister();
FXNewsProvider newsProvider = new FXNewsProvider(newsListener,newsPersister);
newsProvider.getAndPersistNews();
这段代码就可以认为是这个场景中的IoC Service Provider,只不过比较简单,而且目的也过于单一。
Spring的IoC容器就是一个提供依赖注入服务的IoC Service Provider。
3.1 IoC Service Provider 的职责
IoC Service Provider的职责相对来说比较简单,主要有两个:业务对象的构建管理和业务对象间的依赖绑定。
业务对象的构建管理。在IoC场景中,业务对象无需关心所依赖的对象如何构建如何取得,但这部分工作始终需要有人来做。所以,IoC Service Provider需要将对象的构建逻辑从客户端对象那里剥离出来,以免这部分逻辑污染业务对象的实现。
业务对象间的依赖绑定。如果不能完成这个职责,那么,无论业务对象如何的“呼喊”,也不会得到依赖对象的任何响应(最常见的倒是会收到一个NullPointerException)。IoC Service Provider通过结合之前构建和管理的所有业务对象,以及各个业务对象间可以识别的依赖关系,将这些对象所依赖的对象注入绑定,从而保证每个业务对象在使用的时候,可以处于就绪状态。
3.2 运筹帷幄的秘密——IoC Service Provider 如何管理对象间的依赖关系
服务生最终必须知道顾客点的饮品与库存饮品的对应关系,才能为顾客端上适当的饮品。对于为被注入对象提供依赖注入的IoC Service Provider来说,它也同样需要知道自己所管理和掌握的被注入对象和依赖对象之间的对应关系。
IoC Service Provider有几种方式来记录诸多对象之间的对应关系。比如:
①它可以通过最基本的文本文件来记录被注入对象和其依赖对象之间的对应关系;
②它也可以通过描述性较强的XML文件格式来记录对应信息;
③它还可以通过编写代码的方式来注册这些对应信息;
④它也可以通过语音方式来记录对象间的依赖注入关系
3.2.1 直接编码方式
当前大部分的IoC容器都应该支持直接编码方式,比如PicoContainer、Spring、Avalon等
在容器启动之前,我们就可以通过程序编码的方式将被注入对象和依赖对象注册到容器中,并明确它们相互之间的依赖注入关系
//代码清单3-2 直接编码方式管理对象间的依赖注入关系
IoContainer container = ...;
container.register(FXNewsProvider.class,new FXNewsProvider());
container.register(IFXNewsListener.class,new DowJonesNewsListener());
...
FXNewsProvider newsProvider = (FXNewsProvider)container.get(FXNewsProvider.class);
newProvider.getAndPersistNews();
如果是接口注入,可能伪代码看起来要多一些。不过,道理上是一样的,只不过除了注册相应对象,还要将“注入标志接口”与相应的依赖对象绑定一下,才能让容器最终知道是一个什么样的对应
关系,如代码清单3-3所演示的那样
// 代码清单3-3 直接编码形式管理基于接口注入的依赖注入关系
IoContainer container = ...;
container.register(FXNewsProvider.class,new FXNewsProvider());
container.register(IFXNewsListener.class,new DowJonesNewsListener());
...
container.bind(IFXNewsListenerCallable.class, container.get(IFXNewsListener.class));
...
FXNewsProvider newsProvider = (FXNewsProvider)container.get(FXNewsProvider.class);
newProvider.getAndPersistNews();
通过bind方法将“被注入对象”(由IFXNewsListenerCallable接口添加标志)所依赖的对象,绑定为容器中注册过的IFXNewsListener类型的对象实例。容器在返回FXNewsProvider对象实例之前,会根据这个绑定信息,将IFXNewsListener注册到容器中的对象实例注入到“被注入对象”——FXNewsProvider中,并最终返回已经组装完毕的FXNewsProvider对象。
3.2.2 配置文件方式
像普通文本文件、properties文件、XML文件等,都可以成为管理依赖注入关系的载体。不过,最为常见的,还是通过XML文件来管理对象注册和对象间依赖关系,比如Spring IoC容器和在PicoContainer基础上扩展的NanoContainer,都是采用XML文件来管理和保存依赖注入信息的。对于我们例子中的FXNewsProvider来说,也可以通过Spring配置文件的方式(见代码清单3-4)来配置和管理各个对象间的依赖关系
//代码清单3-4 通过Spring的配置方式来管理FXNewsProvider的依赖注入关系
<bean id="newsProvider" class="..FXNewsProvider">
<property name="newsListener">
<ref bean="djNewsListener"/>
</property>
<property name="newPersistener">
<ref bean="djNewsPersister"/>
</property>
</bean>
<bean id="djNewsListener"
class="..impl.DowJonesNewsListener">
</bean>
<bean id="djNewsPersister"
class="..impl.DowJonesNewsPersister">
</bean>
最后,我们就可以像代码清单3-5所示的那样,通过“newsProvider”这个名字,从容器中取得已经组装好的FXNewsProvider并直接使用。
//代码清单3-5 从读取配置文件完成对象组装的容器中获取FXNewsProvider并使用
...
container.readConfigurationFiles(...);
FXNewsProvider newsProvider =(FXNewsProvider)container.getBean("newsProvider");
newsProvider.getAndPersistNews();
3.2.3 元数据方式
这种方式的代表实现是Google Guice,这是Bob Lee在Java 5的注解和Generic的基础上开发的一套IoC框架。我们可以直接在类中使用元数据信息来标注各个对象之间的依赖关系,然后由Guice框架根据这些注解所提供的信息将这些对象组装后,交给客户端对象使用。代码清单3-6演示了使用Guice的相应注解标注后的FXNewsProvider定义。
//代码清单3-6 使用Guice的注解标注依赖关