Spring 5.0.0框架介绍_中文版_3.8

文章作者: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实例,在容器初始化方法(例如InitializingBeanafterPropertiesSet()方法和任何已声明的初始化方法)被调用之前和任何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实现的类定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang.xsd">
<lang:groovy id="messenger"
script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
<lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
</lang:groovy>
<!--
when the above bean (messenger) is instantiated, this custom
BeanPostProcessor implementation will output the fact to the system console
-->
<bean class="scripting.InstantiationTracingBeanPostProcessor"/>
</beans>

注意InstantiationTracingBeanPostProcessor是怎样简单定义的。它甚至没有一个名字,因为它是一个bean,它能像其它bean一样进行依赖注入。(前面的配置也定义了一个bean,它被Groovy脚本支持。Spring动态语言支持在31章『动态语言支持』中进行了详细描述。)

下面的简单Java应用执行了前面的代码和配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}

前面的应用输出结果如下:

1
2
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接口。关于BeanFactoryPostProcessorOrdered的更多细节请看文档。

如果你想改变真正的bean实例(例如,从配置元数据中创建的对象),你应该需要使用BeanPostProcessor(3.8.1小节中描述的)。尽管在BeanFactoryPostProcessor中处理bean实例在技术上是可能的(例如使用BeanFactory.getBean()),但这样做会引起过早的bean实例化,违背标准的容器生命周期。这可能会产生负面影响例如绕过bean后处理。

BeanFactoryPostProcessors的作用域也是在每个容器中。这仅对于容器分层而言。如果在一个容器中你定义了一个BeanFactoryPostProcessor,它将适用于那个容器中的bean定义。一个容器中的bean定义不能被另一个容器中的BeanFactoryPostProcessors进行后处理,即使两个容器是在同一个分层中。

为了修改定义在容器中的配置元数据,当一个bean工厂后处理器在ApplicationContext中声明时,它会自动执行。Spring包含许多预先定义的bean工厂后处理器,例如PropertyOverrideConfigurerPropertyPlaceholderConfigurer。定制的BeanFactoryPostProcessor也可以使用,例如,为了注册定制的属性编辑器。

ApplicationContext会自动检测任何部署在它之内的实现了BeanFactoryPostProcessor接口的bean。在合适的时间,它会使用这些beans作为bean工厂后处理器。你可以像任何你使用的bean那样部署这些后处理器beans。

关于BeanPostProcessors, 通常情况下你不想配置BeanFactoryPostProcessors为延迟初始化。 如果没有别的bean引用Bean(Factory)PostProcessor,后处理器将不会实例化。因此,对它进行延迟初始化会被忽略,即使你将<beans/>元素中的default-lazy-init特性设置为trueBean(Factory)PostProcessor也会急切的初始化。

例: 类名替换PropertyPlaceholderConfigurer

你可以使用PropertyPlaceholderConfigurer读取单独文件中的bean定义来使属性具体化,这个单独文件使用标准的Java Properties格式。这样做可以在部署应用时定制特定环境属性例如数据库URLs和密码,没有复杂性或修改主XML定义文件及容器相关文件的风险。

考虑一下下面的基于XML定义的配置元数据片段,其中定义了一个带有占位符的DataSource。这个例子展示了从外部Properties文件进行属性配置。在运行时,PropertyPlaceholderConfigurer会应用到元数据中,将会替换DataSource中的一些属性。通过${property-name}形式的占位符指定要替换的值,这遵循了Ant/log4j/JSP EL风格。

1
2
3
4
5
6
7
8
9
10
11
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:com/foo/jdbc.properties"/>
</bean>
<bean id="dataSource" destroy-method="close"
class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

真正的属性值来自于另一个以标准Java Properties形式编写的文件:

1
2
3
4
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特性可以提供一个或多个位置,多个位置用逗号分开。

1
<context:property-placeholder location="classpath:com/foo/jdbc.properties"/>

PropertyPlaceholderConfigurer不仅仅查找指定Properties文件中的属性。默认情况下,如果不能在指定属性文件中找到属性,它也检查Java System属性。你可以通过下面三个支持的整数值中的一个设置配置器的systemPropertiesMode属性,从而定制查找行为。

  • never (0): 从不检查system属性

  • fallback (1): 如果不能在指定文件中解析属性,检查system属性,这是默认值。

  • override (2): 在查找指定文件之前,首先检查system属性,这可以使系统属性覆盖任何其它属性源。

更多信息请看PropertyPlaceholderConfigurer文档。

你可以PropertyPlaceholderConfigurer替换类名,有时候非常有用,特别是运行时你必须选择一个特别的实现类的情况下。例如:

1
2
3
4
5
6
7
8
9
10
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath:com/foo/strategy.properties</value>
</property>
<property name="properties">
<value>custom.strategy.class=com.foo.DefaultStrategy</value>
</property>
</bean>
<bean id="serviceStrategy" class="${custom.strategy.class}"/>

如果这个类不能在运行时解析成一个有效类,对于一个非懒惰初始化的bean,当它要创建时,在ApplicationContextpreInstantiateSingletons()期间,bean会解析失败。

例: PropertyOverrideConfigurer

PropertyOverrideConfigurer,另一个bean工厂后处理器,类似于PropertyPlaceholderConfigurer,但不像后者,最初的定义可以有默认值或bean属性一点也没有值。如果一个覆写的Properties文件对于某个bean属性没有任何输入,会使用默认的上下文定义。

注意bean定义没有意识到被覆写了,因此从XML定义文件中它不能立刻很明显的看出在使用覆写的配置器。为了防止多个PropertyOverrideConfigurer实例对于同一个bean属性定义不同的值,根据覆写机制,使用最后一个定义的值。

属性文件配置形式如下:

1
beanName.property=value

例如:

1
2
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

例子文件可以被包含名为dataSource bean的容器定义使用,它有一个driverurl属性。

混合属性命名也支持,除了最后被覆写的属性,只要路径的每部分都已经是非空(假设构造函数进行初始化)。在这个例子中:

1
foo.fred.bob.sammy=123

foo bean中的fred属性的bob属性的sammy属性设为标量值123

指定的覆写值总是字面值;它们不能转成bean引用。当XML bean定义中的初始值指定了一个bean引用时,这个规范同样有效。

Spring 2.5引入了上下文命名空间,可以用专用配置元素配置属性覆写:

1
<context:property-override location="classpath:override.properties"/>

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时,调用ApplicationContextgetBean()方法时,bean的id前面要加上一个$符。因此给定一个id为myBeanFactoryBean,在容器中调用getBean("myBean"),返回FactoryBean的产品,但调用getBean("&myBean")会返回FactoryBean实例本身。

如果有收获,可以请我喝杯咖啡!