文章作者:Tyan
博客:noahsnail.com | CSDN |
3.8 容器扩展点
通常情况下,应用开发者不需要继承ApplicationContext
的实现类。反而是Spring的IoC容器可以通过插入特定集成接口的实现来进行扩展。下面几节将描述这些集成接口。
3.8.1 通过BeanPostProcessor定制bean
BeanPostProcessor
接口定义了回调方法,你可以实现这个方法来提供你自己的(或覆盖容器默认的)实例化逻辑,依赖解析逻辑等等。如果你想在Spring容器完成实例化,配置和初始化bean之后实现一些定制的业务逻辑,你可以插入一个或多个BeanPostProcessor
实现。
你可以配置多个BeanPostProcessor
实例,通过设置order
属性你可以控制BeanPostProcessors
的执行顺序。只有BeanPostProcessor
实现了Ordered
接口时你才可以设置这个属性;如果你编写了你自己的BeanPostProcessor
,你也应该考虑实现Ordered
接口。更多细节请参考BeanPostProcessor
接口和Ordered
接口的Java文档。也可以查看下面的BeanPostProcessors
编程注册的笔记。
BeanPostProcessors
操作一个bean(或对象)实例;也就是说,Spring Ioc容器实例化一个bean实例,然后BeanPostProcessors
完成它们的工作。
BeanPostProcessors
的作用域是每个容器。只有你在使用容器分层的情况下,这才是相关的。如果你在一个容器中定义了一个BeanPostProcessor
,它将只后处理容器中的beans。换句话说,某个容器中定义的beans不能被另一个容器中定义的BeanPostProcessor
进行后处理,即使这两个容器是同一层上的一部分。
为了改变实际的bean定义(例如,定义bean的蓝图),你可以使用3.8.2小节中描述的
BeanFactoryPostProcessor
。
org.springframework.beans.factory.config.BeanPostProcessor
接口包含恰好两个回调方法。当这样一个类在容器中注册为后处理器时,对于容器中创建的每一个bean实例,在容器初始化方法(例如InitializingBean
的afterPropertiesSet()
方法和任何已声明的初始化方法)被调用之前和任何bean初始化回调函数之后,后处理器会从容器中得到一个回调函数。后处理器可以对bean实例进行任何操作,包括完全忽略回调方法。bean后处理器通常检查回调接口或将bean包裹到代理中。为了提供代理包裹逻辑,一些Spring AOP基础结构类被实现为bean后处理器。
ApplicationContext
会自动检测任何配置元数据中定义的实现了BeanPostProcessor
接口的bean。为了能在后面bean创建时调用这些bean,ApplicationContext
会将这些bean注册为后处理器。bean后处理器可以像其它bean一样在容器进行部署。
注意当在一个配置类上使用@Bean
声明一个BeanPostProcessor
时,工厂方法的返回值应该是实现类本身或是org.springframework.beans.factory.config.BeanPostProcessor
接口,这能清晰的表明bean的后处理器特性。此外,在完整的创建它之前,ApplicationContext
不能通过类型自动检测它。由于BeanPostProcessor
需要早一点实例化,为了在上下文中初始化其它的beans,早期的类型检测是非常关键的。
实现
BeanPostProcessor
接口的类是特别的并被容器不同对待。所有的BeanPostProcessors
和它们直接引用的beans在启动时进行实例化,它们是ApplicationContext
特定启动阶段的一部分。接下来,所有BeanPostProcessors
以有序形式进行注册,并适用于容器中所有更进一步的beans。由于AOP自动代理是作为BeanPostProcessor
本身实现的,既不是BeanPostProcessors
也不是它们直接引用的beans适合进行自动代理,因此没有融入它们的方面。
对于这样的bean,你应该看到一个信息日志消息:"Bean foo没资格被所有的
BeanPostProcessor
接口进行处理(例如,不适合自动代理)。"。
注意如果有beans使用自动装配或
@Resource
(可能回到自动装配)注入你的BeanPostProcessor
,当搜索类型匹配的依赖候选者时,Spring可能访问未预料到beans,因此使它们不适合自动代理或其他类型的进行后处理的bean。例如,如果你有一个带有@Resource
注解的依赖,field/setter
名称不能直接对应bean声明的名字,也没有使用name特性,Spring将通过类型匹配来访问其它的bean。
下面的例子展示了在ApplicationContext
中如何编写,注册和使用BeanPostProcessors
。
例子: Hello World, BeanPostProcessor类型
第一个例子阐述了基本用法。这个例子展示了一个定制BeanPostProcessor
实现,实现中调用了每一个bean的toString()
方法。当容器创建它时,会将结果字符串输出到系统控制台。
下面是定制BeanPostProcessor
实现的类定义:
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.BeansException;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// simply return the instantiated bean as-is
public Object postProcessBeforeInitialization(Object bean,
String beanName) throws BeansException {
return bean; // we could potentially return any object reference here...
}
public Object postProcessAfterInitialization(Object bean,
String beanName) throws BeansException {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}
注意InstantiationTracingBeanPostProcessor
是怎样简单定义的。它甚至没有一个名字,因为它是一个bean,它能像其它bean一样进行依赖注入。(前面的配置也定义了一个bean,它被Groovy脚本支持。Spring动态语言支持在31章『动态语言支持』中进行了详细描述。)
下面的简单Java应用执行了前面的代码和配置:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
Messenger messenger = (Messenger) ctx.getBean("messenger");
System.out.println(messenger);
}
}
前面的应用输出结果如下:
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
例: RequiredAnnotationBeanPostProcessor
使用回调函数接口或注解结合定制BeanPostProcessor
实现是扩展Spring IoC容器的常见方法。一个例子是Spring的RequiredAnnotationBeanPostProcessor
——一个BeanPostProcessor
实现附带在Spring发行中,它保证了标记有(任意)注解的beans上的JavaBean
属性能真正(配置成)通过值进行依赖注入。
3.8.2 通过BeanFactoryPostProcessor定制配置元数据
接下来我们要看到的扩展点是org.springframework.beans.factory.config.BeanFactoryPostProcessor
。这个接口的语义与那些BeanPostProcessor
类似,但有一个主要的不同:BeanFactoryPostProcessor
可以操作配置元数据;也就是说,Spring IoC容器允许在容器实例化除了BeanFactoryPostProcessor
之外的任何beans之前,BeanFactoryPostProcessor
读取配置元数据并可能修改它们。
你可以配置多个BeanFactoryPostProcessors
,你可以通过设置order
属性来控制这些BeanFactoryPostProcessors
的执行顺序。但是,只有BeanFactoryPostProcessor
实现了Ordered
接口时你才可以设置这个属性。如果你编写了你自己的BeanFactoryPostProcessor
,你也应该考虑实现Ordered
接口。关于BeanFactoryPostProcessor
和Ordered
的更多细节请看文档。
如果你想改变真正的bean实例(例如,从配置元数据中创建的对象),你应该需要使用
BeanPostProcessor
(3.8.1小节中描述的)。尽管在BeanFactoryPostProcessor
中处理bean实例在技术上是可能的(例如使用BeanFactory.getBean()
),但这样做会引起过早的bean实例化,违背标准的容器生命周期。这可能会产生负面影响例如绕过bean后处理。
BeanFactoryPostProcessors
的作用域也是在每个容器中。这仅对于容器分层而言。如果在一个容器中你定义了一个BeanFactoryPostProcessor
,它将适用于那个容器中的bean定义。一个容器中的bean定义不能被另一个容器中的BeanFactoryPostProcessors
进行后处理,即使两个容器是在同一个分层中。
为了修改定义在容器中的配置元数据,当一个bean工厂后处理器在ApplicationContext
中声明时,它会自动执行。Spring包含许多预先定义的bean工厂后处理器,例如PropertyOverrideConfigurer
和PropertyPlaceholderConfigurer
。定制的BeanFactoryPostProcessor
也可以使用,例如,为了注册定制的属性编辑器。
ApplicationContext
会自动检测任何部署在它之内的实现了BeanFactoryPostProcessor
接口的bean。在合适的时间,它会使用这些beans作为bean工厂后处理器。你可以像任何你使用的bean那样部署这些后处理器beans。
关于
BeanPostProcessors
, 通常情况下你不想配置BeanFactoryPostProcessors
为延迟初始化。 如果没有别的bean引用Bean(Factory)PostProcessor
,后处理器将不会实例化。因此,对它进行延迟初始化会被忽略,即使你将元素中的
default-lazy-init
特性设置为true
,Bean(Factory)PostProcessor
也会急切的初始化。
例: 类名替换PropertyPlaceholderConfigurer
你可以使用PropertyPlaceholderConfigurer
读取单独文件中的bean定义来使属性具体化,这个单独文件使用标准的Java Properties
格式。这样做可以在部署应用时定制特定环境属性例如数据库URLs和密码,没有复杂性或修改主XML定义文件及容器相关文件的风险。
考虑一下下面的基于XML定义的配置元数据片段,其中定义了一个带有占位符的DataSource
。这个例子展示了从外部Properties
文件进行属性配置。在运行时,PropertyPlaceholderConfigurer
会应用到元数据中,将会替换DataSource
中的一些属性。通过${property-name}
形式的占位符指定要替换的值,这遵循了Ant/log4j/JSP EL风格。
真正的属性值来自于另一个以标准Java Properties
形式编写的文件:
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root
因此,在运行是字符串${jdbc.username}
被替换为sa
,其它的匹配属性文件中的key的占位符的值以同样方式替换。PropertyPlaceholderConfigurer
会检查bean中大多数属性和特性的占位符。此外,占位符的前缀和后缀都可以定制。
Spring 2.5中引入了上下文命名空间,可以通过专用配置元素配置属性占位符。在location
特性可以提供一个或多个位置,多个位置用逗号分开。
PropertyPlaceholderConfigurer
不仅仅查找指定Properties
文件中的属性。默认情况下,如果不能在指定属性文件中找到属性,它也检查Java System
属性。你可以通过下面三个支持的整数值中的一个设置配置器的systemPropertiesMode
属性,从而定制查找行为。
never (0): 从不检查
system
属性fallback (1): 如果不能在指定文件中解析属性,检查
system
属性,这是默认值。override (2): 在查找指定文件之前,首先检查
system
属性,这可以使系统属性覆盖任何其它属性源。
更多信息请看PropertyPlaceholderConfigurer
文档。
你可以
PropertyPlaceholderConfigurer
替换类名,有时候非常有用,特别是运行时你必须选择一个特别的实现类的情况下。例如:
classpath:com/foo/strategy.properties
custom.strategy.class=com.foo.DefaultStrategy
如果这个类不能在运行时解析成一个有效类,对于一个非懒惰初始化的bean,当它要创建时,在
ApplicationContext
的preInstantiateSingletons()
期间,bean会解析失败。
例: PropertyOverrideConfigurer
PropertyOverrideConfigurer
,另一个bean工厂后处理器,类似于PropertyPlaceholderConfigurer
,但不像后者,最初的定义可以有默认值或bean属性一点也没有值。如果一个覆写的Properties
文件对于某个bean属性没有任何输入,会使用默认的上下文定义。
注意bean定义没有意识到被覆写了,因此从XML定义文件中它不能立刻很明显的看出在使用覆写的配置器。为了防止多个PropertyOverrideConfigurer
实例对于同一个bean属性定义不同的值,根据覆写机制,使用最后一个定义的值。
属性文件配置形式如下:
beanName.property=value
例如:
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb
例子文件可以被包含名为dataSource
bean的容器定义使用,它有一个driver
和url
属性。
混合属性命名也支持,除了最后被覆写的属性,只要路径的每部分都已经是非空(假设构造函数进行初始化)。在这个例子中:
foo.fred.bob.sammy=123
foo
bean中的fred
属性的bob
属性的sammy
属性设为标量值123
。
指定的覆写值总是字面值;它们不能转成bean引用。当XML bean定义中的初始值指定了一个bean引用时,这个规范同样有效。
Spring 2.5引入了上下文命名空间,可以用专用配置元素配置属性覆写:
3.8.3 使用FactoryBean定制实例化逻辑
为对象实现org.springframework.beans.factory.FactoryBean
接口的是工厂本身。
FactoryBean
接口是Spring IoC的实例化逻辑可插入性的一个点。如果你有复杂的初始化代码,相比于大量的冗余的XML代码用Java语言来表达会更好,那么你可以创建你自己的FactoryBean
,在类里面编写复杂的初始化逻辑,并将你定制的FactoryBean
插入到容器中。
FactoryBean
接口提供了三个方法:
Object getObject()
: 返回一个工厂创建的对象实例。这个实例可能被共享, 依赖于工厂是否返回一个单例或原型。boolean isSingleton()
: 如果FactoryBean
返回单例,返回true,否则返回false。Class getObjectType()
: 返回getObject()
方法返回的类型,如果类型不能提前知道则返回null。
FactoryBean
的概念和接口在Spring框架中的许多地方都使用了;Spring本身中有不止50个FactoryBean
接口的实现。
当你需要向容器请求一个真正的FactoryBean
实例本身来代替它产生的bean时,调用ApplicationContext
的getBean()
方法时,bean的id前面要加上一个$
符。因此给定一个id为myBean
的FactoryBean
,在容器中调用getBean("myBean")
,返回FactoryBean
的产品,但调用getBean("&myBean")
会返回FactoryBean
实例本身。