Appearance
Spring 核心:IoC 容器 - Part2
Tip:基于 Spring Core 5.3.30 版本。
1. Bean 作用域
当你创建一个 Bean 定义时,你实际上是在创建一个用于创建该 Bean 定义所定义的类的实例的配方。Bean 定义是一个配方的概念很重要,因为它意味着,与类一样,你可以从一个单一的配方创建多个对象实例。
你不仅可以控制从特定 Bean 定义创建的对象中要插入的各种依赖关系和配置值,还可以控制从特定 Bean 定义创建的对象的作用域。这种方法既强大又灵活,因为你可以通过配置选择创建对象的作用域,而不是在 Java 类级别上硬编码对象的作用域。Bean 可以被定义为部署在若干作用域中的一个。Spring 框架支持六种作用域,其中四种仅在你使用了 Web 感知的 ApplicationContext 时才可用。你也可以创建自定义作用域。
下表描述了支持的作用域:
| 作用域 | 说明 |
|---|---|
singleton | (默认)将单个 Bean 定义作用域限定为 Spring IoC 容器中的单个对象实例 |
prototype | 将单个 Bean 定义作用域限定为任意数量的对象实例 |
request | 将单个 Bean 定义作用域限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有其自己的 Bean 实例,这个实例是基于单个 Bean 定义创建的。只在一个 Web 感知的 Spring ApplicationContext 上下文中有效 |
session | 将单个 Bean 定义作用域限定为 HTTP 会话的生命周期。只在一个 Web 感知的 Spring ApplicationContext 上下文中有效 |
application | 将单个 Bean 定义作用域限定为 ServletContext 的生命周期。只在一个 Web 感知的 Spring ApplicationContext 上下文中有效 |
websocket | 将单个 Bean 定义作用域限定为 WebSocket 的生命周期。只在一个 Web 感知的 Spring ApplicationContext 上下文中有效 |
Warning:截至 Spring 3.0,线程作用域是可用的,但默认情况下未注册。有关更多信息,请参阅
SimpleThreadScope的文档。有关如何注册此或任何其他自定义作用域的说明,请参阅使用自定义作用域小节。
1.1. 单例作用域
单例 Bean 只管理一个共享实例,对于与该 Bean 定义匹配的 ID 或多个 ID 的所有 Bean 请求,Spring 容器都返回该特定的 Bean 实例。
换句话说,当你定义一个 Bean 定义,并将其作用域设置为单例时,Spring IoC 容器会创建该 Bean 定义所定义的对象的一个实例。这个单一实例被存储在这种单例 Bean 的缓存中,所有后续对该命名 Bean 的请求和引用都将返回缓存的对象。下面的图片展示了单例作用域的工作原理:

Spring 中的单例 Bean 的概念与《设计模式》(Gang of Four,GoF)一书中定义的单例模式有所不同。《设计模式》中的单例模式将对象的作用域硬编码为每个类加载器创建一个特定类的一个且仅有一个实例。Spring 中的单例作用域最好描述为每个容器和每个 Bean。这意味着,如果你在单个 Spring 容器中为特定类定义了一个 Bean,Spring 容器将创建该 Bean 定义所定义的类的一个且仅有一个实例。单例作用域是 Spring 中的默认作用域。要在 XML 中将 Bean 定义为单例,你可以按照以下示例定义 Bean:
XML
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>1
2
3
4
2
3
4
1.2. 原型作用域
非单例原型(prototype)作用域的 Bean 部署导致每次请求该特定 Bean 时都会创建一个新的 Bean 实例。也就是说,当 Bean 被注入到另一个 Bean 中或者通过容器的 getBean() 方法调用请求它时,都会创建一个新的实例。一般来说,你应该将原型作用域用于所有有状态的 Bean,将单例作用域用于无状态的 Bean。
以下图示说明了 Spring 的原型作用域:

