BeanFactory的XML之旅
bean的继承
假设我们某一天需要对FXNewsProvider使用继承进行扩展,那么可能会声明如下代码所示的子类定义:
实际上,我们想让该子类与FXNewsProvider使用相同的IFXNewsPersister,即DowJonesNewsPersister,那么可以使用如下方所示的配置。
但实际上,这种配置存在冗余,而且也没有表现两者之间的纵向关系。所以,我们可以引入XML 中的bean的“继承”配置,见代码:
我们在声明subNewsProvider的时候,使用了parent
属性,将其值指定为superNewsProvider,这样就继承了superNewsProvider定义的默认值,只需要将特定的属性进行更改,而不要全部又重新2定义一遍。
parent
属性还可以与abstract
属性结合使用,达到 将相应bean定义模板化的目的。比如,我们还可以像下方代码所演示的这样声明以上类定义。
newsProviderTemplate
的bean定义通过abstract
属性声明为true
,说明这个bean定义不需要实例化。实际上,这就是之前提到的可以不指定class属性的少数场景之一(当然,同时指定class和abstract=“true”也是可以的)。该bean定义只是一个配置模板,不对应任何对象。superNewsProvider
和subNewsProvider
通过parent指向这个模板定义,就拥有了该模板定义的所有属性配置。当多个bean定义拥有多个相同默认属性配置的时候,你会发现这种方式可以带来很大的便利。
另外,既然这里提到abstract,对它就多说几句。容器在初始化对象实例的时候,不会关注将abstract属性声明为true的bean定义。如果你不想容器在初始化的时候实例化某些对象,那么可以将其abstract属性赋值true,以避免容器将其实例化。对于ApplicationContext容器尤其如此,因为默认情况下,ApplicationContext会在容器启动的时候就对其管理的所有bean进行实例化,只有标志为abstract的bean除外。
bean 的 scope
BeanFactory除了拥有作为IoCServiceProvider的职责,作为一个轻量级容器,它还有着其他一些职责,其中就包括对象的生命周期管理 。
本节主要讲述容器中管理的对象的scope这个概念。多数中文资料在讲解bean的scope时喜欢用“ 作用域”这个名词,应该还算贴切吧。不过,我更希望告诉你scope这个词到底代表什么意思,至于你怎么称呼它反而不重要。
scope用来声明容器中的对象所应该处的限定场景或者说该对象的存活时间,即容器在对象进入其相应的scope之前,生成并装配这些对象,在该对象不再处于这些scope的限定之后,容器通常会销毁这些对象。
打个比方吧:我们都是处于社会(容器)中,如果把中学教师作为一个类定义,那么当容器初始化这些类之后,中学教师只能局限在中学这样的场景中;中学,就可以看作中学教师的scope。
Spring容器最初提供了两种bean的scope类型:singleton和prototype,但发布2.0之后,又引入了另外三种scope类型,即request、session和globalsession类型。不过这三种类型有所限制,只能在Web应用中使用。也就是说,只有在支持Web应用的ApplicationContext中使用这三个scope才是合理的。我们可以通过使用<bean>
的singleton或者scope属性来指定相应对象的scope,其中,scope属性只能在XSD格式的文档声明中使用,类似于如下代码所演示的形式:
让我们来看一下容器提供的这几个scope是如何限定相应对象的吧!
1. singleton
配置中的bean定义可以看作是一个模板,容器会根据这个模板来构造对象。但是要根据这个模板构造多少对象实例,又该让这些构造完的对象实例存活多久,则由容器根据bean定义的scope语意来决定。标记为拥有singleton scope的对象定义,在Spring的IoC容器中 只存在一个实例,所有对该对象的引用将共享这个实例。该实例从容器启动,并因为第一次被请求而初始化之后,将一直存活到容器退出,也就是说,它与IoC容器“几乎”拥有相同的“寿命”。
图4-5是Spring参考文档中所给出的singleton的bean的实例化和注入语意演示图例,或许可以更形象地说明问题。
需要注意的一点是,不要因为名字的原因而与GoF所提出的Singleton模式相混淆,二者的语意是不同的:标记为singleton的bean是由容器来保证这种类型的bean在同一个容器中只存在一个共享实例;而Singleton模式则是保证在同一个Classloader中只存在一个这种类型的实例。
可以从两个方面来看待singleton的bean所具有的特性。
-
对象实例数量。singleton类型的bean定义,在一个容器中只存在一个共享实例,所有对该类型bean的依赖都引用这一单一实例。这就好像每个幼儿园都会有一个滑梯一样,这个幼儿园的小朋友共同使用这一个滑梯。而对于该幼儿园容器来说,滑梯实际上就是一个singleton的bean。
-
对象存活时间。singleton类型bean定义,从容器启动,到它第一次被请求而实例化开始,只要容器不销毁或者退出,该类型bean的单一实例就会一直存活。
通常情况下,如果你不指定bean的scope,singleton便是容器默认的scope。
2. prototype
针对声明为拥有prototype scope的bean定义, 容器在接到该类型对象的请求的时候,会每次都重新生成一个新的对象实例给请求方。虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不再拥有当前返回对象的引用,请求方需要自己负责当前返回对象的后继生命周期的管理工作,包括该对象的销毁。也就是说,容器每次返回给请求方一个新的对象实例之后,就任由这个对象实例“自生自灭”了。
让我们继续幼儿园的比喻,看看prototype在这里应该映射到哪些事物。将苹果的bean定义的scope声明为prototype,在每个小朋友领取苹果的时候,我们都是分发一个新的苹果给他。发完之后,小朋友爱怎么吃怎么吃,爱什么时候吃什么时候吃。而如果你把苹果的bean定义的scope声明为singleton会是什么情况呢?如果第一个小朋友比较谦让,那么他可能对这个苹果只咬一口,但是下一个小朋友吃多少就不知道了。当吃得只剩一个果核的时候,下一个来吃苹果的小朋友肯定要哭鼻子的。
所以,对于那些请求方不能共享使用的对象类型,应该将其bean定义的scope设置为prototype。这样,每个请求方可以得到自己对应的一个对象实例,而不会出现上面“哭鼻子”的现象。通常,声明为prototype的scope的bean定义类型,都是一些有状态的,比如保存每个顾客信息的对象。
从Spring 参考文档上的这幅图片(见图4-6),你可以再次了解一下拥有prototype scope的bean定义,在实例化对象并注入依赖的时候,它的具体语意是个什么样子。
你用以下形式来指定某个bean定义的scope为prototype类型,效果是一样的:
3. request、session和global session
这三个scope类型是Spirng2.0之后新增加的,它们不像之前的singleton和prototype那么“通用”,因为它们只适用于Web应用程序,通常是与XmlWebApplicationContext共同使用,而这些将在第6部分详细讨论。不过,既然它们也属于scope的概念,这里就简单提几句。
只能使用scope属性才能指定这三种“bean的scope类型”。也就是说,你不得不使用基于XSD文档声明的XML配置文件格式。
- request
request通常的配置形式如下:
Spring容器,即XmlWebApplicationContext
会为每个HTTP请求创建一个全新的RequestProcessor对象供当前请求使用,当请求结束后,该对象实例的生命周期也就结束。当同时有10个HTTP请求进来的时候,容器会分别针对这10个请求返回10个全新的RequestProcessor
对象实例,且它们之间互不干扰。从不是很严格的意义上说,request可以看作prototype的一种特例,除了场景更加具体之外,语意上差不多。
- session
对于Web应用来说,放到session中的最普遍的信息就是用户的登录信息,对于这种放到session中的信息,我们可使用如下形式指定其scope为session:
Spring容器会为每个独立的session创建属于它们自己的全新的UserPreferences对象实例。与request相比,除了拥有session scope的bean的实例具有比request scope的bean可能更长的存活时间,其他方面真是没什么差别。
- global session
global session只有应用在基于portlet的Web应用程序中才有意义,它映射到portlet的global范围的session。如果在普通的基于servlet的Web应用中使用了这个类型的scope,容器会将其作为普通的session类型的scope对待。
4. 自定义scope类型
默认的singleton和prototype是硬编码到代码中的,而request、session和global session,包括自定义scope类型,则属于可扩展的scope行列,它们都实现了org.springframework.beans.factory.config.Scope接口。
要实现自己的scope类型,首先需要给出一个Scope接口的实现类,接口定义中的4个方法并非都是必须的,但get和remove方法必须实现。
有了Scope的实现类之后,我们需要把这个Scope注册到容器中,才能供相应的bean定义使用。通常情况下,我们可以使用ConfigurableBeanFactory
的以下方法注册自定义scope:
除了直接编码调用ConfigurableBeanFactory
的registerScope来注册scope,Spring还提供了一个专门用于统一注册自定义scope的BeanFactoryPostProcessor
实现(有关BeanFactoryPostProcessor的更多细节稍后将详述),即org.springframework.beans.factory.config.CustomScopeConfigurer
。对于ApplicationContext来说,因为它可以自动识别并加载BeanFactoryPostProcessor,所以我们就可以直接在配置文件中,通过这个CustomScopeConfigurer
注册来Scope实现类。
Warning
注(感觉这块用得很少,所以并没有把原文贴全)
工厂方法与 FactoryBean
Warning
这里可以读读 Spring 中的设计模式详解 _ JavaGuide(Java 面试 + 学习指南) ,然后结合后面内容进行参考学习
在强调“面向接口编程”的同时,有一点需要注意:虽然对象可以通过声明接口来避免对特定接口实现类的过度耦合,但总归需要一种方式将声明依赖接口的对象与接口实现类关联起来。否则,只依赖一个不做任何事情的接口是没有任何用处的。假设我们有一个像下方代码所声明的Foo类,它声明了一个BarInterface依赖。
如果该类是由我们设计并开发的,那么还好说,我们可以通过依赖注入,让容器帮助我们解除接口与实现类之间的耦合性。但是,有时,我们需要依赖第三方库,需要实例化并使用第三方库中的相关类,这时,接口与实现类的耦合性需要其他方式来避免。
通常的做法是通过使用工厂方法(Factory Method)模式, 提供一个工厂类来实例化具体的接口实现类,这样,主体对象只需要依赖工厂类,具体使用的实现类有变更的话,只是变更工厂类,而主体对象不需要做任何变动。下方代码演示了这种做法。
针对使用工厂方法模式实例化对象的方式,Spring的IoC容器同样提供了对应的集成支持。我们所要做的,只是将工厂类所返回的具体的接口实现类注入给主体对象 (这里是Foo)。
1. 静态工厂方法(Static Factory Method)
假设某个第三方库发布了BarInterface
,为了向使用该接口的客户端对象屏蔽以后可能对BarInterface实现类的变动,同时还提供了一个静态的工厂方法实现类StaticBarInterfaceFactory
,代码如下:
为了将该静态工厂方法类返回的实现注入Foo,我们使用以下方式进行配置(通过setter方法注入方式为Foo注入BarInterface的实例):
class
指定静态方法工厂类,factory-method
指定工厂方法名称,然后,容器调用该静态方法工厂类的指定工厂方法(getInstance
),并返回方法调用后的结果,即BarInterfaceImpl
的实例。也就是说,为foo注入的bar实际上是BarInterfaceImpl
的实例,即方法调用后的结果,而不是静态工厂方法类(StaticBarInterfaceFactory
)。我们可以实现自己的静态工厂方法类返回任意类型的对象实例,但工厂方法类的类型与工厂方法返回的类型没有必然的相同关系。
某些时候,有的工厂类的工厂方法可能需要参数来返回相应实例,而不一定非要像我们的getInstance()
这样没有任何参数。对于这种情况,可以通过<constructor-arg>
来指定工厂方法需要的参数,比如现在StaticBarInterfaceFactory
需要其他依赖来返回某个BarInterface
的实现,其定义可能如下:
为了让包含方法参数的工厂方法能够预期返回相应的实现类实例,我们可以像下方代码所演示的那样,通过<constructor-arg>
为工厂方法传入相应参数。
唯一需要注意的就是,针对静态工厂方法实现类的bean定义,使用<constructor-arg>
传入的是工厂方法的参数,而不是静态工厂方法实现类的构造方法的参数。(况且,静态工厂方法实现类也没有提供显式的构造方法。)
2. 非静态工厂方法(Instance Factory Method)
既然可以将静态工厂方法实现类的工厂方法调用结果作为bean注册到容器中,我们同样可以针对基于工厂类实例的工厂方法调用结果应用相同的功能,只不过,表达方式可能需要稍微变一下。
现在为BarInterface提供非静态的工厂方法实现类,该类定义如下代码所示:
因为工厂方法为非静态的,我们只能通过某个NonStaticBarInterfaceFactory实例来调用该方法(哦,错了,是容器来调用),那么也就有了如下的配置内容:
NonStaticBarInterfaceFactory
是作为正常的bean注册到容器的,而bar的定义则与静态工厂方法的定义有些不同。现在使用factory-bean
属性来指定工厂方法所在的工厂类实例,而不是通过class属性来指定工厂方法所在类的类型。指定工厂方法名则相同,都是通过factory-method
属性进行的。
如果非静态工厂方法调用时也需要提供参数的话,处理方式是与静态的工厂方法相似的,都可以通过<constructor-arg>
来指定方法调用参数。
3. FactoryBean
FactoryBean是Spring容器提供的一种可以扩展容器对象实例化逻辑的接口 ,请不要将其与容器名称BeanFactory相混淆。FactoryBean,其主语是Bean,定语为Factory,也就是说,它本身与其他注册到容器的对象一样,只是一个Bean而已,只不过,这种类型的Bean本身就是生产对象的工厂(Factory)。
当某些对象的实例化过程过于烦琐,通过XML配置过于复杂,使我们宁愿使用Java代码来完成这个实例化过程的时候,或者,某些第三方库不能直接注册到Spring容器的时候,就可以实现org.spring-framework.beans.factory.FactoryBean
接口,给出自己的对象实例化逻辑代码。
当然,不使用FactoryBean,而像通常那样实现自定义的工厂方法类也是可以的。不过,FactoryBean是Spring提供的对付这种情况的解决方式。
要实现并使用自己的FactoryBean其实很简单,org.springframework.beans.factory.FactoryBean
只定义了三个方法,如以下代码所示:
getObject()
方法会返回该FactoryBean“生产”的对象实例,我们需要实现该方法以给出自己的对象实例化逻辑。
getObjectType()
方法仅返回getObject()方法所返回的对象的类型,如果预先无法确定,则返回null。
isSingleton()
方法返回结果用于表明,工厂方法(getObject()
)所“生产”的对象是否要以singleton形式存在于容器中。如果以singleton形式存在,则返回true,否则返回false;
如果我们想每次得到的日期都是第二天,可以实现一个如下方代码所示的FactoryBean。
要使用NextDayDateFactoryBean
,只需要如下这样将其注册到容器即可:
配置上看不出与平常的bean定义有何不同,不过,只有当我们看到NextDayDateDisplayer
的定义的时候,才会知道FactoryBean的魔力到底在哪。NextDayDateDisplayer
的定义如下:
看到了嘛?NextDayDateDisplayer所声明的依赖dateOfNextDay的类型为DateTime,而不是NextDayDateFactoryBean。也就是说FactoryBean
类型的bean定义,通过正常的id引用,
容器返回的是FactoryBean所“生产”的对象类型,而非FactoryBean实现本身 。
如果一定要取得FactoryBean本身的话,可以通过在bean定义的id之前加前缀&
来达到目的。下方代码展示了获取FactoryBean本身与获取FactoryBean“生产”的对象之间的差别。
Spring容器内部许多地方了使用FactoryBean。下面是一些比较常见的FactoryBean实现,你可以参照FactoryBean的Javadoc以了解更多内容。
JndiObjectFactoryBean
LocalSessionFactoryBean
SqlMapClientFactoryBean
ProxyFactoryBean
TransactionProxyFactoryBean