参考《Spring揭秘》
1.IoC基本概念
1.1 IoC是什么 (让别人为你服务)
IoC:控制翻转(全称为 Inversion of control);或依赖注入(Dependency Injection)。
问题:如果我们依赖于某个类或者服务,最简单有效的方式就是直接在类的构造函数中新建相应的依赖类。可以发现,我们需要主动地去获取依赖的对象!但其实我们的目的是只要依赖这个对象的时候,它能够准备就绪,我们完全可以不管这个对象是自己找来的还是别人送来的。
分析:IoC就是为了帮助我们避免“大费周折”,它的反转,就反转在让你从原来的事必躬亲,转变为现在的享受服务。
通常情况下,被注入对象会直接依赖于被依赖对象。但是,在IoC的场景中,二者之间通过IoC Service
Provider来打交道,所有的被注入对象和依赖对象现在由IoC Service Provider统一管理。被注入对象需要
什么,直接跟IoC Service Provider招呼一声,后者就会把相应的被依赖对象注入到被注入对象中,从而
达到IoC Service Provider为被注入对象服务的目的。IoC Service Provider在这里就是通常的IoC容器所充
当的角色。从被注入对象的角度看,与之前直接寻求依赖对象相比,依赖对象的取得方式发生了反转,
控制也从被注入对象转到了IoC Service Provider那里。
1.2 依赖注入方式
现在我们有了IoC容器,不再需要事必躬亲的去获取我们依赖的对象。
被注入对象、被依赖对象、IoC容器 三者之间的关系,我来举个例子:我们(被注入对象)肚子饿了,需要一份午餐(被依赖对象),不再需要我们亲自跑到餐馆取餐(主动地去获取依赖的对象),外卖小哥(IoC)会帮我们取餐,然后送到我们家门前,通过我们在家门上的小洞递进来。这个通过家门上的小洞递进来的动作,就是依赖注入。
我们在门上预留的这个“小洞”,既是我们提供给外卖小哥递餐进来的一种方式,同时也是一种“通知”。如果我们需要的是一个汉堡,我们就在门上开一个汉堡形状的小洞;如果需要一个苹果,就在门上开一个苹果形状的小洞……洞的形状,只允许通过相应形状的食物。这个洞,既向外卖小哥表达了我们的需求(例如苹果形状的小洞代表我们想要一个苹果作为午餐),又是一个递送方式,到时候小哥会将与小洞形状相同的食物,通过这个小洞递送进屋子里。
我们将这个门上带有形状的小洞,称之为依赖注入方式。
我们暴露给外卖小哥的带有形状的小洞,在IoC模式中有3种具体的实现方式,称为3种依赖注入方式。
1.2.1构造方法注入
构造方法注入,就是被注入对象可以通过在其构造方法中声明依赖对象的参数列表, 让外部(通常是IoC容器)知道它需要哪些依赖对象。
IoC Service Provider会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。同一个对象是不可能被构造两次的,因此,被注入对象的构造乃至其整个生命周期,应该是由IoC Service Provider来管理的。
优点:构造方法注入方式比较直观,对象被构造完成后,即进入就绪状态,可以马上使用。
1.2.2 setter方法注入
对于JavaBean对象来说,通常会通过setXXX()和getXXX()方法来访问对应属性。这些setXXX()方法统称为setter方法,getXXX()当然就称为getter方法。通过setter方法,可以更改相应的对象属性,通过getter方法,可以获得相应属性的状态。所以,当前对象只要为其依赖对象所对应的属性添加setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。
优点:setter方法注入虽不像构造方法注入那样,让对象构造完成后即可使用,但相对来说更宽松一些,可以在对象构造完成后再注入
1.2.3 接口注入 (已过时)
被注入对象如果想要IoC Service Provider为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。
IoC Service Provider最终通过这些接口来了解应该为被注入对象注入什么依赖对象。
FXNewsProvider为了让IoC Service Provider为其注入所依赖的IFXNewsListener,首先需要实现IFXNewsListenerCallable接口,这个接口会声明一个injectNewsListner方法(方法名随意),该方法的参数,就是所依赖对象的类型。这样,InjectionServiceContainer对象,即对应的IoC Service Provider就可以通过这个接口方法将依赖对象注入到被注入对象FXNewsProvider当中。
在这种情况下,实现的接口和接口中声明的方法名称都不重要。重要的是接口中声明方法的参数类型,必须是“被注入对象”所依赖对象的类型。
1.2.4 总结
构造方法注入和setter方法注入因为其侵入性较弱,且易于理解和使用,所以是现在使用最多的注入方式;而接口注入因为侵入性较强,近年来已经不流行了。
1.3 IoC 的意义
从主动获取依赖关系的方式转向IoC方式,不只是一个方向上的改变,除了省事之外,简单的转变背后实际上蕴藏着更多的玄机。
使用IoC后,对象具有更好的可测试性、可重用性和可扩展性。
一句话来概括IoC:IoC是一种可以帮助我们解耦各业务对象间依赖关系的对象绑定方式!
2 IoC Service Provider
虽然业务对象可以通过IoC方式声明相应的依赖,但是最终仍然需要通过某种角色或者服务将这些相互依赖的对象绑定到一起,而IoC Service Provider就对应IoC场景中的这一角色。IoC Service Provider在这里是一个抽象出来的概念,它可以指代任何将IoC场景中的业务对象绑定到一起的实现方式。它可以是一段代码,也可以是一组相关的类,甚至可以是比较通用的IoC框架或者IoC容器实现。比如,可以通过以下代码绑定与新闻相关的对象。
这段代码就可以认为是这个场景中的IoC Service Provider,只不过比较简单,而且目的也过于单一罢了。要将系统中几十、几百甚至数以千计的业务对象绑定到一起,采用这种方式显然是不切实际的。通用性暂且不提,单单是写这些绑定代码也会是一种很糟糕的体验。不过,好在现在许多开源产品通过各种方式为我们做了这部分工作。所以,目前来看,我们只需要使用这些产品提供的服务就可以了。
Spring的IoC容器就是一个提供依赖注入服务的IoC Service Provider。
2.1 IoC Service Provider 的职责
- **业务对象的构建管理。**在IoC场景中,业务对象无需关心所依赖的对象如何构建如何取得,但这部分工作始终需要有人来做。所以,IoC Service Provider需要将对象的构建逻辑从客户端对象那里剥离出来,以免这部分逻辑污染业务对象的实现。
- **业务对象间的依赖绑定。**对于IoC Service Provider来说,这个职责是最艰巨也是最重要的,这是它的最终使命之所在。如果不能完成这个职责,那么,无论业务对象如何的“呼喊”,也不会得到依赖对象的任何响应(最常见的倒是会收到一个NullPointerException)。IoC Service Provider通过结合之前构建和管理的所有业务对象,以及各个业务对象间可以识别的依赖关系,将这些对象所依赖的对象注入绑定,从而保证每个业务对象在使用的时候,可以处于就绪状态。
2.2 IoC Service Provider 如何管理对象间的依赖关系
前面我们说过,被注入对象可以通过多种方式通知IoC Service Provider为其注入相应依赖。但问题在于,收到通知的IoC Service Provider是否就一定能够完全领会被注入对象的意图,并及时有效地为其提供想要的依赖呢?有些时候,事情可能并非像我们所想象的那样理所当然。
拿前面外卖小哥的例子来说,我们把我们需要的午餐的形状标识成门上的小洞,通过这种方式通知外卖小哥我们需要一个汉堡,但是外卖小哥需要知道我们需要的这个汉堡要去哪里取。对于IoC Service Provider来说,也同样需要知道自己所管理和掌握的被注入对象和依赖对象之间的对应关系:
- 它可以通过最基本的文本文件来记录被注入对象和其依赖对象之间的对应关系;
- 它也可以通过描述性较强的XML文件格式来记录对应信息;
- 它还可以通过编写代码的方式来注册这些对应信息;
- ……
IoC Service Provider产品使用的注册对象管理信息的方式主要有以下几种:
2.2.1 直接编码方式
当前大部分的IoC容器都应该支持直接编码方式,比如Spring。在容器启动之前,我们就可以通过程序编码的方式将被注入对象和依赖对象注册到容器中,并明确它们相互之间的依赖注入关系。
通过为相应的类指定对应的具体实例,可以告知IoC容器,当我们要这种类型的对象实例时,请将容器中注册的、对应的那个具体实例返回给我们。
2.2.2 配置文件方式
这是一种较为普遍的依赖注入关系管理方式。像普通文本文件、properties文件、XML文件等,都可以成为管理依赖注入关系的载体。不过,最为常见的,还是通过XML文件来管理对象注册和对象间依赖关系,比如Spring IoC容器。
我们就可以像以下代码清单所示的那样,通过“newsProvider”这个名字,从容器中取得已经组装好的FXNewsProvider并直接使用
2.2.3 元数据方式
这种方式的代表实现是Google Guice,这是Bob Lee在Java 5的注解和Generic的基础上开发的一套IoC框架。我们可以直接在类中使用元数据信息来标注各个对象之间的依赖关系,然后由Guice框架根据这些注解所提供的信息将这些对象组装后,交给客户端对象使用。
通过@Inject,我们指明需要IoC Service Provider通过构造方法注入方式,为FXNewsProvider注入其所依赖的对象。至于余下的依赖相关信息,在Guice中是由相应的Module来提供的,以下代码清单出了FXNewsProvider所使用的Module实现。
通过Module指定进一步的依赖注入相关信息之后,我们就可以直接从Guice那里取得最终已经注入完毕,并直接可用的对象了
当然,注解最终也要通过代码处理来确定最终的注入关系,从这点儿来说,注解方式可以算作编码方式的一种特殊情况。
2.3 小结
IoC Service Provider只是为了简化概念而提出的一个一般性的概念。我们将由一般到特殊,一起深入了解一个特定的IoC Service Provider实现产品,即Spring提供的IoC容器。
3.Spring的IoC容器之BeanFactory
Spring的IoC容器是一个IoC Service Provider,但是,这只是它被冠以IoC之名的部分原因,我们不能忽略的是“容器”。Spring的IoC容器是一个提供IoC支持的轻量级容器,除了基本的IoC支持,它作为轻量级容器还提供了IoC之外的支持。如在Spring的IoC容器之上,Spring还提供了相应的AOP框架支持、企业级服务集成等服务。Spring的IoC容器和IoC Service Provider所提供的服务之间存在一定的交集,二者的关系如图所示。
Spring提供了两种容器类型:BeanFactory和ApplicationContext。
- BeanFactory。基础类型IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略(lazy-load)。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。对于资源有限,并且功能要求不是很严格的场景,BeanFactory是比较合适的IoC容器选择。
- ApplicationContext。ApplicationContext在BeanFactory的基础上构建,是相对比较高级的容器实现,除了拥有BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如事件发布、国际化信息支持等。ApplicationContext所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory也会长一些。在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext类型的容器是比较合适的选择。
ApplicationContext间接继承自BeanFactory,所以说它是构建于BeanFactory之上的IoC容器。另外,在没有特殊指明的情况下,以BeanFactory为中心所讲述的内容同样适用于ApplicationContext,这一点需要明确一下,二者有差别的地方会在合适的位置给出解释。
既然Spring框架提倡使用POJO,那么把每个业务对象看作一个JavaBean对象,或许更容易理解为什么Spring的IoC基本容器会起这么一个名字。作为Spring提供的基本的IoC容器,BeanFactory可以完成作为IoC Service Provider的所有职责,包括业务对象的注册和对象间依赖关系的绑定。将应用所需的所有业务对象交给BeanFactory之后,剩下要做的,就是直接从BeanFactory取得最终组装完成并且可用的对象。至于这个最终业务对象如何组装,你不需要关心,BeanFactory会帮你搞定。所以,对于客户端来说,与BeanFactory打交道其实很简单,最基本地,BeanFactory肯定会公开一个取得组装完成的对象的方法接口:
上面代码中的方法基本上都是查询相关的方法,例如,取得某个对象的方法(getBean)、查询某个对象是否存在于容器中的方法(containsBean),或者取得某个bean的状态或者类型的方法等。因为通常情况下,对于独立的应用程序,只有主入口类才会跟容器的API直接耦合。
3.1 拥有BeanFactory之后的生活 (整体畅想)
确切地说,拥有BeanFactory之后的生活没有太大的变化。前后唯一的不同,就是对象之间依赖关系的解决方式改变了。这就是所谓的“拉拉扯扯的事情”。之前我们的系统业务对象需要自己去“拉”(Pull)所依赖的业务对象,有了BeanFactory之类的IoC容器之后,需要依赖什么让BeanFactory为我们推过来(Push)就行了。所以,简单点儿说,拥有BeanFactory之后,要使用IoC模式进行系统业务对象的开发。
在BeanFactory出现之前,我们通常会直接在应用程序的入口类的main方法中,自己实例化相应的对象并调用之,如以下代码所示:
FXNewsProvider newsProvider = new FXNewsProvider();
newsProvider.getAndPersistNews();
不过,现在既然有了BeanFactory,我们通常只需将“生产线图纸”交给BeanFactory,让BeanFactory为我们生产一个FXNewsProvider,如以下代码所示:
BeanFactory container = new XmlBeanFactory(new ClassPathResource("配置文件路径"));
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
ApplicationContext container = new ClassPathXmlApplicationContext("配置文件路径");
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
ApplicationContext container = new FileSystemXmlApplicationContext("配置文件路径");
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
3.2 BeanFactory的对象注册与依赖绑定方式
BeanFactory作为一个IoC Service Provider,为了能够明确管理各个业务对象以及业务对象之间的依赖绑定关系,同样需要某种途径来记录和管理这些信息。第2章中介绍了3种方式来管理这些信息。而BeanFactory几乎支持所有这些方式!
3.2.1 直接编码方式
把编码方式单独提出来称作一种方式并不十分恰当。因为不管什么方式,最终都需要编码才能“落实”所有信息并付诸使用。不过,通过这些代码,起码可以让我们更加清楚BeanFactory在底层是如何运作的。
public static void main(String[] args)
{
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory)bindViaCode(beanRegistry);
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaCode(BeanDefinitionRegistry registry)
{
AbstractBeanDefinition newsProvider = new RootBeanDefinition(FXNewsProvider.class,true);
AbstractBeanDefinition newsListener = new RootBeanDefinition(DowJonesNewsListener.class,true);
AbstractBeanDefinition newsPersister = new RootBeanDefinition(DowJonesNewsPersister.class,true);
// 将bean定义注册到容器中
registry.registerBeanDefinition("djNewsProvider", newsProvider);
registry.registerBeanDefinition("djListener", newsListener);
registry.registerBeanDefinition("djPersister", newsPersister);
// 指定依赖关系
// 1. 可以通过构造方法注入方式
ConstructorArgumentValues argValues = new ConstructorArgumentValues();
argValues.addIndexedArgumentValue(0, newsListener);
argValues.addIndexedArgumentValue(1, newsPersister);
newsProvider.setConstructorArgumentValues(argValues);
// 2. 或者通过setter方法注入方式
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.addPropertyValue(new PropertyValue("newsListener",newsListener));
propertyValues.addPropertyValue(new PropertyValue("newPersistener",newsPersister));
newsProvider.setPropertyValues(propertyValues);
// 绑定完成
return (BeanFactory)registry;
BeanFactory只是一个接口,我们最终需要一个该接口的实现来进行实际的Bean的管理,DefaultListableBeanFactory就是这么一个比较通用的BeanFactory实现类。DefaultListableBeanFactory除了间接地实现了BeanFactory接口,还实现了BeanDefinitionRegistry接口,该接口才是在BeanFactory的实现中担当Bean注册管理的角色。基本上,BeanFactory接口只定义如何访问容器内管理的Bean的方法,各个BeanFactory的具体实现类负责具体Bean的注册以及管理工作。
BeanDefinitionRegistry接口定义抽象了Bean的注册逻辑。通常情况下,具体的BeanFactory实现类会实现这个接口来管理Bean的注册。
打个比方说,BeanDefinitionRegistry就像图书馆的书架,所有的书是放在书架上的。虽然你还书或者借书都是跟图书馆(也就是BeanFactory,或许BookFactory可能更好些)打交道,但书架才是图书馆存放各类图书的地方。所以,书架相对于图书馆来说,就是它的“BookDefinitionRegistry”。每一个受管的对象,在容器中都会有一个BeanDefinition的实例(instance)与之相对应,该BeanDefinition的实例负责保存对象的所有必要信息,包括其对应的对象的class类型、是否是抽象类、构造方法参数以及其他属性等。当客户端向BeanFactory请求相应对象的时候,BeanFactory会通过这些信息为客户端返回一个完备可用的对象实例。RootBeanDefinition和ChildBeanDefinition是BeanDefinition的两个主要实现类。
3.2.2 外部配置文件方式
Spring的IoC容器支持两种配置文件格式:Properties文件格式和XML文件格式。
采用外部配置文件时,Spring的IoC容器有一个统一的处理方式。通常情况下,需要根据不同的外部配置文件格式,给出相应的BeanDefinitionReader实现类,由BeanDefinitionReader的相应实现类负责将相应的配置文件内容读取并映射到BeanDefinition,然后将映射后的BeanDefinition注册到一个BeanDefinitionRegistry,之后,BeanDefinitionRegistry即完成Bean的注册和加载。当然,大部分工作,包括解析文件格式、装配BeanDefinition之类的工作,都是由BeanDefinitionReader的相应实现类来做的,BeanDefinitionRegistry只不过负责保管而已。
BeanDefinitionRegistry beanRegistry = <某个BeanDefinitionRegistry实现类,通常为DefaultListableBeanFactory>;
BeanDefinitionReader beanDefinitionReader = new BeanDefinitionReaderImpl(beanRegistry);
beanDefinitionReader.loadBeanDefinitions("配置文件路径");
// 现在我们就取得了一个可用的BeanDefinitionRegistry实例
- Properties配置格式的加载
Spring提供了org.springframework.beans.factory.support.PropertiesBeanDefinitionReader类用于Properties格式配置文件的加载,所以,我们不用自己去实现BeanDefinitionReader,只要根据该类的读取规则,提供相应的配置文件即可。
djNewsProvider.(class)=..FXNewsProvider
# ----------通过构造方法注入的时候-------------
djNewsProvider.$0(ref)=djListener
djNewsProvider.$1(ref)=djPersister
# ----------通过setter方法注入的时候---------
# djNewsProvider.newsListener(ref)=djListener
# djNewsProvider.newPersistener(ref)=djPersister
djListener.(class)=..impl.DowJonesNewsListener
djPersister.(class)=..impl.DowJon
esNewsPersister
- XML配置格式的加载
XML配置格式是Spring支持最完整,功能最强大的表达方式。当然,一方面这得益于XML良好的语意表达能力;另一方面,就是Spring框架从开始就自始至终保持XML配置加载的统一性。同Properties配置加载类似,现在只不过是转而使用XML而已。
<?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="djNewsProvider" class="..FXNewsProvider">
<constructor-arg index="0">
<ref bean="djNewsListener"/>
</constructor-arg>
<constructor-arg index="1">
<ref bean="djNewsPersister"/>
</constructor-arg>
</bean>
<bean id="djNewsListener" class="..impl.DowJonesNewsListener">
</bean>
<bean id="djNewsPersister" class="..impl.DowJonesNewsPersister">
</bean>
</beans>
与为Properties配置文件格式提供PropertiesBeanDefinitionReader相对应,Spring同样为XML格式的配置文件提供了现成的BeanDefinitionReader实现,即XmlBeanDefinitionReader。XmlBeanDefinitionReader负责读取Spring指定格式的XML配置文件并解析,之后将解析后的文件内容映射到相应的BeanDefinition,并加载到相应的BeanDefinitionRegistry中。
除了提供XmlBeanDefinitionReader用于XML格式配置文件的加载,Spring还在DefaultListableBeanFactory的基础上构建了简化XML格式配置加载的XmlBeanFactory实现。
new XmlBeanFactory(new ClassPathResource("../news-config.xml"));
3.2.3 注解方式
在提到BeanFactory所支持的对象注册与依赖绑定方式的时候,说的是BeanFactory“几乎”支持IoC Service Provider可能使用的所有方式。之所以这么说,有两个原因。
- 在Spring 2.5发布之前,Spring框架并没有正式支持基于注解方式的依赖注入;
- Spring 2.5发布的基于注解的依赖注入方式,如果不使用classpath-scanning功能的话,仍然部分依赖于“基于XML配置文件”的依赖注入方式。
如果要通过注解标注的方式为FXNewsProvider注入所需要的依赖,现在可以使用@Autowired以及@Component对相关类进行标记。
@Component
public class FXNewsProvider
{
@Autowired
private IFXNewsListener newsListener;
@Autowired
private IFXNewsPersister newPersistener;
public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister)
{
this.newsListener = newsListner;
this.newPersistener = newsPersister;
}
...
}
@Component
public class DowJonesNewsListener implements IFXNewsListener
{
...
}
@Component
public class DowJonesNewsPersister implements IFXNewsPersister
{
...
}
@Autowired是这里的主角,它的存在将告知Spring容器需要为当前对象注入哪些依赖对象。而@Component则是配合Spring 2.5中新的classpath-scanning功能使用的。现在我们只要再向Spring的配置文件中增加一个“触发器”,使用@Autowired和@Component标注的类就能获得依赖对象的注入了。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<context:component-scan base-package="cn.spring21.project.base.package"/>
</beans>
<context:component-scan/>会到指定的包(package)下面扫描标注有@Component的类,如果找到,则将它们添加到容器进行管理,并根据它们所标注的@Autowired为这些类注入符合条件的依赖对象。
在以上所有这些工作都完成之后,我们就可以像通常那样加载配置并执行当前应用程序了,如以下代码所示:
public static void main(String[] args)
{
ApplicationContext ctx = new ClassPathXmlApplicationContext("配置文件路径");
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("FXNewsProvider");
newsProvider.getAndPersistNews();
}
3.3 BeanFactory的XML之旅
XML格式的容器信息管理方式是Spring提供的最为强大、支持最为全面的方式。
3.3.1 <beans>和<bean>
所有使用 XML 文件进行配置信息加载的 Spring IoC 容器,包括 BeanFactory 和
ApplicationContext的所有XML相应实现,都使用统一的XML格式。在Spring 2.0版本之前,这种格式由Spring提供的DTD规定,也就是说,所有的Spring容器加载的XML配置文件的头部,都需要以下形式的DOCTYPE声明:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
...
</beans>
从Spring 2.0版本之后,Spring在继续保持向前兼容的前提下,既可以继续使用DTD方式的DOCTYPE
进行配置文件格式的限定,又引入了基于XML Schema的文档声明。所以,Spring 2.0之后,同样可以
使用如下所示代码所展示的基于XSD的文档声明:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ➥
xmlns:util="http://www.springframework.org/schema/util" ➥
xmlns:jee="http://www.springframework.org/schema/jee" ➥
xmlns:lang="http://www.springframework.org/schema/lang" ➥
xmlns:aop="http://www.springframework.org/schema/aop" ➥
xmlns:tx="http://www.springframework.org/schema/tx" ➥
xsi:schemaLocation="http://www.springframework.org/schema/beans ➥
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd ➥
http://www.springframework.org/schema/util ➥
http://www.springframework.org/schema/util/spring-util-2.0.xsd ➥
http://www.springframework.org/schema/jee ➥
http://www.springframework.org/schema/jee/spring-jee-2.0.xsd ➥
http://www.springframework.org/schema/lang ➥
http://www.springframework.org/schema/lang/spring-lang-2.0.xsd ➥
http://www.springframework.org/schema/aop ➥
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd ➥
http://www.springframework.org/schema/tx ➥
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
</beans>
不过,不管使用哪一种形式的文档声明,实际上限定的元素基本上是相同的。
3.3.1.1 <beans>
<beans>是XML配置文件中最顶层的元素,它下面可以包含0或者1个<description>和多个<bean>以及<import>或者<alias>
<beans>作为所有<bean>的“统帅”,它拥有相应的属性(attribute)对所辖的<bean>进行统一的默认行为设置,包括如下几个。
- default-lazy-init。其值可以指定为true或者false,默认值为false。用来标志是否对所有的<bean>进行延迟初始化。
- default-autowire。可以取值为no、byName、byType、constructor以及autodetect。默认值为no,如果使用自动绑定的话,用来标志全体bean使用哪一种默认绑定方式。
- default-dependency-check。可以取值none、objects、simple以及all,默认值为none,即不做依赖检查。
- default-init-method。如果所管辖的<bean>按照某种规则,都有同样名称的初始化方法的话,可以在这里统一指定这个初始化方法名,而不用在每一个<bean>上都重复单独指定。
- default-destroy-method。与default-init-method相对应,如果所管辖的bean有按照某种规则使用了相同名称的对象销毁方法,可以通过这个属性统一指定。
3.3.1.2 <description>、<import>和<alias>
之所以把这几个元素放到一起讲解,是因为通常情况下它们不是必需的。
- 可以通过<description>在配置的文件中指定一些描述性的信息。通常情况下,该元素是省略的。当然,如果愿意,<description>随时可以为我们效劳。
- 通常情况下,可以根据模块功能或者层次关系,将配置信息分门别类地放到多个配置文件中。在想加载主要配置文件,并将主要配置文件所依赖的配置文件同时加载时,可以在这个主要的配置文件中通过<import>元素对其所依赖的配置文件进行引用。比如,如果A.xml中的<bean>定义可能依赖B.xml中的某些<bean>定义,那么就可以在A.xml中使用<import>将B.xml引入到A.xml,以类似于<import resource=“B.xml”/>的形式。
- 可以通过<alias>为某些<bean>起一些“外号”(别名),通常情况下是为了减少输入。比如,假设有个<bean>,它的名称为dataSourceForMasterDatabase,你可以为其添加一个<alias>,像这样<alias name=“dataSourceForMasterDatabase” alias=“masterDataSource”/>。以后通dataSourceForMasterDatabase或者masterDataSource来引用这个<bean>都可以,只要你觉得方便就行。
3.3.2 业务对象的注册 <bean>
如下代码演示了最基础的对象配置形式:
<bean id="djNewsListener" class="..impl.DowJonesNewsListener">
</bean>
id属性: 通过id属性来指定当前注册对象的beanName是什么。这里,通过id指定beanName为djNewsListener。实际上,并非任何情况下都需要指定每个<bean>的id,有些情况下,id可以省略,比如后面会提到的内部<bean>以及不需要根据beanName明确依赖关系的场合等。除了可以使用id来指定<bean>在容器中的标志,还可以使用name属性来指定<bean>的别名(alias)。比如,以上定义,我们还可以像如下代码这样,为其添加别名:
<bean id="djNewsListener" name="/news/djNewsListener,dowJonesNewsListener" class="..impl.DowJonesNewsListener">
</bean>
与id属性相比,name属性的灵活之处在于,name可以使用id不能使用的一些字符,比如/。而且还可以通过逗号、空格或者冒号分割指定多个name。name的作用跟使用为id指定多个别名基本相同。
**class属性:**每个注册到容器的对象都需要通过元素的class属性指定其类型,否则,容器可不知道这个对象到底是何方神圣。在大部分情况下,该属性是必须的。仅在少数情况下不需要指定,如后面将提到的在使用抽象配置模板的情况下。
3.3.3 对象间依赖关系的绑定
对象之间需要相互协作,在横向上它们存在一定的依赖性。而现在我们就是要看一下,在Spring的IoC容器的XML配置中,应该如何表达这种依赖性。既然业务对象现在都符合IoC的规则,那么要了解的表达方式其实也很简单,无非就是看一下构造方法注入和setter方法注入通过XML是如何表达的而已。
3.3.3.1构造方法注入的XML之道
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg ref="djNewsListener"/>
<constructor-arg ref="djNewsPersister"/>
</bean>
有些时候,容器在加载XML配置的时候,因为某些原因,无法明确配置项与对象的构造方法参数列表的一一对应关系,就需要请<constructor-arg>的type或者index属性出马。比如,对象存在多个构造方法,当参数列表数目相同而类型不同的时候,容器无法区分应该使用哪个构造方法来实例化对象,或者构造方法可能同时传入最少两个类型相同的对象。
type属性:
public class MockBusinessObject {
private String dependency1;
private int dependency2;
public MockBusinessObject(String dependency)
{
this.dependency1 = dependency;
}
public MockBusinessObject(int dependency)
{
this.dependency2 = dependency;
}
...
@Override
public String toString() {
return new ToStringBuilder(this).append("dependency1", dependency1).append("dependency2", dependency2).toString();
}
}
该类声明了两个构造方法,分别都只是传入一个参数,且参数类型不同。这时,我们可以进行配置,如以下代码所示:
<bean id="mockBO" class="..MockBusinessObject">
<constructor-arg type="int">
<value>111111</value>
</constructor-arg>
</bean>
**index属性:**当某个业务对象的构造方法同时传入了多个类型相同的参数时,Spring又该如何将这些配置中的信息与实际对象的参数一一对应呢?好在,如果配置项信息和对象参数可以按照顺序初步对应的话,Spring还是可以正常工作的。
3.3.3.2 setter方法注入的XML之道
与构造方法注入可以使用<constructor-arg>注入配置相对应,Spring为setter方法注入提供了<property>元素。<property>有一个name属性(attribute),用来指定该<property>将会注入的对象所对应的实例变量名称。
<bean id="djNewsProvider" class="..FXNewsProvider">
<property name="newsListener" ref="djNewsListener"/>
<property name="newPersistener" ref="djNewsPersister"/>
</bean>
使用<property>的setter方法注入和使用<constructor-arg>的构造方法注入并不是水火不容的。实际上,如果需要,可以同时使用这两个元素