数据访问对象(Data Access Object,DAO)通常不会被配置为原型,因为典型的 DAO 不会保存任何会话状态。我们更容易地重用了单例图中的核心部分。
以下示例在 XML 中将一个 Bean 定义为原型:
XML
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>与其他作用域不同,Spring 不管理原型 Bean 的完整生命周期。容器实例化、配置和组装原型对象,并将其交给客户端,而不再对该原型实例进行任何记录。因此,无论作用域如何,初始化生命周期回调方法都会被调用,但对于原型而言,配置的销毁生命周期回调不会被调用。客户端代码必须清理原型作用域的对象,并释放原型 Bean 持有的昂贵资源。为了让 Spring 容器释放原型作用域 Bean 持有的资源,可以尝试使用自定义 Bean 后处理器,该后处理器持有需要清理的 Bean 的引用。
在某些方面,Spring 容器在处理原型作用域 Bean 时的角色类似于 Java 的 new 操作符。在此之后,所有的生命周期管理都必须由客户端处理。有关 Spring 容器中 Bean 生命周期的详细信息,请参阅生命周期回调小节。
1.3. 具有原型 Bean 依赖项的单例 Bean
当你在单例作用域的 Bean 中使用依赖于原型作用域的 Bean 时,需要注意依赖关系是在实例化时解析的。因此,如果你将一个原型作用域的 Bean 依赖注入到一个单例作用域的 Bean 中,会实例化一个新的原型 Bean,然后将其注入到单例 Bean 中。只有一个原型实例被提供给该单例作用域 Bean。
然而,如果你希望单例作用域的 Bean 在运行时重复获取原型作用域的 Bean 的新实例。你不能将原型作用域的 Bean 注入到单例 Bean 中,因为该注入只会发生一次,即当 Spring 容器实例化单例 Bean 并解析并注入其依赖项时。如果你需要在运行时多次获得原型 Bean 的新实例,请参阅方法注入。
1.4. 请求、会话、应用和 WebSocket 作用域
只有当你使用支持 Web 的 Spring ApplicationContext 实现(比如 XmlWebApplicationContext)时,才能使用 request、session、application 和 websocket 作用域。如果你在常规的 Spring IoC 容器(比如 ClassPathXmlApplicationContext)中使用这些作用域,会抛出一个 IllegalStateException,指示未知的 Bean 作用域。
1.4.1. 初始 Web 配置
为了支持在 request、session、application 和 websocket 级别(Web 作用域的 Bean)上进行 Bean 的作用域管理,需要在定义 Bean 之前进行一些轻微的初始配置(对于标准作用域 singleton 和 prototype,不需要进行这种初始设置)。
如何完成这种初始设置取决于你所使用的 Servlet 环境。
如果你在 Spring Web MVC 中访问作用域 Bean,实际上就是在由 Spring DispatcherServlet 处理的请求中,那么不需要进行任何特殊的设置。DispatcherServlet 已经暴露了所有相关的状态。
如果你使用 Servlet 2.5 的 Web 容器,并且请求是在 Spring 的 DispatcherServlet 之外处理的(例如在使用 JSF 或 Struts 时),你需要注册 org.springframework.web.context.request.RequestContextListener ServletRequestListener。对于 Servlet 3.0+,可以通过使用 WebApplicationInitializer 接口在编程方式上完成此操作。或者,对于较旧的容器,也可以将以下声明添加到你的 Web 应用程序的 web.xml 文件中:
XML
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
或者,如果你的监听器设置存在问题,可以考虑使用 Spring 的 RequestContextFilter。过滤器映射取决于周围的 Web 应用程序配置,所以你需要根据情况进行更改。以下示例显示了 Web 应用程序的过滤器部分:
XML
<web-app>
...
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
DispatcherServlet、RequestContextListener 和 RequestContextFilter 都做同样的事情,即将 HTTP 请求对象绑定到正在处理该请求的 Thread 上。这样做可以使得请求和会话作用域的 Bean 在调用链中的后续位置可用。
1.4.2. 请求作用域
考虑以下用于 Bean 定义的 XML 配置:
XML
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>Spring 容器会根据每个 HTTP 请求使用 loginAction 的 Bean 定义来创建一个新的 LoginAction Bean 实例。换句话说,loginAction Bean 的作用域是 HTTP 请求级别的。你可以随意更改被创建的实例的内部状态,因为来自相同 loginAction Bean 定义的其他实例不会看到这些状态的变化。它们是针对单个请求的。当请求处理完成时,作用域为请求的 Bean 就会被丢弃。
在使用基于注解的组件或 Java 配置时,可以使用 @RequestScope 注解将组件分配到 request 作用域。以下示例展示了如何这样做:
Java
@RequestScope
@Component
public class LoginAction {
// ...
}1
2
3
4
5
2
3
4
5
1.4.3. 会话作用域
考虑以下用于 Bean 定义的 XML 配置:
XML
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>Spring 容器会使用 userPreferences 的 Bean 定义在单个 HTTP Session 的生命周期内创建一个新的 UserPreferences Bean 实例。换句话说,userPreferences Bean 的作用域实际上是 HTTP Session 级别的。与请求作用域的 Bean 类似,你可以随意更改被创建的实例的内部状态,因为其他使用相同 userPreferences Bean 定义创建的 HTTP Session 实例不会看到这些状态的变化,因为它们是特定于单个 HTTP Session 的。当 HTTP Session 最终被丢弃时,作用域为该特定 HTTP Session 的 Bean 也会被丢弃。
在使用基于注解的组件或 Java 配置时,可以使用 @SessionScope 注解将组件分配到 session 作用域。
Java
@SessionScope
@Component
public class UserPreferences {
// ...
}1
2
3
4
5
2
3
4
5
1.4.4. 应用程序作用域
考虑以下用于 Bean 定义的 XML 配置:
XML
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>Spring 容器会使用 appPreferences 的 Bean 定义在整个 Web 应用程序中创建一个新的 AppPreferences Bean 实例。换句话说,appPreferences Bean 的作用域是 ServletContext 级别的,并且存储为常规的 ServletContext 属性。这在某种程度上类似于 Spring 的单例 Bean,但有两个重要的不同之处:它是针对 ServletContext 的单例,而不是针对 Spring 的 ApplicationContext(在任何给定的 Web 应用程序中可能有多个),而且它实际上是暴露的,因此可以作为 ServletContext 属性看到。
在使用基于注解的组件或 Java 配置时,可以使用 @ApplicationScope 注解将组件分配到 application 作用域。以下示例展示了如何这样做:
Java
@ApplicationScope
@Component
public class AppPreferences {
// ...
}1
2
3
4
5
2
3
4
5
1.4.5. WebSocket 作用域
WebSocket 作用域与 WebSocket 会话的生命周期相关,并适用于 STOMP over WebSocket 应用程序,请参阅 WebSocket 作用域以了解更多详情。
1.4.6. 作为依赖项的作用域 Bean
Spring IoC 容器不仅管理对象(Bean)的实例化,还管理协作者(或依赖项)的装配。如果你想将一个 HTTP 请求作用域的 Bean 注入到另一个生命周期更长的 Bean 中,你可以选择注入一个 AOP 代理来代替作用域 Bean。换句话说,你需要注入一个代理对象,该对象暴露与作用域对象相同的公共接口,但它也可以从相关作用域(比如 HTTP 请求)中获取真正的目标对象,并将方法调用委托给真正的对象。
Warning
你还可以在作用域为
singleton的 Bean 之间使用<aop:scoped-proxy/>,这样引用就会通过一个中间代理,该代理是可序列化的,因此能够在反序列化时重新获取目标单例 Bean。当针对作用域为
prototype的 Bean 声明<aop:scoped-proxy/>时,共享代理上的每个方法调用都会导致创建一个新的目标实例,然后将调用转发到该实例。此外,作用域代理并不是以生命周期安全方式访问较短作用域中的 Bean 的唯一方法。你还可以将注入点(即构造函数或 Setter 参数或自动装配字段)声明为
ObjectFactory<MyTargetBean>,允许在每次需要时通过getObject()调用来按需检索当前实例,而无需保留实例或单独存储它。作为扩展变体,你可以声明
ObjectProvider<MyTargetBean>,它提供了几种额外的访问变体,包括getIfAvailable和getIfUnique。JSR-330 的变体称为
Provider,它与Provider<MyTargetBean>声明一起使用,并且每次检索尝试时都需要相应的get()调用。有关 JSR-330 的更多详细信息,请参阅这里。
下面示例中的配置只有一行,但理解背后的 “为什么” 和 “如何” 同样重要:
XML
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/>
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.something.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
为了创建这样的代理,你需要在作用域 Bean 的定义中插入一个子元素 <aop:scoped-proxy/>(参见选择要创建的代理类型和基于 XML Schema 的配置)。为什么需要在作用域为 request、session 和自定义作用域级别的 Bean 定义中使用 <aop:scoped-proxy/> 元素?考虑以下单例 Bean 定义,并将其与你需要为上述作用域定义的内容进行对比(请注意,以下 userPreferences Bean 定义目前是不完整的):
XML
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>1
2
3
4
5
2
3
4
5
在上面的示例中,单例 Bean(userManager)被注入了一个对 HTTP Session 作用域 Bean(userPreferences)的引用。这里的关键点是,userManager Bean 是一个单例:它在容器中只实例化一次,而它的依赖项(在本例中只有一个,即 userPreferences Bean)也只被注入一次。这意味着 userManager Bean 只对确切相同的 userPreferences 对象(也就是最初注入的对象)进行操作。
然而,当将一个生命周期较短的作用域 Bean 注入到一个生命周期较长的作用域 Bean 中时(例如,将一个 HTTP Session 作用域的协作 Bean 作为依赖项注入到单例 Bean 中),这并不是你想要的行为。相反,你需要一个 userManager 对象,并且在一个 HTTP Session 的生命周期内,你需要一个特定于该 HTTP Session 的 userPreferences 对象。因此,容器会创建一个对象,该对象暴露与 UserPreferences 类完全相同的公共接口,它可以从作用域机制(HTTP 请求、Session 等)中获取真正的 UserPreferences 对象。容器将这个代理对象注入到 userManager Bean 中,而 userManager Bean 则不知道这个 UserPreferences 引用是一个代理。在这个示例中,当一个 UserManager 实例在依赖注入的 UserPreferences 对象上调用方法时,实际上是在调用代理上的方法。代理然后从(在这种情况下)HTTP Session 中获取真正的 UserPreferences 对象,并将方法调用委托给检索到的真正的 UserPreferences 对象。
因此,当将 request 作用域和 session 作用域的 Bean 注入到协作对象中时,你需要以下(正确和完整的)配置,就像以下示例所示:
XML
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>1
2
3
4
5
6
7
2
3
4
5
6
7
Warning:当你在
FactoryBean实现的<bean>声明中放置<aop:scoped-proxy/>时,被作用域管理的是工厂 Bean 本身,而不是从getObject()返回的对象。
1.4.6.1. 选择要创建的代理类型
默认情况下,当 Spring 容器为标记有 <aop:scoped-proxy/> 元素的 Bean 创建代理时,会创建一个基于 CGLIB 的类代理。
Warning:CGLIB 代理只拦截公共方法的调用!不要在这样的代理上调用非公共方法。这些方法不会被委托给实际的作用域目标对象。
另外,你可以通过在 <aop:scoped-proxy/> 元素的 proxy-target-class 属性中指定 false 的值,来配置 Spring 容器为这些作用域 Bean 创建标准的基于 JDK 接口的代理。使用基于 JDK 接口的代理意味着你不需要在应用程序类路径中使用额外的库来影响这种代理。但是,这也意味着作用域 Bean 的类必须实现至少一个接口,并且所有注入了作用域 Bean 的协作对象必须通过其接口引用该 Bean。以下示例展示了基于接口的代理:
XML
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.stuff.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
有关选择基于类或基于接口的代理的更详细信息,请参阅代理机制。
1.5. 自定义作用域
Bean 的作用域机制是可扩展的。你可以定义自己的作用域,甚至重新定义现有的作用域(尽管后者被认为是不良实践)。不过你不能覆盖内置的 singleton 和 prototype 作用域。
1.5.1. 创建自定义作用域
要将你的自定义作用域集成到 Spring 容器中,你需要实现 org.springframework.beans.factory.config.Scope 接口。要了解如何实现自定义作用域,请参阅 Spring Framework 本身提供的 Scope 实现以及 Scope JavaDoc,该文档详细说明了你需要实现的方法。
Scope 接口有四个方法,用于从作用域中获取对象、将对象从作用域中移除,以及让它们被销毁。
例如,session 作用域的实现支持返回 session 作用域的 Bean(如果不存在,则方法返回该 Bean 的新实例,并将其绑定到 session 中以供将来引用)。以下方法从底层作用域中返回对象:
Java
Object get(String name, ObjectFactory<?> objectFactory)例如,session 作用域的实现支持将 session 作用域的 Bean 从底层 session 中移除。移除后应该返回该对象,但如果找不到具有指定名称的对象,则可以返回 null。以下方法从底层作用域中移除对象:
Java
Object remove(String name)以下方法注册一个回调,当作用域被销毁或作用域中的指定对象被销毁时,作用域应该调用该回调:
Java
void registerDestructionCallback(String name, Runnable destructionCallback)有关销毁回调的更多信息,请参阅 JavaDoc 或 Spring 的作用域实现。
以下方法获取底层作用域的会话标识符:
Java
String getConversationId()这个标识符对于每个作用域都是不同的。对于一个 Session 作用域的实现来说,这个标识符可以是 Session 标识符。
1.5.2. 使用自定义作用域
当你编写并测试一个或多个自定义的 Scope 实现后,你需要让 Spring 容器知道你的新作用域。以下方法是向 Spring 容器注册新的 Scope 的核心方法:
Java
void registerScope(String scopeName, Scope scope);这个方法在 ConfigurableBeanFactory 接口中声明,在大多数附带的 Spring ApplicationContext 实现中,通过 BeanFactory 属性可以获得该接口。
registerScope(...) 方法的第一个参数是与作用域关联的唯一名称。在 Spring 容器本身中,这些名称的示例包括 singleton 和 prototype。registerScope(...) 方法的第二个参数是你希望注册和使用的自定义 Scope 实现的实际实例。
假设你编写了自定义的 Scope 实现,然后像下面的示例一样注册它:
Note:下面的示例使用了 Spring 中包含的
SimpleThreadScope,但它默认情况下不会被注册。对于你自己的自定义Scope实现,操作步骤是相同的。
Java
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);1
2
2
然后,你可以创建符合你自定义 Scope 规则的 Bean 定义,如下所示:
XML
<bean id="..." class="..." scope="thread">使用自定义 Scope 实现,你不仅可以通过编程方式注册作用域,还可以使用 CustomScopeConfigurer 类来声明性地进行作用域注册,如下所示的示例所示:
XML
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="thing2" class="x.y.Thing2" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="thing1" class="x.y.Thing1">
<property name="thing2" ref="thing2"/>
</bean>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2. 自定义 Bean 的性质
Spring 框架提供了一些接口,你可以使用这些接口来自定义 Bean 的性质。本节将它们分为以下几类:
2.1. 生命周期回调
要与容器管理的 Bean 生命周期进行交互,你可以实现 Spring 的 InitializingBean 和 DisposableBean 接口。容器会分别调用前者的 afterPropertiesSet() 方法和后者的 destroy() 方法,让 Bean 在初始化和销毁时执行特定的操作。
Tip
在现代的 Spring 应用程序中,通常认为使用 JSR-250
@PostConstruct和@PreDestroy注解来接收生命周期回调是最佳实践。使用这些注解意味着你的 Bean 不会与 Spring 特定的接口耦合。有关详细信息,请参阅使用@PostConstruct和@PreDestroy。如果你不想使用 JSR-250 注解,但仍然想消除耦合,可以考虑使用
init-method和destroy-method的 Bean 定义元数据。
Spring Framework 在内部使用 BeanPostProcessor 实现来处理它可以找到的任何回调接口,并调用适当的方法。如果你需要自定义功能或其他 Spring 默认不提供的生命周期行为,你可以自己实现一个 BeanPostProcessor。有关更多信息,请参阅容器扩展点。
除了初始化和销毁回调之外,Spring 管理的对象还可以实现 Lifecycle 接口,以便这些对象可以参与由容器自身生命周期驱动的启动和关闭过程。
这些生命周期回调接口在本节中进行了描述。
2.1.1. 初始化回调
org.springframework.beans.factory.InitializingBean 接口允许 Bean 在容器设置了 Bean 的所有必要属性之后执行初始化工作。InitializingBean 接口指定了一个方法:
Java
void afterPropertiesSet() throws Exception;我们建议不要使用 InitializingBean 接口,因为它会不必要地将代码耦合到 Spring。相反,我们建议使用 @PostConstruct 注解或指定一个 POJO 初始化方法。在基于 XML 的配置元数据中,你可以使用 init-method 属性来指定具有 void 无参数签名的方法的名称。在 Java 配置中,你可以使用 @Bean 的 initMethod 属性。请参阅接收生命周期回调。考虑以下示例:
XML
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>Java
public class ExampleBean {
public void init() {
// do some initialization work
}
}1
2
3
4
5
2
3
4
5
前面的示例几乎和下面的示例有着完全相同的效果:
XML
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>Java
public class AnotherExampleBean implements InitializingBean {
@Override
public void afterPropertiesSet() {
// do some initialization work
}
}1
2
3
4
5
6
2
3
4
5
6
然而,前面两个示例中的第一个示例并不会将代码与 Spring 进行耦合。
Warning
请注意,
@PostConstruct和一般的初始化方法都是在容器的单例创建锁内执行的。只有在从@PostConstruct方法返回后,Bean 实例才被视为完全初始化并准备好发布给其他对象。这些单独的初始化方法仅用于验证配置状态,并可能基于给定的配置准备一些数据结构,但不进行任何进一步的与外部 Bean 访问的活动。否则,就会存在初始化死锁的风险。对于需要触发昂贵的后初始化活动的场景,例如异步数据库准备步骤,你的 Bean 应该实现
SmartInitializingSingleton.afterSingletonsInstantiated()或依赖上下文刷新事件:实现ApplicationListener<ContextRefreshedEvent>或声明其注解等价物@EventListener(ContextRefreshedEvent.class)。这些变体都是在所有常规单例初始化之后发生的,因此不会受到任何单例创建锁的影响。或者,你可以实现
(Smart)Lifecycle接口,并与容器的整体生命周期管理集成,包括自动启动机制、预销毁停止步骤以及可能的停止/重启回调(见下文)。
2.1.2. 销毁回调
实现 org.springframework.beans.factory.DisposableBean 接口可以让一个 Bean 在包含它的容器被销毁时获得一个回调。DisposableBean 接口指定了一个方法:
Java
void destroy() throws Exception;我们建议不要使用 DisposableBean 回调接口,因为它会不必要地将代码与 Spring 耦合。作为替代,我们建议使用 @PreDestroy 注解或指定一个通用方法,该方法由 Bean 定义支持。在基于 XML 的配置元数据中,你可以使用 <bean/> 上的 destroy-method 属性。在 Java 配置中,你可以使用 @Bean 的 destroyMethod 属性。请参阅接收生命周期回调。考虑以下定义:
XML
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>Java
public class ExampleBean {
public void cleanup() {
// do some destruction work (like releasing pooled connections)
}
}1
2
3
4
5
2
3
4
5
前面的定义几乎和下面的定义有着完全相同的效果:
XML
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>Java
public class AnotherExampleBean implements DisposableBean {
@Override
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}1
2
3
4
5
6
2
3
4
5
6
然而,前面两个定义中的第一个并不会将代码与 Spring 进行耦合。
Tip 1:你可以给
<bean>元素的destroy-method属性赋予特殊的(inferred)值,这会告诉 Spring 在特定的 Bean 类上自动检测公共的close或shutdown方法。(因此,任何实现了java.lang.AutoCloseable或java.io.Closeable的类都会匹配。)你还可以在<beans>元素的default-destroy-method属性上设置这个特殊的(inferred)值,以将这种行为应用到一组 Bean 上(参见默认的初始化和销毁方法)。请注意,这是 Java 配置类中@Bean方法的默认行为。
Tip 2:为了延长关闭阶段,你可以实现
Lifecycle接口,并在调用任何单例 Bean 的销毁方法之前接收一个早期停止信号。你也可以实现SmartLifecycle,以进行有时限的停止步骤,容器将等待所有此类停止处理完成,然后再继续执行销毁方法。
2.1.3. 默认的初始化和销毁方法
当你编写初始化和销毁方法回调时,如果不使用 Spring 特定的 InitializingBean 和 DisposableBean 回调接口,通常会使用诸如 init()、initialize()、dispose() 等名称的方法。理想情况下,此类生命周期回调方法的命名在整个项目中应该是统一的,以确保所有开发人员都使用相同的方法名称并保持一致性。
你可以配置 Spring 容器来 “查找” 每个 Bean 上命名的初始化和销毁回调方法。这意味着作为应用程序开发人员,你可以编写你的应用程序类,并使用名为 init() 的初始化回调,而无需在每个 Bean 定义中配置 init-method="init" 属性。Spring IoC 容器在创建 Bean 时(并遵循前面描述的标准生命周期回调契约)调用该方法。这个特性还可以强制执行初始化和销毁方法回调的一致命名约定。
假设你的初始化回调方法命名为 init(),销毁回调方法命名为 destroy()。那么你的类将类似于以下示例中的类:
Java
public class DefaultBlogService implements BlogService {
private BlogDao blogDao;
public void setBlogDao(BlogDao blogDao) {
this.blogDao = blogDao;
}
// this is (unsurprisingly) the initialization callback method
public void init() {
if (this.blogDao == null) {
throw new IllegalStateException("The [blogDao] property must be set.");
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
你可以在一个类似以下示例的 Bean 中使用该类:
XML
<beans default-init-method="init">
<bean id="blogService" class="com.something.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
</bean>
</beans>1
2
3
4
5
6
7
2
3
4
5
6
7
在顶层 <beans/> 元素上存在的 default-init-method 属性会让 Spring IoC 容器识别 Bean 类中名为 init 的方法作为初始化方法回调。当创建和组装 Bean 时,如果 Bean 类有这样的方法,它会在适当的时候被调用。
你可以类似地(在 XML 中)通过在顶层 <beans/> 元素上使用 default-destroy-method 属性来配置销毁方法回调。
对于已经存在的 Bean 类,如果它们的回调方法命名与约定不符,你可以通过在 <bean/> 元素本身中使用 init-method 和 destroy-method 属性来覆盖默认值(在 XML 中)。
Spring 容器保证在 Bean 被提供所有依赖项后立即调用配置的初始化回调。因此,初始化回调是在原始 Bean 引用上调用的,这意味着 AOP 拦截器等还没有应用到 Bean 上。首先完全创建目标 Bean,然后应用 AOP 代理(例如)及其拦截器链。如果目标 Bean 和代理是分开定义的,你的代码甚至可以直接与原始目标 Bean 交互,绕过代理。因此,将拦截器应用到 init 方法上会不一致,因为这样做会将目标 Bean 的生命周期与其代理或拦截器耦合在一起,并在你的代码直接与原始目标 Bean 交互时产生奇怪的语义。
2.1.4. 组合生命周期机制
从 Spring 2.5 开始,你有三种选择来控制 Bean 的生命周期行为:
InitializingBean和DisposableBean回调接口;- 自定义的
init()和destroy()方法; @PostConstruct和@PreDestroy注解;
你可以结合使用这些机制来控制给定 Bean 的生命周期。
Note:如果为一个 Bean 配置了多个生命周期机制,并且每个机制都配置了不同的方法名称,那么每个配置的方法会按照它们在列表中的顺序依次执行。然而,如果为这些生命周期机制中的一个方法配置了相同的方法名称(例如,用于初始化方法的
init()),那么该方法只会被执行一次,就像前面的部分所解释的那样。
对于同一个 Bean 配置了多个不同的初始化方法,它们的调用顺序如下:
- 带有
@PostConstruct注解的方法; InitializingBean回调接口中定义的afterPropertiesSet()方法;- 自定义配置的
init()方法;
销毁方法的调用顺序相同:
- 带有
@PreDestroy注解的方法; DisposableBean回调接口中定义的destroy()方法;- 自定义配置的
destroy()方法;
2.1.5. 启动和关闭回调
Lifecycle 接口定义了任何具有自己生命周期需求的对象的基本方法(例如启动和停止某些后台进程):
Java
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}1
2
3
4
5
2
3
4
5
任何由 Spring 管理的对象都可以实现 Lifecycle 接口。然后,当 ApplicationContext 本身接收到启动和停止信号(例如,在运行时进行停止/重启场景时),它会将这些调用级联到上下文中定义的所有 Lifecycle 实现。它通过委托给一个 LifecycleProcessor 来实现这一点,如下面的示例所示。
Java
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}1
2
3
4
2
3
4
请注意,LifecycleProcessor 本身也是 Lifecycle 接口的扩展。它还添加了另外两个方法,用于对上下文的刷新和关闭做出反应。
Tip
请注意,常规的
org.springframework.context.Lifecycle接口是用于显式启动和停止通知的简单契约,并不意味着在上下文刷新时自动启动。如果需要对自动启动进行精细控制,并且需要对特定 Bean 进行优雅停止(包括启动和停止阶段),请考虑实现扩展的org.springframework.context.SmartLifecycle接口。另外,请注意停止(stop)通知不能保证在销毁(destroy)之前。在常规关闭时,所有的
LifecycleBean 在通用销毁回调被传播之前会先收到停止通知。然而,在上下文生命周期内进行的热刷新或停止刷新尝试时,只会调用销毁方法。
启动和关闭调用的顺序可能很重要。如果任何两个对象之间存在 “depends-on” 关系,则依赖方在其依赖项之后启动,并在其依赖项之前停止。但是,有时候直接的依赖关系是未知的。你可能只知道某种类型的对象应该在另一种类型的对象之前启动。在这种情况下,SmartLifecycle 接口定义了另一个选项,即 getPhase() 方法,该方法在其父接口 Phased 上定义。以下是 Phased 接口的定义:
Java
public interface Phased {
int getPhase();
}1
2
3
2
3
以下清单显示了 SmartLifecycle 接口的定义:
Java
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}1
2
3
4
5
2
3
4
5
在启动时,具有最低阶段的对象首先启动。在停止时,按相反的顺序进行。因此,实现了 SmartLifecycle 接口并且其 getPhase() 方法返回 Integer.MIN_VALUE 的对象将会是最先启动和最后停止的对象之一。在另一端,阶段值为 Integer.MAX_VALUE 将表示该对象应该最后启动和最先停止(可能是因为它依赖于其他正在运行的进程)。在考虑阶段值时,还需要知道任何未实现 SmartLifecycle 接口的 “普通” Lifecycle 对象的默认阶段是 0。因此,任何负的阶段值表示对象应该在这些标准组件之前启动(并在它们之后停止)。对于任何正的阶段值,情况相反。
SmartLifecycle 定义的停止方法接受一个回调。任何实现都必须在该实现的关闭过程完成后调用该回调的 run() 方法。这样可以在必要时进行异步关闭,因为 LifecycleProcessor 接口的默认实现 DefaultLifecycleProcessor 会等待每个阶段内的对象组调用该回调,其默认每个阶段的超时时间为 30 秒。您可以通过在上下文中定义一个名为 lifecycleProcessor 的 Bean 来覆盖默认的生命周期处理器实例。如果只想修改超时时间,则定义以下内容就足够了:
XML
<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
<!-- timeout value in milliseconds -->
<property name="timeoutPerShutdownPhase" value="10000"/>
</bean>1
2
3
4
2
3
4
如前所述,LifecycleProcessor 接口还定义了用于刷新和关闭上下文的回调方法。后者在上下文关闭时驱动关闭过程,就好像显式调用了 stop() 方法一样。相反,刷新回调则启用了 SmartLifecycle Bean 的另一个特性。当上下文刷新时(在所有对象都已实例化和初始化之后),该回调被调用。在那时,默认的生命周期处理器会检查每个 SmartLifecycle 对象的 isAutoStartup() 方法返回的布尔值。如果为 true,则该对象将在那时启动,而不是等待上下文或其自身的 start() 方法的显式调用(与上下文刷新不同,标准上下文实现不会自动启动上下文)。phase 值和任何 “depends-on” 关系决定了启动顺序,如前所述。
2.1.6. 在非 Web 应用中优雅地关闭 Spring IoC 容器
这一部分仅适用于非 Web 应用程序。Spring 的基于 Web 的 ApplicationContext 实现已经有了相关的代码,可以在相关的 Web 应用程序关闭时优雅地关闭 Spring IoC 容器。
如果你在非 Web 应用程序环境中(例如,富客户端桌面环境)使用 Spring 的 IoC 容器,请在 JVM 中注册一个关闭钩子。这样做可以确保优雅地关闭,并调用你的单例 Bean 上的相关销毁方法,以释放所有资源。你仍然必须正确配置和实现这些销毁回调。
要注册关闭钩子,请调用 ConfigurableApplicationContext 接口声明的 registerShutdownHook() 方法,如下例所示。
Java
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Boot {
public static void main(final String[] args) throws Exception {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
// add a shutdown hook for the above context...
ctx.registerShutdownHook();
// app runs here...
// main method exits, hook is called prior to the app shutting down...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2.2. ApplicationContextAware 和 BeanNameAware
当一个 ApplicationContext 创建一个实现了 org.springframework.context.ApplicationContextAware 接口的对象实例时,该实例会获得对该 ApplicationContext 的引用。以下列出了 ApplicationContextAware 接口的定义:
Java
public interface ApplicationContextAware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}1
2
3
2
3
因此,Bean 可以通过 ApplicationContext 接口或将引用转换为此接口的已知子类(例如 ConfigurableApplicationContext,它公开了额外的功能)来以编程方式操作创建它们的 ApplicationContext。一个用途是以编程方式检索其他 Bean。有时这种能力是有用的。但是,一般来说,应该避免使用它,因为它将代码与 Spring 紧密耦合,并且不遵循控制反转的风格,其中协作者作为属性提供给 Bean。ApplicationContext 的其他方法提供了访问文件资源、发布应用程序事件和访问 MessageSource 的功能。这些额外的特性在 ApplicationContext 的附加功能中进行了描述。
自动装配是获得对 ApplicationContext 的引用的另一种选择。传统的 constructor 和 byType 自动装配模式(如自动装配协作者中所述)可以分别为构造函数参数或 Setter 方法参数提供 ApplicationContext 类型的依赖项。为了获得更多的灵活性,包括自动装配字段和多参数方法的能力,可以使用基于注解的自动装配功能。如果你这样做,当字段、构造函数或方法带有 @Autowired 注解时,ApplicationContext 会自动装配到期望 ApplicationContext 类型的字段、构造函数参数或方法参数中。有关更多信息,请参阅使用 @Autowired。
当 ApplicationContext 创建一个实现 org.springframework.beans.factory.BeanNameAware 接口的类时,该类将获得对其关联对象定义中定义的名称的引用。以下代码显示了 BeanNameAware 接口的定义:
Java
public interface BeanNameAware {
void setBeanName(String name) throws BeansException;
}1
2
3
2
3
回调在普通 Bean 属性填充完成后但在初始化回调(如 InitializingBean.afterPropertiesSet() 或自定义的 “init” 方法)之前被调用。
2.3. 其他 Aware 接口
除了之前讨论过的 ApplicationContextAware 和 BeanNameAware,Spring 提供了一系列的 Aware 回调接口,让 Bean 可以向容器表明它们需要某种特定的基础设施依赖。一般来说,名称就表明了依赖类型。下表总结了最重要的 Aware 接口:
| 接口名称 | 注入的依赖项 | 解释 |
|---|---|---|
ApplicationContextAware | 声明 ApplicationContext | ApplicationContextAware 和 BeanNameAware |
ApplicationEventPublisherAware | 封装 ApplicationContext 的事件发布器 | ApplicationContext 的附加功能 |
BeanClassLoaderAware | 用于加载 Bean 类的类加载器 | 实例化 Bean |
BeanFactoryAware | 声明 BeanFactory | BeanFactory API |
BeanNameAware | 声明 Bean 的名称 | ApplicationContextAware 和 BeanNameAware |
LoadTimeWeaverAware | 加载期间处理类定义的织入器 | Spring 框架中的 AspectJ 加载时织入 |
MessageSourceAware | 解析消息的策略(支持参数化和国际化) | ApplicationContext 的附加功能 |
NotificationPublisherAware | Spring JMX 通知发布器 | 通知 |
ResourceLoaderAware | 用于低级别访问资源的加载器 | 资源 |
ServletConfigAware | 容器当前运行的 ServletConfig(仅在 Web 感知的 Spring ApplicationContext 中有效) | Spring MVC |
ServletContextAware | 容器当前运行的 ServletContext(仅在 Web 感知的 Spring ApplicationContext 中有效) | Spring MVC |
Aware 接口请注意,使用这些接口将您的代码与 Spring API 绑定在一起,不符合控制反转的风格。因此,我们建议将它们用于需要以编程方式访问容器的基础设施 Bean。
3. Bean 定义继承
一个 Bean 定义可以包含大量的配置信息,包括构造函数参数、属性值和容器特定的信息,比如初始化方法、静态工厂方法名称等等。子 Bean 定义会继承父定义的配置数据。子定义可以根据需要覆盖一些值或添加其他值。使用父子 Bean 定义可以节省很多输入。实际上,这是一种模板化的形式。
如果您通过编程方式使用 ApplicationContext 接口,子 Bean 定义将由 ChildBeanDefinition 类表示。大多数用户不会在这个级别上使用它们。相反,他们会在诸如 ClassPathXmlApplicationContext 这样的类中以声明的方式配置 Bean 定义。当您使用基于 XML 的配置元数据时,可以通过使用 parent 属性来指示一个子 Bean 定义,将父 Bean 指定为此属性的值。以下示例显示了如何这样做:
XML
<bean id="inheritedTestBean" abstract="true"
class="org.springframework.beans.TestBean">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithDifferentClass"
class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBean" init-method="initialize">
<property name="name" value="override"/>
<!-- the age property value of 1 will be inherited from parent -->
</bean>1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
子 Bean 定义可以使用父定义中的 Bean 类(如果未指定),但也可以进行覆盖。在后一种情况下,子 Bean 类必须与父类兼容(即,它必须接受父类的属性值)。
子 Bean 定义从父定义中继承作用域、构造函数参数值、属性值和方法重写,同时可以添加新值。您指定的任何作用域、初始化方法、销毁方法或 static 工厂方法设置都会覆盖相应的父设置。
剩余的设置始终从子定义中获取:依赖项、自动装配模式、依赖项检查、单例和延迟初始化。
在前面的示例中,通过使用 abstract 属性将父 Bean 定义显式标记为抽象。如果父定义未指定类,则需要显式将父 Bean 定义标记为 abstract,如下例所示:
XML
<bean id="inheritedTestBeanWithoutClass" abstract="true">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBeanWithoutClass" init-method="initialize">
<property name="name" value="override"/>
<!-- age will inherit the value of 1 from the parent bean definition-->
</bean>1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
由于父 Bean 定义不完整,并且被显式标记为 abstract,因此无法单独实例化父 Bean。当一个定义是 abstract 时,它只能用作纯模板 Bean 定义,作为子定义的父定义。如果试图单独使用这样的 abstract 父 Bean,例如将其作为另一个 Bean 的 ref 属性引用或使用显式的 getBean() 调用其父 Bean 的 ID,将会返回错误。同样地,容器内部的 preInstantiateSingletons() 方法会忽略被定义为抽象的 Bean 定义。
Note:
ApplicationContext默认情况下会预先实例化所有的单例 Bean。因此,对于你只想将其作为模板使用的(父)Bean 定义,尤其是对于单例 Bean,如果这个定义指定了一个类,你必须确保将abstract属性设置为true,否则应用程序上下文实际上会尝试预先实例化这个abstractBean。
4. 容器扩展点
通常情况下,应用程序开发人员不需要继承 ApplicationContext 实现类。相反,Spring IoC 容器可以通过插入特殊集成接口的实现来进行扩展。接下来的几节将描述这些集成接口。
4.1. 使用 BeanPostProcessor 自定义 Bean
BeanPostProcessor 接口定义了回调方法,您可以实现这些方法来提供自己的(或覆盖容器的默认)实例化逻辑、依赖关系解析逻辑等。如果您希望在 Spring 容器完成实例化、配置和初始化 Bean 后实现一些自定义逻辑,可以插入一个或多个自定义的 BeanPostProcessor 实现。
您可以配置多个 BeanPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanPostProcessor 实例的运行顺序。只有当 BeanPostProcessor 实现了 Ordered 接口时,您才能设置这个属性。如果您编写自己的 BeanPostProcessor,您应该考虑实现 Ordered 接口。更多详细信息,请参阅 BeanPostProcessor 和 Ordered 接口的 Java 文档。
Note
BeanPostProcessor实例操作 Bean(或对象)实例。也就是说,Spring IoC 容器实例化一个 Bean 实例,然后BeanPostProcessor实例开始工作。
BeanPostProcessor实例的作用范围是每个容器。这只在你使用容器层次结构时才相关。如果你在一个容器中定义了一个BeanPostProcessor,它只对该容器中的 Bean 后置处理。换句话说,一个容器中定义的 Bean 不会被另一个容器中定义的BeanPostProcessor后置处理,即使这两个容器都是同一层次结构的一部分。要更改实际的 Bean 定义(也就是定义 Bean 的蓝图),你需要使用
BeanFactoryPostProcessor,如使用BeanFactoryPostProcessor自定义配置元数据中所述。
org.springframework.beans.factory.config.BeanPostProcessor 接口正好包含两个回调方法。当这样的类被注册为容器的后处理器时,对于容器创建的每个 Bean 实例,后处理器都会从容器中获得回调,分别在调用容器初始化方法(例如 InitializingBean.afterPropertiesSet() 或任何声明的 init 方法)之前,以及在任何 Bean 初始化回调之后。后处理器可以对 Bean 实例采取任何操作,包括完全忽略回调。Bean 后处理器通常会检查回调接口,或者它可能用代理包装一个 Bean。一些 Spring AOP 基础设施类被实现为 Bean 后处理器,以提供代理包装逻辑。
一个 ApplicationContext 会自动检测在配置元数据中定义的任何实现 BeanPostProcessor 接口的 Bean。ApplicationContext 会将这些 Bean 注册为后处理器,以便在 Bean 创建时后续调用。Bean 后处理器可以像任何其他 Bean 一样在容器中部署。
需要注意的是,当在配置类上使用 @Bean 工厂方法声明一个 BeanPostProcessor 时,工厂方法的返回类型应该是实现类本身,或者至少是 org.springframework.beans.factory.config.BeanPostProcessor 接口,明确指示该 Bean 的后处理器性质。否则,ApplicationContext 在完全创建之前无法按类型自动检测它。由于 BeanPostProcessor 需要在早期实例化以应用于上下文中其他 Bean 的初始化,因此这种早期类型检测至关重要。
Tip:以编程方式注册
BeanPostProcessor实例虽然推荐的
BeanPostProcessor注册方式是通过ApplicationContext的自动检测(如前所述),但你可以通过使用addBeanPostProcessor方法在ConfigurableBeanFactory上以编程的方式注册它们。这在你需要在注册之前评估条件逻辑,甚至在层次结构中跨上下文复制 Bean 后处理器时非常有用。但需要注意的是,通过编程方式添加的BeanPostProcessor实例不遵循Ordered接口。在这里,注册顺序决定了执行顺序。此外,请注意,无论有没有显式排序,通过编程方式注册的BeanPostProcessor实例始终在自动检测注册的实例之前处理。
Warning:
BeanPostProcessor实例和 AOP 自动代理实现
BeanPostProcessor接口的类是特殊的,容器对它们进行了特殊处理。所有BeanPostProcessor实例及其直接引用的 Bean 都会在启动时实例化,作为ApplicationContext特殊启动阶段的一部分。然后,所有BeanPostProcessor实例都以排序的方式注册,并应用于容器中的所有后续 Bean。因为 AOP 自动代理本身就是一个BeanPostProcessor,所以无论是BeanPostProcessor实例还是它们直接引用的 Bean 都不符合自动代理的条件,因此不会被织入切面。对于这样的 Bean,你应该会看到一个信息日志消息:
Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)。如果你使用自动装配或
@Resource(可能会回退到自动装配)将 Bean 连接到你的BeanPostProcessor,Spring 在搜索类型匹配的依赖项候选时可能会访问到意外的 Bean,因此,使它们没有资格进行自动代理或其他类型的 Bean 后置处理。例如,如果你有一个用@Resource注解的依赖项,其中字段或 Setter 名称并不直接对应 Bean 的声明名称,并且没有使用name属性,Spring 会通过类型匹配来访问其他的 Bean。
以下示例展示了如何在 ApplicationContext 中编写、注册和使用 BeanPostProcessor 实例。
4.1.1. 示例:Hello World,BeanPostProcessor 风格
这个例子展示了基本的用法。例子中展示了一个自定义的 BeanPostProcessor 实现,它在容器创建每个 Bean 时调用 toString() 方法,并将结果字符串打印到系统控制台。
以下代码展示了自定义 BeanPostProcessor 实现类的定义:
Java
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// simply return the instantiated bean as-is
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean; // we could potentially return any object reference here...
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
以下 beans 元素使用了 InstantiationTracingBeanPostProcessor:
XML
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
https://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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
请注意 InstantiationTracingBeanPostProcessor 仅仅被定义了,甚至没有一个名字,因为它是一个 Bean,所以可以像任何其他 Bean 一样进行依赖注入。前面的配置还定义了一个由 Groovy 脚本支持的 Bean。有关 Spring 动态语言支持的详细介绍请参阅这里。
以下 Java 应用程序运行了前面的代码和配置:
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 = ctx.getBean("messenger", Messenger.class);
System.out.println(messenger);
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
以上应用程序的输出类似于以下内容:
Text
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@2729611
2
2
4.1.2. 示例:AutowiredAnnotationBeanPostProcessor
在自定义 BeanPostProcessor 实现中使用回调接口或注解是扩展 Spring IoC 容器的常见方式。一个例子是 Spring 的 AutowiredAnnotationBeanPostProcessor,它是一个随 Spring 发行的 BeanPostProcessor 实现,用于自动装配带有注解的字段、Setter 方法和任意配置方法。
4.2. 使用 BeanFactoryPostProcessor 自定义配置元数据
接下来我们要看的扩展点是 org.springframework.beans.factory.config.BeanFactoryPostProcessor。这个接口的语义与 BeanPostProcessor 类似,但有一个主要区别:BeanFactoryPostProcessor 操作的是 Bean 配置元数据。也就是说,Spring IoC 容器允许 BeanFactoryPostProcessor 读取配置元数据,并在容器实例化除 BeanFactoryPostProcessor 实例以外的任何 Bean 之前对其进行潜在修改。
你可以配置多个 BeanFactoryPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanFactoryPostProcessor 实例的运行顺序。但是,只有当 BeanFactoryPostProcessor 实现了 Ordered 接口时,你才能设置这个属性。如果你编写自己的 BeanFactoryPostProcessor,也应该考虑实现 Ordered 接口。详细信息请参阅 BeanFactoryPostProcessor 和 Ordered 接口的 JavaDoc。
Note
如果你想要改变实际的 Bean 实例(也就是从配置元数据创建的对象),那么你需要使用
BeanPostProcessor(在前面的使用BeanPostProcessor自定义 Bean 一节中有描述)。虽然在BeanFactoryPostProcessor中技术上也可以操作 Bean 实例(例如,通过使用BeanFactory.getBean()),但这样做会导致 Bean 过早地被实例化,违反了标准的容器生命周期。这可能会引起负面影响,比如绕过 Bean 后置处理。另外,
BeanFactoryPostProcessor实例的作用范围是每个容器。这只在你使用容器层次结构时才相关。如果你在一个容器中定义了一个BeanFactoryPostProcessor,它只对该容器中的 Bean 定义生效。换句话说,一个容器中定义的 Bean 不会被另一个容器中定义的BeanFactoryPostProcessor处理,即使这两个容器都是同一层次结构的一部分。
当 BeanFactoryPostProcessor 声明在 ApplicationContext 中时,它会自动运行,以便对定义容器的配置元数据进行更改。Spring 包含许多预定义的 BeanFactoryPostProcessor,例如 PropertyOverrideConfigurer 和 PropertySourcesPlaceholderConfigurer。你也可以使用自定义的 BeanFactoryPostProcessor,例如注册自定义属性编辑器。
ApplicationContext 会自动检测部署到其中的任何实现了 BeanFactoryPostProcessor 接口的 Bean。它会在适当的时候将这些 Bean 用作 Bean 工厂后处理器。你可以像部署其他 Bean 一样部署这些后处理器 Bean。
Note:就像
BeanPostProcessor一样,你通常不希望将BeanFactoryPostProcessor配置为延迟初始化。如果没有其他 Bean 引用Bean(Factory)PostProcessor,那么该后处理器将根本不会被实例化。因此,将其标记为延迟初始化将被忽略,即使你在声明<beans />元素时将default-lazy-init属性设置为true,Bean(Factory)PostProcessor也会被急切地实例化。
4.2.1. 示例:类名替换 PropertySourcesPlaceholderConfigurer
你可以使用 PropertySourcesPlaceholderConfigurer 通过标准的 Java Properties 格式,将 Bean 定义中的属性值外部化到一个单独的文件中。这样做可以使部署应用程序的人能够自定义特定于环境的属性,如数据库 URL 和密码,而无需修改主 XML 定义文件或容器的文件,从而避免了复杂性和风险。
考虑以下基于 XML 的配置元数据片段,其中定义了一个带有占位符值的 DataSource:
Java
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations" value="classpath:com/something/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>1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
该示例显示了从外部 Properties 文件配置的属性。在运行时,PropertySourcesPlaceholderConfigurer 被应用于元数据,替换 DataSource 的一些属性。要替换的值被指定为 ${property-name} 形式的占位符,这遵循 Ant、log4j 和 JSP EL 的风格。
实际的值来自另一个采用标准 Java Properties 格式的文件:
Properties
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root1
2
3
4
2
3
4
因此,${jdbc.username} 字符串在运行时被替换为值 sa,对于在属性文件中与键匹配的其他占位符值也是如此。PropertySourcesPlaceholderConfigurer 会检查 Bean 定义的大多数属性中的占位符。此外,你可以自定义占位符的前缀和后缀。
在 Spring 2.5 中引入的 context 命名空间后,你可以使用专用的配置元素配置属性占位符。你可以在 location 属性中,以逗号分隔的列表形式,提供一个或多个位置,如下例所示:
XML
<context:property-placeholder location="classpath:com/something/jdbc.properties"/>PropertySourcesPlaceholderConfigurer 不仅在你指定的 Properties 文件中查找属性。默认情况下,如果它在指定的属性文件中找不到属性,它还会检查 Spring Environment 属性和常规 Java System 属性。
Tip
你可以使用
PropertySourcesPlaceholderConfigurer来替换类名,这在你需要在运行时选择特定的实现类时有时会很有用。以下示例展示了如何做到这一点:XML<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer"> <property name="locations"> <value>classpath:com/something/strategy.properties</value> </property> <property name="properties"> <value>custom.strategy.class=com.something.DefaultStrategy</value> </property> </bean> <bean id="serviceStrategy" class="${custom.strategy.class}"/>1
2
3
4
5
6
7
8
9
10如果在运行时无法将类解析为有效的类,那么在即将创建 Bean 时,Bean 的解析会失败,这发生在
ApplicationContext的preInstantiateSingletons()阶段(对于非延迟初始化的 Bean)。
4.2.2. 示例:PropertyOverrideConfigurer
PropertyOverrideConfigurer 是另一个 Bean 工厂后处理器,类似于 PropertySourcesPlaceholderConfigurer,但与后者不同的是,原始定义可以为 Bean 属性设置默认值,或者根本不设置任何值。如果覆盖的 Properties 文件中没有某个 Bean 属性的条目,则会使用默认的上下文定义。
需要注意的是,Bean 定义并不知道自己被覆盖了,因此从 XML 定义文件中并不能立即看出覆盖配置器正在使用。如果有多个 PropertyOverrideConfigurer 实例为同一个 Bean 属性定义了不同的值,由于覆盖机制,最后一个实例会覆盖之前的值。
属性文件的配置行采用以下格式:
Properties
beanName.property=value以下清单显示了格式的示例:
Properties
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb1
2
2
这个示例文件可以与一个包含名为 dataSource 的 Bean 的容器定义一起使用,该 Bean 具有 driver 和 url 属性。
复合属性名也是受支持的,只要路径的每个组件(除了要被覆盖的最终属性之外)都已经是非空的(可能是由构造函数初始化的)。在以下示例中,tom Bean 的 fred 属性的 bob 属性的 sammy 属性被设置为标量值 123:
Properties
tom.fred.bob.sammy=123Note:指定的覆盖值始终是字面值。它们不会被转换为 Bean 引用。当 XML Bean 定义中的原始值指定了一个 Bean 引用时,这个约定也适用。
在 Spring 2.5 中引入的 context 命名空间后,可以使用专用的配置元素来配置属性覆盖,如下面的示例所示:
XML
<context:property-override location="classpath:override.properties"/>4.3. 使用 FactoryBean 自定义实例化逻辑
你可以为本身就是工厂的对象实现 org.springframework.beans.factory.FactoryBean 接口。
FactoryBean 接口是 Spring IoC 容器实例化逻辑的可插入点。如果你有复杂的初始化代码,最好用 Java 表达,而不是 (可能会很冗长的) XML,你可以创建自己的 FactoryBean,在该类内部编写复杂的初始化代码,然后将自定义的 FactoryBean 注入到容器中。
FactoryBean<T> 接口提供了三个方法:
T getObject(): 返回该工厂创建的对象的实例。实例可能是共享的,取决于该工厂返回的是单例还是原型;boolean isSingleton(): 如果该FactoryBean返回单例,则返回true,否则返回false。该方法的默认实现返回true;Class<?> getObjectType(): 返回getObject()方法返回的对象类型,如果类型事先不知道,则返回null;
FactoryBean 的概念和接口在 Spring 框架的许多地方都有应用。Spring 本身就提供了超过 50 个 FactoryBean 接口的实现。
当你需要向容器请求一个 FactoryBean 实例本身而不是它所生产的 Bean 时,可以在调用 ApplicationContext 的 getBean() 方法时,在 Bean 的 id 前面加上 & 符号。因此,对于一个 id 为 myBean 的 FactoryBean,在容器上调用 getBean("myBean") 将返回 FactoryBean 的产品,而调用 getBean("&myBean") 将返回 FactoryBean 实例本身。