Spring提供了两种容器类型:BeanFactory和ApplicationContext。
- BeanFactory:基础类型的Ioc容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略。即只有当客户端对象需要访问容器中的某个受管对象时,才对该受管对象进行初始化以及依赖注入操作。所以相对来说,该容器启动初期速度较快,所需要的资源有限,因此BeanFactroy比较适合用于资源有限,并且功能要求不是很严格的场景。
- ApplicationContext:ApplicationContext是在BeanFactory的基础上构建的,是相对于高级的容器实现,因此ApplicationContext还提供了一些例如事件发布、国际化信息支持的高级特性。ApplicationContext所管理的对象,在该容器启动之后,默认全部初始化并绑定完成。所以相对BeanFactory来说,ApplicationContext要求更多的系统资源,同时启动时间也会长一些。因此ApplicationContext更适合于资源充足并且要求更多功能的场景中。
通过下图可以对BeanFactory和ApplicationContext之间的关系有一个更清晰的认知。
BeanFactory,顾名思义就是生产Bean的工厂,而Spring提倡使用POJO,所以可以把每个业务对象看作一个JavaBean对象。BeanFactory可以完成作为IoC Service Provider的所有职责,包括业务对象的注册和对象间依赖关系的绑定。
BeanFactory就像一个汽车生产厂。你从其他汽车零件厂商或者自己的零件生产部门取得汽车零件送入这个汽车生产厂,最后只需要从生产线的终点取得汽车成品就可以了。相似的,将应用程序所需的所有业务对象交给BeanFactory之后,剩下的就是直接从BeanFactory取得最终组装完成并且可用的对象。至于这个最终业务对象如何组装,你不需要担心,BeanFactory会帮你搞定。
拥有BeanFactory之后的生活
确切的说,拥有BeanFactory之后的生活没有太大的变化,有变化的也在只是一些拉拉扯扯的事情,客观一点就是对象之间依赖关系的解决方式改变了。之前系统业务对象需要自己去“拉”所依赖的业务对象,有了BeanFactory之类的IoC之后,需要依赖什么,让BeanFactory为我们推过来就行啦。以一个FX新闻系统的例子来说,FX新闻应用设计和实现框架代码如下:
1 | 1-设计FXNewsProvider类用于普遍的新闻处理 |
BeanFactory会说,这些都让我来干吧!此时BeanFactory说这些事情让他来做,但是它并不知道该怎么做,所以就需要你来教它怎么做。通常情况下,会使用XML文件配置的方式来教它,也就是告诉BeanFactory如何来注册并管理各个业务对象之间的依赖关系。下面是BeanFactory的XML配置方式实现业务对象间的依赖关系的一个例子代码:
1 | <beans> |
在BeanFactory出现之前,我们通常会直接在应用程序的入口类的main方法中,自己动手实例化相应的对象并调用,如以下代码:
1 | FXNewsProvider newsProvider = new FXNewsProvider(); |
不过现在有了BeanFactory,通过xml文件这张“图纸”告诉BeanFactory怎么做之后,让BeanFactory为我们生产一个FXNewsProvider,如以下代码所示:
1 | BeanFactory container = new XmlBeanFactory(new ClassPathResource("配置文件路径")); |
BeanFactory的对象注册与依赖绑定方式
为了让BeanFactory能够明确管理各个业务对象以及业务对象之间的依赖绑定关系,同样需要需要某种途径来记录和管理这些信息。与上一章的IoC Service Provider提到的三种方式,BeanFactory几乎支持所有这些方式。
直接编码方式
先看一下使用直接编码方式FX新闻系统相关类是如何注册并绑定的:
1 | public static void main(String[] args) { |
BeanFactory只是一个接口,我们最终需要一个该接口的实现来进行实际的Bean管理,DefaultListableBeanFactory就是这么一个比较通用的BeanFactory实现类。具体关系可以看下图:
BeanDefaultListableBeanFactory除了间接的实现了BeanFactory接口,还实现了BeanDefinitionRegistry接口,该接口才是现在BeanFactory的实现中担当Bean注册管理的角色。打个比方说,BeanDefinitionRegistry就像图书馆的数加,所有的书是放在书架上的,虽然你还书借书都是跟图书馆(也就是BeanFactory)打交道,但书架才是图书馆存放各类图书的地方。
基本上BeanFactory接口只定义如何访问容器内管理Bean的方法,各个BeanFactory的实现类才负责具体Bean的注册以及管理工作。BeanDefinitionRegistry接口定义抽象了Bean的注册逻辑。通常情况下,具体的BeanFactory实现类会实现这个接口来管理Bean的注册。
每一个受管的对象,在容器中都会有一个BeanDefinition的实例与之相对应,该BeanDefinition的实例负责保存对象的所有必要信息,包括其对应的对象的class类型、是否是抽象类、构造方法参数以及其他属性等。当客户端向BeanFactory请求相对应对象的时候,BeanFactory会通过这些信息为客户端返回一个完备可用的对象实例。
所以现在可以回到上面的代码中了,上面代码大致可以分为三个步骤:
- 在main方法中,首先构造一个DefaultListableBeanFactory作为BeanDefinitionRegistry,然后将其交给bindViaCode方式进行具体的对象注册和相关逻辑管理,然后就可以通过该方法返回的BeanFactory取得需要的对象。
- 在bindViaCode方法中,首先针对相应的业务对象构造与其相对应的BeanDefinition,使用了RootBeanDefinition作为BeanDefinition的实现类。构造完成后,将这些BeanDefinite注册到通过方法参数传进来的BeanDefinitionRegistry中。
- 之后我们可以通过构造方法,或者setter方法,为其注入相关依赖。最后以BeanFactory的形式返回已经注册并绑定了所有相关业务对象的BeanDefinitionRegistry实例。
外部配置文件方式
Spring的IoC容器支持两种配置文件格式:Properties文件格式和XML文件格式。
采用外部配置文件时,Spring的IoC荣有一个统一的处理方式。通常情况下,需根据不同的外部配置文件格式,给出相应的BeanDefinitionReader实现类,由BeanDefinitionReader的相应实现类负责将相应的配置文件的内容读取并映射到BeanDefinition,然后将映射后的BeanDefinition注册到一个BeanDefinitionRegistry,之后BeanDefinitionRegistry即完成Bean的注册和加载。
Properties配置格式的加载
Spring提供了PropertiesBeanDefiniteReader类用于Properties格式配置文件的加载,只需要根据该类的读取规则,提供相应的配置文件即可,示例代码如下:
1 | djNewsProvider.(class)=..FXNewsProvider |
这些内容都是特定于Spring的PropertiesBeanDefinitionReader的,下面是简单的语法介绍:
- djNewsProvider作为beanName,后面通过(class)表明对应的实现类是什么,实际上使用djNewsProvider.class=…的形式也是可以的,但是不提倡。
- 通过在表示beanName的名称后添加.$[number]后缀的形式,来表示当前beanName对应的对象需要通过构造方法注入的方式注入相应依赖对象。$0和1$1后面的(ref)用来表示所依赖的是引用对象,而不是普通的类型。如果不加(ref),则会将djListener和djPersister作为简单的String类型进行注入,异常也是自然不可避免了。
- setter方法注入与构造方法注入最大的区别就是,它不使用数字顺序来指定注入的位置,而使用相应的属性名称来指定注入,同样也不要忘掉(ref)。
在使用Properties文件配置完后,就可以使用了,下面是一个使用演示:
1 | public static void main(String[] args) { |
XML配置格式的加载
XML配置格式是Spring支持最完整,功能最强大,也是使用最频繁的表达方式。例子如下:
1 |
|
相对应的加载XML配置文件的BeanFactory的使用演示如下:
1 | public static void main(String[] args) { |
与Properties一样,XML同样有Spring提供的现成的BeanDefinitionReader实现,即XmlBeanDefinitionReader。
注解方式
注解是Java5之后才引入的,所以相对来说也更加简洁方便一些,使用注解的方式为FXNewsProvider注入所需要的依赖,现在可以使用@Autowired以及@Component对相关类进行标记。下面是FXNews使用注解标注后的情况。
1 |
|
@Autowired是这里的主角,它的存在将告知Spring容器需要为当前对象注入哪些依赖对象。而@Component则是配合Spring中的classpath-scanning功能使用。现在我们只要再向Spring的配置文件中增加一个“触发器”,使用@Autowired和@Component标注的类就能获得依赖对象的注入了。下面是在配置文件中配置的方法:
1 |
|
context:component-scan会到指定的包下面扫描有@Component的类,如果找到,则将他们添加到容器进行管理,并根据它们所标注的@Autowired为这些类注入符合条件的依赖对象。所以在做完上面的工作之后,就可以使用了:
1 | public static void main(String[] args) { |
XML配置方法
XML格式的容器信息管理方式是Spring提供的最为强大、支持最为全面的方式,因此在这里讲述一下XML文件的以标签和一些属性。
<beans>和<bean>
所有的Spring容器加载的XML配置文件的头部,都需要基于文档声明,而DTD和XSD的声明方式不一样,下面是一个对比:
1 | <!-- DTD方式 --> |
所有注册到容器的业务对象,在Spring中称之为Bean。所以,每一个对象在XML的映射中也自然而然的对应一个<bean>。既然容器最终可以管理所有的业务对象,那么XML中把这些叫做<bean>的元素组织起来,就叫做<beans>。
<beans>之唯我独尊
<beans>是XML配置我呢见中最顶层的元素,它下面可以包含0或者1个<description>和多个<bean>以及<import>或者<alias>。<beans>作为所有
- default-lazy-init:其值可以指定为true或者false,默认值为false。用来标志是否对所有的<bean>进行延迟初始化,但是指定为true时并不一定会延迟初始化,如果某个非延迟的bean依赖于该bean,那么该bean就不会延迟初始化。
- default-autowire:可以取值为no-不采取任何形式的自动绑定,完全依赖于手工明确配置、byName-与XML文件中声明的bean定义的beanName的值进行匹配、byType-会根据当前bean定义类型,按类型匹配、constructor-同样是按类型绑定,但是是匹配构造放的参数类型,而不是实例属性的类型,以及autodetect-byTye和constructor的结合体,如果是无参构造方法则是由byType,否则使用constructor模式。默认为no。如果使用自动绑定的话,用来标志全体bean使用哪一种默认绑定方式。
- default-dependency-check:可以取值为none-不检查、objects-只对对象引用类型依赖进行检查、simple-对简单属性类型以及相关的collection进行依赖检查,对象引用类型的依赖除外,以及all-simple和objects的结合,默认为none,即不做检查。
- default-init-method:如果所管辖的所有<bean>都有同样名称的初始化方法,可以在这里统一指定这个初始化方法名,而不用每一个单独指定。
- default-destroy-method:与default-init-method相对应,如果所管辖的所有<bean>都有同样名称的销毁方法,可以在这里统一指定这个销毁方法名,而不用每一个单独指定。
<description>、<import>和<alias>
这几个元素通常情况下不是必须的,这里只是了解一下。
- <description>:在配置文件中指定一些描述性的信息。
- <import>:可以在主要的配置文件中通过这个标签元素对其所依赖的配置文件引用,例如在A文件中<import resource=”B.xml”>
- <alias>:为某些<bean>起一些别名,通常情况下是为了减少输入。
孤孤单单的id和class
id属性
通常,每个注册到容器的对象都需要一个唯一标志来将其与同处一室的bean兄弟们区分开来,通过id属性来指定当前注册对象的beanName是什么。实际上并非任何情况下都需要指定每个<bean>的id,有些情况下可以省略,会在后面提到。
除了使用id来指定<bean>在容器中的标志,还可以使用name属性来指定<bean>的别名。name比id灵活之处在于,name可以使用id不能使用的一些字符,比如、。而且还可以通过逗号、空格或者冒号分割指定多个name。name的作用和alias的作用基本相同。
class属性
每个注册到容器的对象都需要通过<bean>元素的class属性指定其类型。在大部分情况下该属性都是必须的,仅在少数情况下不需要指定,后面会提到。
XML中的继承
1 | <bean id="newsProviderTemplate" abstract="true"> |
根据上面的代码可以看出,我们在声明superNewsProvider和subNewsProvider的时候,使用了parent属性,将其值指定为newsProviderTemplate,这样我们就继承了newsProviderTemplate定义的默认值,只需要将指定的属性进行更改,而不要全部又重新定义一遍。
parent属性还可以与abstract属性结合使用,达到将相应bean定义模板化的目的。newsProviderTemplate的bean定义通过abstract属性声明为true,说明这个bean定义不需要实例化,这就是之前提到的可以不指定class属性的少数场景之一。该bean只是配置一个模板,不对应任何对象,superNewsProvider和subNewsProvider通过parent指向这个模板定义,就拥有了该模板定义的所有属性配置。
容器在初始化对象实例的时候,不会关注abstract的bean。如果你不想容器在初始化对象实例的时候,那么可以将其abstract属性赋值为true,以避免容器将其实例化。同样对于ApplicationContext也是如此。
bean的scope(作用域)
scope用来声明容器中的对象所应该处的限定场景或者说该对象的存活时间,即容器在对象进入相应的scope之前,生产并装配这些对象,在该对象不在处于这些scope的限定之后,容器通常会销毁这些对象。
Spring最初提供了两种bean的scope类型:singleton和prototype,但是2.0发布之后,又引入了另外三种scope类型,即request、session和globa session类型,对应这三种类型只能在Web应用中使用。
singleton
标记为拥有singleton scope的对象定义,在Spring的IoC容器中只存在一个实例,所有对该对象的引用将共享这个实例,也就是说它与IoC容器的寿命“几乎”相同。
不要将这里的singleton与设计模式中的singleton相混淆。二者的语义是不同的:标记为singleton的bean是由容器来保证这种类型的bean在同一个容器中只存在一个共享实例;而Singleton模式则是保证在同一个Classloader中只存在一个这种类型的实例。
可以从两个方面看singleton的bean所具有的特性:
- 对象实例数量:singleton的bean定义,在一个容器中只存在一个共享实例,所有对该类型bean的依赖都引用这一单一实例。
- 对象存活时间:singleton类型bean定义,从容器启动,到它第一次被请求而实例化开始,只要容器不销毁或者退出,该类型bean的单一实例就会一直存活。
通常情况下,如果你不指定bean的scope,singleton便是容器默认的scope。
prototype
针对声明为拥有prototype scope的bean定义,容器在接到该类型对象的请求的时候,会每次都重新生成一个新的对象实例给请求方。虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不再拥有当前返回对象的引用,请求方需要自己负责当前返回对象的后继生命周期的管理工具,包括销毁。
所以对于那些请求不能共享使用的对象类型,应该将其bean定义的scope设置为prototype,这样可以用它来保存一些用户的状态信息。
request
Spring容器,即XmlWebApplicationContext会为每一个HTTP请求创建一个全新的RequestProcessor对象供当前请求使用,当请求结束后,该对象实例的生命周期即告结束。
session
Spring容器会为每个独立的session创建属于它们自己的全新的UserPreferences对象实例。与request相比,除了可能有更长的存活时间,其它方面真是没什么区别。
global session
global session只应用于基于porlet的Web应用程序中才有意义,在普通的基于servlet的Web应用中使用了这个类型的scope,容器会将其作为普通的session类型对待。
自定义scope
默认的singleton和prototype是硬编码到代码中的,而request、session和globa session,包括自定义scope类型,都实现了Scope接口,该接口定义如下:
1 | public interface Scope { |
要实现自己的scope类型,首先要给出一个Scope接口的实现类,并非4个方法都是必须的,但get和remove方法必须实现。下面是一个例子:
1 | public class ThreadScope implements Scope { |
接下来就是把这个新定义的scope注册到容器中,才能供相应的bean使用。我们有两种方式可以注册。
一种就是我们可以使用ConfigurableBeanFactory的以下方法去注册:void registerScope(String scopeName, Scope scope),其中参数scopeName就是使用的bean定义可以指定的名称,参数scope就是我们提供的scope实现类实例。注册例子如下:
1 | Scope threadScope = new ThreadScope(); |
另一种方式就是可以在xml文件中配置CustomScopeConfigurer来注册scope。例如“
1 | <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> |
接下来就可以使用这个自定义的scope了,例如:
1 | <bean id="beanName" class="..." scope="thread"> |
工厂方法与FactoryBean
工厂方法(Factory Method)模式,提供一个工厂类来实例化具体的接口实现类,这样主体对象只需要依赖工厂类,具体使用的实现类有变更的话,只是变更工厂类,而主体对象不需要做任何变动,这样就避免了接口与实现类的耦合性。
针对这种方式,Spring的IoC容器同样提供了对应的集成支持,我们要做的,只是将工厂类所返回的具体的接口实现类注入给主体对象。
静态工厂方法
假设现在有一个接口BarInterface,为了向使用该接口的客户端对象屏蔽以后可能对BarInterface实现类的变动,因此还提供了一个静态的工厂方法实现类StaticBarInterfaceFactory,代码如下:
1 | public class StaticBarInterfaceFactory { |
为了将该静态方法类返回的实现注入Foo,我们使用以下方式进行配置:
1 | <bean id="foo" class="...Foo"> |
class指定静态方法工厂类,factory-method指定工厂方法名称,然后,容器调用该静态方法工厂类的指定工厂方法,并返回方法调用后的结果,即BarInterfaceImpl的实例。
某些时候,有的工厂类的工厂方法需要参数来返回相应实例,可以通过<constructor-arg>来指定工厂方法需要的参数:
1 | public class StaticBarInterfaceFactory { |
1 | <bean id="foo" class="...Foo"> |
唯一需要注意的是,针对静态工厂方法实现类的bean定义,使用<constructor-arg>传入的是工厂方法的参数,而不是静态工厂方法的构造方法的参数,况且,静态工厂方法实现类也没有提供显式的构造方法。
非静态工厂方法
针对非静态方法的调用方式也很简单,只是需要稍微该变一下代码即可:
1 | public class NonStaticBarInterfaceFactory { |
1 | <bean id="foo" class="...Foo"> |
现在的最主要不同就在于是使用factory-bean属性来指定工厂方法所在的工厂类实例,而不是通过class属性来指定工厂方法所在类的类型。如果需要参数的话,配置方法同静态工厂方法一样。
FactoryBean
当某些对象的实例化过程因为繁琐,使用XML配置过于复杂时,就可以考虑通过代码的方式来完成这个实例化过程,FactoryBean接口就是为此而生的。FactoryBean接口只定义了三个方法,如下:
1 | public interface FactoryBean { |
- getObject()方法会返回该FactoryBean生产的对象实例,我们需要实现该方法以给出自己的对象实例化逻辑;
- getObjectType()方法仅返回getObject()方法所返回的对象的类型,如果预先无法确定,则返回null;
- isSingleton()方法返回结果用于表明是否为单例实例。
这是一个例子,得到第二天的日期:
1 | public class NextDayDateFactoryBean implements FactoryBean { |
接下来只需要将其注册到容器中即可:
1 | <bean id="nextDayDateDisplayer" class="...NextDayDateDisplayer"> |
接下来就是最重要的地方,来看看NextDayDateDisplayer的定义:
1 | public class NextDayDateDisplayer { |
NextDayDateDisplayer所声明的依赖dateOfNextDay的类型为DateTime,而不是NextDayDateFactoryBean,也就是说FactoryBean类型的bean定义,通过正常id引用,容器返回的时FactoryBean所生产的对象类型,而非其本身。当然如果一定要取得FactoryBean本身的话,可以通过bean定义的id之前加前缀“&”来达到目的。
方法注入以及方法替换
在引出标题内容之前,需要先提一下有关bean的scope的prototype属性的陷阱,我们知道,拥有prototype类型scope的bean,在请求方法每次向容器请求该类型对象的时候,容器都会返回一个全新的该对象实例,下面看看这个情况:
1 | public class MockNewsPersister implements IFXNewsPersister { |
配置为:
1 | <bean id="newsBean" class="..domain.FXNewsBean" singleton="false"> |
当我们多次调用MockNewsPersister的persistNews时,看看结果:
1 | BeanFactory container = new XmlBeanFactory(new ClassPathResource("..")); |
从输出看对象实例是相同的,而这与我们的初衷时相悖的。问题实际上不出在FXNewsBean的scope类型是否是prototype的,而是出在实例的取得方式上面。虽然FXNewsBean拥有prototype类型的scope,但当容器将一个FXNewsBean的实例注入MockNewsPersister之后,MockNewsPersister就会一直持有这个FXNewsBean实例的引用。虽然每次输出都调用了getNewBean()方法并返回一个FXNewsBean的实例,但实际上每次返回的都是MockNewsPersister持有的容器第一次注入的实例。这就是问题之所在。
换句话说,第一个实例注入后,MockNewsPersister再也没有重新想容器申请新的实例,所以容器也不会重新为其注入新的FXNewsBean类型的实例。
下面就是介绍解决这个问题的方法了。
方法注入
若要使用方法注入的方式,该方法必须能够被子类实现或者覆写,因为容器会为我们要进行方法注入的对象使用Cglib动态生成一个子类实现,从而代替当前对象,既然我们的getNewsBean()方法已经满足以上方法声明要求,就只需要正确配置该类了:
1 | <bean id="newsBean" class="..domain.FXNewsBean" singleton="false"> |
通过<lookup-method>的name属性指定需要注入的方法名,bean属性指定需要注入的对象,当getNewsBean方法被调用的时候,容器可以每次返回一个新的FXNewsBean类型的实例。所以这个时候再次检查执行结果,输出的实例引用就是不同的了。
殊途同归
除了使用方法注入来达到“每次调用都让容器返回新的对象实例”的目的,还可以使用其他方式达到相同的目的:
- 使用BeanFactoryAware:我们知道,即使没有方法注入,只要在实现getNewsBean()方法的时候,能够保证每次调用BeanFactory的getBean(“newsBean”),就同样可以每次都取得新的FXNewsBean对象实例。BeanFactoryAware接口就可以帮我们完成这一步,其定义如下:
1
2
3public interface BeanFactoryAware {
void setBeanFactory(BeanFactory beanFactory) throws BeansException;
}
实现BeanFactoryAware接口和配置的代码如下:
1 | public class MockNewsPersister implements IFXNewsPersister,BeanFactoryAware { |
1 | <bean id="newsBean" class="..domain.FXNewsBean" singleton="false"> |
- 使用ObjectFatoryCreatingFatoryBean:实际上ObjectFatoryCreatingFatoryBean实现了BeanFactoryAware接口,它返回的ObjectFactory实例这是特定于与Spring容器进行交互的一个实现而已。使用它的好处就是,隔离了客户端对象对BeanFactory的直接引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class MockNewsPersister implements IFXNewsPersister {
private ObjectFactory newsBeanFactory;
public void persistNews(FXNewsBean bean) {
persistNews();
}
public void persistNews() {
System.out.println("persist bean:"+getNewsBean());
}
public FXNewsBean getNewsBean() {
return newsBeanFactory.getObject();
}
public void setNewsBeanFactory(ObjectFactory newsBeanFactory) {
this.newsBeanFactory = newsBeanFactory;
}
}
1 | <bean id="newsBean" class="..domain.FXNewsBean" singleton="false"> |
方法替换
基本上可以认为,方法替换可以帮助我们实现简单的方法拦截功能。假设某天我看FXNewsProvider不爽,想替换掉它的getAndPersistNews方法默认逻辑,这是我就可以用方法替换将它的原有逻辑给替换掉。
替换的方法就是实现一个叫做MethodReplacer接口,简单的实现例子如下:
1 | public class FXNewsProviderMethodReplacer implements MethodReplacer { |
接下来把它配置到xml文件中就可以了:
1 | <bean id="djNewsProvider" class="..FXNewsProvider"> |
容器加载过程
先看一张IoC容器的工作框图:
两大阶段
IoC容器所起的作用就如上图所展示的那样,他会以某种方式加载Configuration MetaData(通常为XML配置文件),然后根据这些信息绑定整个系统对象,最终组装成一个可用的基于轻量级容器的应用系统。
Spring的IoC容器实现以上功能的过程,基本上分为两个阶段:容器启动阶段和Bean的实例化阶段。
容器启动阶段
容器启动伊始,首先会通过某种途径加载Configuration MetaData。除了代码方式比较直接,大部分情况下都会依赖于工具类BeanDefinitionReader对加载的配置文件进行解析和分析,并将分析后的信息编组为相应的BeanDefinition,最后把这些保存了bean定义必要信息的BeanDefinition,注册到相应的BeanDefinitionRegistry,这样启动阶段就完成了。
Bean实例化阶段
该阶段,容器会首先检查所请求的对象之前是否已经实例化。如果没有,则会根据注册的BeanDefinition所提供的信息实例化被请求对象,并为其注入依赖。如果该对象实现了某些回调接口,也会根据回调接口的要求来装配它。当该对象装配完毕后,容器会立即将其返回请求方使用。
插手两大阶段
Spring提供了一种叫做BeanFactoryPostProcessor的容器扩展机制,该机制允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做相应的修改。这就相当于在容器第一阶段结束之后,第二阶段开始之前,加入一道工序,让我们对最终的BeanDefinition做一些额外的操作,比如修改其中bean定义的某些属性,为bean定义增加其他信息等。
如果自定义实现BeanFactoryPostProcessor,通常实现接口BeanFactoryPostProcessor就可以了,如果有多个自定义实现的类,则实现Order接口来规定它们的顺序。
但是Spring为我们提供了三种常用的已经实现好的BeanFactoryPostProcessor实现类,它们分别是 PropertyPlaceholderConfigurer、PropertyOverrideConfigurer和CustomEditorConfigure。在介绍这三种实现类之前,先需要了解它们的配置方法是什么,很简单,只需要在XML配置文件中加入以下代码即可:
1 | ... |
PropertyPlaceholderConfigurer
通常情况下,对于MySQL数据库或者Redis数据库之类的配置,为了不将这些系统管理信息相关的信息与业务对象相关的信息混杂到XML文件中,会单独把这些信息配置到一个.properties文件中。PropertyPlaceholderConfigurer允许我们在XML配置文件中使用占位符,并将这些占位符所代表的资源单独配置到见简单的properties文件中来加载,例如最常见的:
1 | <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> |
对应的properties文件内容则为:
1 | jdbc.url=jdbc:mysql://server/MAIN?useUnicode=true&characterEncoding=ms932&failOverReadOnly=false&roundRobinLoadBalance=true |
基本就如上面所说的那样,当BeanFactory完成第一阶段加载完成所有配置信息之后,BeanFactory中保存的对象的属性信息还只是以占位符的形式存在,如”${jdbc.url}”。当PropertyPlaceholderConfigurer被应用时,他会使用properties配置文件中的配置信息来替换相应的BeanDefintion中占位符所表示的属性值。PropertyPlaceholderConfigurer不单会从其配置的properties文件中加载配置项,同时还会检查Java的System类中的Properties。
PropertyOverrideConfigurer
PropertyOverrideConfigurer的作用就是,通过它可以对容器中篇日志的任何你想处理的bean定义的property信息进行覆盖替换。比如之前的dataSource定义中,maxActive的值为100,如果我们觉得不合适,那么可以通过PropertyOverrideConfigurer将其覆盖为200:dataSource.maxActive=200。
如果要对容器中的某些bean定义的property信息进行覆盖,我们需要按照如下规则提供一个PropertyOverrideConfigurer使用的配置文件:beanName.propertyName=value。然后通过以下方法将PropertyOverrideConfigurer注册到容器即可:
1 | <bean class="org.springframework.beans.factory.config.PropertyOverrideConfigurer"> |
当容器中配置的多个PropertyOverrideConfigurer对同一个bean定义的同一个property值进行处理的时候,最后一个会生效。
CustomEditorConfigure
因为我们配置XML文件都是String类型,即容器从XML格式的文件中读取的都是字符串形式,最终应用程序却是由各种类型的对象所构成的。要想完成这种由字符串到具体对象的转换,都需要这种转换规则相关的信息,而CustomEditorConfigure就是帮我们传达类似信息的。
bean的生命周期
接下来就是第二大阶段了,即bean实例化阶段的实现逻辑。这里还需要再提一次BeanFactory和ApplicationContext对于bean加载的区别:
- BeanFactory对象实例化默认采用延迟初始化。
- ApplicationContext启动之后会实例化所有的bean定义。
当调用getBean()方法时,内部发现该bean定义之前还没有被实例化之后,会通过createBean()方法来进行具体的对象实例化,实例化过程如图所示:
下面就来详细看看其中的步骤。
Bean的实例化与BeanWrapper
容器在内部实现的时候,采用“策略模式”来决定采用何种方式初始化bean实例。通常,可以通过反射或者CGLIB动态字节码生成来初始化相应的bean实例或者动态生成其子类。
第一步
容器只要根据相应的bean定义的BeanDefintion取得实例化信息,结合CglibSubclassingInstantiationStrategy通过反射的方式,以及不同的bean定义类型,就可以返回实例化完成的对象实例。但是,返回的不是构造完成的对象实例,而是以BeanWrapper对构造完成的对象实例进行包括,返回相应的BeanWrapper实例。
第二步
第一步结束后返回BeanWrapper实例而不是原先的对象实例,就是为了第二步“设置对象属性”。使用BeanWrapper对bean实例操作很方便,可以免去直接使用Java反射API操作对象实力的反锁,看一段代码就知道Spring容器内部时如何设置对象属性的了:
1 | Object provider = Class.forName("package.name.FXNewsProvider").newInstance(); |
各色的Aware接口
当对象实例化完成后并且相关属性以及依赖设置完成之后,Spring容器会检查当前对象实例是否实现了一系列的以Aware命名结尾的接口定义,如果是,则将这些Aware接口定义中规定的依赖注入给当前对象实例。下面介绍几个重要的Aware接口:
- ApplicationContextAware: 获得ApplicationContext对象,可以用来获取所有Bean definition的名字。
- BeanFactoryAware:获得BeanFactory对象,可以用来检测Bean的作用域。
- BeanNameAware:获得Bean在配置文件中定义的名字。
- ResourceLoaderAware:获得ResourceLoader对象,可以获得classpath中某个文件。
- ServletContextAware:在一个MVC应用中可以获取ServletContext对象,可以读取context中的参数。
- ServletConfigAware: 在一个MVC应用中可以获取ServletConfig对象,可以读取config中的参数。
BeanPostProcessor
BeanPostProcessor会处理容器内所有符合条件的实例化后的对象实例,该接口声明了两个方法,分别在不同的时机执行:
1 | public interface BeanPostProcessor { |
postProcessBeforeInitialization()方法是上图中BeanPostProcessor前置处理这一步将会执行的方法,postProcessAfterInitialization()则是对应图中BeanPostProcessor后置处理那一步将会执行的方法。BeanPostProcessor的两个方法中都传入了原来的对象实例的引用,这位我们操作扩展容器的对象实例化过程中的行为提供了极大的便利。
InitializingBean和init-method
InitializingBean是容器内部广泛使用的一个对象生命周期表示接口,其定义如下:
1 | public interface InitializingBean { |
其作用在于,在对象实例化过程调用过“BeanPostProcessor”的前置处理之后,会接着检测当前对象是否实现了InitializingBean接口,如果是,则回到调用其afterPropertiesSet()方法进一步调整对象实例的状态。
通过init-method,系统中业务对象的自定义初始化操作可以以任何方式命名。而不再受制于InitializingBean的afterPropertiesSet()。在自己定义init方法后,只需要在XML文件中简单配置一下就好了:
1 | <beans> |
这样FXTradeDateCalculator类中的setupHolidays方法就是会在该阶段执行的方法了。
DisposableBean与destroy-method
当所有的一切,该设置的设置,该注入的注入,该调用的调用之后,容器将检查singleton类型的bean实例,看其是否实现了DisposableBean接口。或者其对应的bean定义是否通过<bean>的destroy-method属性制定了自定义的对象销毁方法,如果是,就会为该实例注册一个用于对象销毁的回调,以及在这些singleton类型的对象实例销毁之前,执行销毁逻辑。
不过这些自定义的对象销毁逻辑,在对象实例初始化完成并注册了相关的回调方法之后,并不会马上执行。回调方法注册后,在使用完成后,只有在执行回调方法之后,才会执行销毁操作。
对于BeanFactory注册回调方法的方式为:
1 | ((ConfigurableListableBeanFactory)container).destroySingletons(); |
对于ApplicationContext注册回调方法的方式为:
1 | ((AbstractApplicationContext)container).registerShutdownHook(); |
至此为止,bean就算走完了它在容器中“光荣”的一生。