定制 Bean 的特性 (Customizing the Nature of a Bean)
Spring 框架提供了许多接口,你可以使用这些接口来定制 Bean 的特性。本节将它们分为以下几类:
生命周期回调 (Lifecycle Callbacks)
为了与容器对 Bean 生命周期的管理进行交互,你可以实现 Spring 的 InitializingBean 和 DisposableBean 接口。容器为前者调用 afterPropertiesSet(),为后者调用 destroy(),以便让 Bean 在初始化和销毁时执行某些操作。
提示
在现代 Spring 应用中,JSR-250 的 @PostConstruct 和 @PreDestroy 注解通常被认为是接收生命周期回调的最佳实践。使用这些注解意味着你的 Bean 不会与 Spring 特定的接口耦合。详情请参阅使用 @PostConstruct 和 @PreDestroy。
如果你不想使用 JSR-250 注解但仍想消除耦合,请考虑使用 init-method 和 destroy-method Bean 定义元数据。
在内部,Spring 框架使用 BeanPostProcessor 实现来处理它能找到的任何回调接口并调用适当的方法。如果你需要自定义特性或 Spring 默认不提供的其他生命周期行为,你可以自己实现 BeanPostProcessor。更多信息请参阅容器扩展点 (Container Extension Points)。
除了初始化和销毁回调之外,Spring 管理的对象还可以实现 Lifecycle 接口,以便这些对象可以参与由容器自身生命周期驱动的启动和关闭过程。
本节将介绍这些生命周期回调接口。
初始化回调 (Initialization Callbacks)
org.springframework.beans.factory.InitializingBean 接口允许 Bean 在容器设置了 Bean 上所有必要的属性后执行初始化工作。InitializingBean 接口指定了一个方法:
void afterPropertiesSet() throws Exception;我们建议你不要使用 InitializingBean 接口,因为它会将代码不必要地耦合到 Spring。或者,我们建议使用 @PostConstruct 注解或指定 POJO 初始化方法。对于基于 XML 的配置元数据,你可以使用 init-method 属性来指定具有 void 无参数签名的方法名称。对于 Java 配置,你可以使用 @Bean 的 initMethod 属性。请参阅接收生命周期回调。考虑以下示例:
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>public class ExampleBean {
public void init() {
// 执行一些初始化工作
}
}class ExampleBean {
fun init() {
// 执行一些初始化工作
}
}上面的示例与下面的示例(由两个列表组成)的效果几乎完全相同:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>public class AnotherExampleBean implements InitializingBean {
@Override
public void afterPropertiesSet() {
// 执行一些初始化工作
}
}class AnotherExampleBean : InitializingBean {
override fun afterPropertiesSet() {
// 执行一些初始化工作
}
}然而,前两个示例中的第一个并没有将代码耦合到 Spring。
注意
请注意,@PostConstruct 和一般的初始化方法是在容器的单例创建锁内执行的。只有在从 @PostConstruct 方法返回后,Bean 实例才被视为完全初始化并准备好发布给其他人。此类单独的初始化方法仅用于验证配置状态,并可能根据给定的配置准备一些数据结构,但不应进行进一步的外部 Bean 访问活动。否则,存在初始化死锁的风险。
对于需要触发极其耗时的后初始化活动(例如异步数据库准备步骤)的场景,你的 Bean 应该实现 SmartInitializingSingleton.afterSingletonsInstantiated() 或依靠上下文刷新事件:实现 ApplicationListener<ContextRefreshedEvent> 或声明其注解等效项 @EventListener(ContextRefreshedEvent.class)。这些变体在所有常规单例初始化之后发生,因此在任何单例创建锁之外。
或者,你可以实现 (Smart)Lifecycle 接口,并与容器的整体生命周期管理集成,包括自动启动机制、预销毁停止步骤以及潜在的停止/重启回调(见下文)。
销毁回调 (Destruction Callbacks)
实现 org.springframework.beans.factory.DisposableBean 接口可以让 Bean 在包含它的容器被销毁时获得回调。DisposableBean 接口指定了一个方法:
void destroy() throws Exception;我们建议你不要使用 DisposableBean 回调接口,因为它会将代码不必要地耦合到 Spring。或者,我们建议使用 @PreDestroy 注解或指定 Bean 定义支持的通用方法。对于基于 XML 的配置元数据,你可以在 <bean/> 上使用 destroy-method 属性。对于 Java 配置,你可以使用 @Bean 的 destroyMethod 属性。请参阅接收生命周期回调。考虑以下定义:
<bean id="exampleDestructionBean" class="examples.ExampleBean" destroy-method="cleanup"/>public class ExampleBean {
public void cleanup() {
// 执行一些销毁工作(如释放连接池连接)
}
}class ExampleBean {
fun cleanup() {
// 执行一些销毁工作(如释放连接池连接)
}
}上述定义与以下定义的效果几乎完全相同:
<bean id="exampleDestructionBean" class="examples.AnotherExampleBean"/>public class AnotherExampleBean implements DisposableBean {
@Override
public void destroy() {
// 执行一些销毁工作(如释放连接池连接)
}
}class AnotherExampleBean : DisposableBean {
override fun destroy() {
// 执行一些销毁工作(如释放连接池连接)
}
}然而,前两个定义中的第一个并没有将代码耦合到 Spring。
请注意,Spring 还支持推断销毁方法,检测名为 close 或 shutdown 的公共方法。这是 Java 配置类中 @Bean 方法的默认行为,并自动匹配 java.lang.AutoCloseable 或 java.io.Closeable 实现,同样不会将销毁逻辑耦合到 Spring。
提示
对于 XML 中的销毁方法推断,你可以为 <bean> 元素的 destroy-method 属性分配一个特殊的 (inferred) 值,这会指示 Spring 自动检测特定 Bean 定义类上的公共 close 或 shutdown 方法。你还可以在 <beans> 元素的 default-destroy-method 属性上设置此特殊值,以将此行为应用于整个 Bean 定义集(请参阅默认初始化和销毁方法)。
注意
为了扩展关闭阶段,你可以实现 Lifecycle 接口,并在调用任何单例 Bean 的销毁方法之前接收早期停止信号。你还可以实现 SmartLifecycle 以实现有时间限制的停止步骤,容器将等待所有此类停止处理完成后再继续执行销毁方法。
默认初始化和销毁方法
当你编写不使用 Spring 特定的 InitializingBean 和 DisposableBean 回调接口的初始化和销毁方法回调时,你通常会编写名为 init()、initialize()、dispose() 等的方法。理想情况下,此类生命周期回调方法的名称在整个项目中应该是标准化的,以便所有开发人员使用相同的方法名称并确保一致性。
你可以配置 Spring 容器在每个 Bean 上“查找”命名的初始化和销毁回调方法名称。这意味着,作为应用开发人员,你可以编写应用程序类并使用名为 init() 的初始化回调,而无需为每个 Bean 定义配置 init-method="init" 属性。Spring IoC 容器在创建 Bean 时(并按照前面描述的标准生命周期回调契约)调用该方法。此特性还强制执行了初始化和销毁方法回调的一致命名约定。
假设你的初始化回调方法名为 init(),销毁回调方法名为 destroy()。那么你的类将类似于以下示例中的类:
public class DefaultBlogService implements BlogService {
private BlogDao blogDao;
public void setBlogDao(BlogDao blogDao) {
this.blogDao = blogDao;
}
// 这(不出所料)是初始化回调方法
public void init() {
if (this.blogDao == null) {
throw new IllegalStateException("The [blogDao] property must be set.");
}
}
}class DefaultBlogService : BlogService {
private var blogDao: BlogDao? = null
// 这(不出所料)是初始化回调方法
fun init() {
if (blogDao == null) {
throw IllegalStateException("The [blogDao] property must be set.")
}
}
}然后你可以像下面这样使用该类定义 Bean:
<beans default-init-method="init">
<bean id="blogService" class="com.something.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
</bean>
</beans>在顶级 <beans/> 元素属性上存在 default-init-method 属性会导致 Spring IoC 容器将 Bean 类上名为 init 的方法识别为初始化方法回调。在创建和组装 Bean 时,如果 Bean 类具有此类方法,则会在适当的时候调用它。
你可以通过在顶级 <beans/> 元素上使用 default-destroy-method 属性来类似地(在 XML 中)配置销毁方法回调。
如果现有的 Bean 类已经具有与约定不一致的回调方法名称,你可以通过使用 <bean/> 本身的 init-method 和 destroy-method 属性指定(在 XML 中)方法名称来覆盖默认值。
Spring 容器保证在 Bean 被提供所有依赖项后立即调用配置的初始化回调。因此,初始化回调是在原始 Bean 引用上调用的,这意味着 AOP 拦截器等尚未应用于该 Bean。目标 Bean 首先被完全创建,然后应用 AOP 代理(例如)及其拦截器链。如果目标 Bean 和代理是分开定义的,你的代码甚至可以与原始目标 Bean 交互,绕过代理。因此,将拦截器应用于 init 方法是不一致的,因为这样做会将目标 Bean 的生命周期耦合到其代理或拦截器,并在你的代码直接与原始目标 Bean 交互时留下奇怪的语义。
组合生命周期机制
从 Spring 2.5 开始,你有三种控制 Bean 生命周期行为的选择:
InitializingBean和DisposableBean回调接口- 自定义
init()和destroy()方法 @PostConstruct和@PreDestroy注解
你可以组合这些机制来控制给定的 Bean。
注意
如果为一个 Bean 配置了多种生命周期机制,并且每种机制都配置了不同的方法名称,那么配置的每个方法都将按此注释后的顺序列出运行。但是,如果配置了相同的方法名称(例如,初始化方法的 init())用于多种生命周期机制,那么该方法将运行一次,如上一节中所述。
为同一个 Bean 配置的具有不同初始化方法的多种生命周期机制按如下顺序调用:
- 用
@PostConstruct注解的方法 - 由
InitializingBean回调接口定义的afterPropertiesSet() - 自定义配置的
init()方法
销毁方法的顺序相同:
- 用
@PreDestroy注解的方法 - 由
DisposableBean回调接口定义的destroy() - 自定义配置的
destroy()方法
启动和关闭回调 (Startup and Shutdown Callbacks)
Lifecycle 接口为任何具有自己的生命周期要求(例如启动和停止某些后台进程)的对象定义了基本方法:
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}任何 Spring 管理的对象都可以实现 Lifecycle 接口。然后,当 ApplicationContext 本身接收到启动和停止信号时(例如,在运行时的停止/重新启动方案中),它会将这些调用级联到该上下文中定义的所有 Lifecycle 实现。它通过委托给 LifecycleProcessor 来执行此操作,如下面的列表所示:
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}请注意,LifecycleProcessor 本身是 Lifecycle 接口的扩展。它还添加了另外两个方法用于对上下文被刷新和关闭做出反应。
提示
请注意,常规的 org.springframework.context.Lifecycle 接口只是用于显式启动和停止通知的普通契约,并不意味着在上下文刷新时自动启动。为了对特定 Bean 的自动启动和优雅停止(包括启动和停止阶段)进行细粒度控制,请考虑实现扩展的 org.springframework.context.SmartLifecycle 接口。
另外,请注意停止通知不保证在销毁之前到来。在常规关闭时,所有 Lifecycle Bean 首先收到停止通知,然后才传播通用的销毁回调。然而,在上下文生命周期中的热刷新或停止的刷新尝试时,仅调用销毁方法。
启动和关闭调用的顺序非常重要。如果任何两个对象之间存在“depends-on”关系,则依赖方在其依赖项之后启动,并其依赖项之前停止。然而,有时直接依赖关系是未知的。你可能只知道某种类型的对象应该在另一种类型的对象之前启动。在这些情况下,SmartLifecycle 接口定义了另一个选项,即在其超接口 Phased 上定义的 getPhase() 方法。下面的列表显示了 Phased 接口的定义:
public interface Phased {
int getPhase();
}下列表显示了 SmartLifecycle 接口的定义:
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}启动时,相位(Phase)值最低的对象先启动。停止时,遵循相反的顺序。因此,实现 SmartLifecycle 且其 getPhase() 方法返回 Integer.MIN_VALUE 的对象将是最先启动并最后停止的对象。在光谱的另一端,相位值为 Integer.MAX_VALUE 将指示该对象应最后启动并最先停止(可能是因为它依赖于正在运行的其他进程)。在考虑相位值时,还必须知道不实现 SmartLifecycle 的任何“普通” Lifecycle 对象的默认相位为 0。因此,任何负相位值都表示对象应在这些标准组件之前启动(并在它们之后停止)。对于任何正相位值,情况正好相反。
SmartLifecycle 定义的停止方法接受一个回调。任何实现在该实现的关闭过程完成后必须调用该回调的 run() 方法。这在需要异步关闭的地方非常有用,因为 LifecycleProcessor 接口的默认实现 DefaultLifecycleProcessor 将等待每个相位内的对象组调用该回调,直到其超时值。默认的每个相位超时为 30 秒。你可以通过在上下文中定义名为 lifecycleProcessor 的 Bean 来覆盖默认的生命周期处理器实例。如果你只想修改超时,定义以下内容即可:
<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
<!-- 超时值(以毫秒为单位) -->
<property name="timeoutPerShutdownPhase" value="10000"/>
</bean>如前所述,LifecycleProcessor 接口也为刷新和关闭上下文定义了回调方法。后者驱动关闭过程,就像显式调用了 stop() 一样,但它发生在上下文关闭时。另一方面,“刷新”回调启用了 SmartLifecycle Bean 的另一个特性。当上下文被刷新时(在所有对象都被实例化和初始化之后),将调用该回调。此时,默认的生命周期处理器会检查每个 SmartLifecycle 对象 isAutoStartup() 方法返回的布尔值。如果为 true,则该对象此时启动,而不是等待显式调用上下文的或它自己的 start() 方法(与上下文刷新不同,常规上下文实现不会自动发生上下文启动)。phase 值和任何“depends-on”关系决定了启动顺序,如前所述。
在非 Web 应用程序中优雅地关闭 Spring IoC 容器
注意
本节仅适用于非 Web 应用程序。Spring 的基于 Web 的 ApplicationContext 实现已经具备了在相关 Web 应用程序关闭时优雅地关闭 Spring IoC 容器的代码。
如果在非 Web 应用程序环境(例如,富客户端桌面环境)中使用 Spring 的 IoC 容器,请向 JVM 注册一个关闭钩子(shutdown hook)。这样做可以确保优雅地关闭,并调用单例 Bean 上相关的销毁方法,以便释放所有资源。你仍然必须正确配置并实现这些销毁回调。
要注册关闭钩子,请调用 ConfigurableApplicationContext 接口上声明的 registerShutdownHook() 方法,如下例所示:
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");
// 为上述上下文添加关闭钩子...
ctx.registerShutdownHook();
// 应用在此运行...
// main 方法退出,钩子在应用关闭前被调用...
}
}import org.springframework.context.support.ClassPathXmlApplicationContext
fun main() {
val ctx = ClassPathXmlApplicationContext("beans.xml")
// 为上述上下文添加关闭钩子...
ctx.registerShutdownHook()
// 应用在此运行...
// main 方法退出,钩子在应用关闭前被调用...
}线程安全与可见性 (Thread Safety and Visibility)
Spring 核心容器以线程安全的方式发布创建的单例实例,通过单例锁保护访问并保证在其他线程中的可见性。
因此,应用提供的 Bean 类不必担心其初始化状态的可见性。只要常规配置字段仅在初始化阶段发生变化,就不必将其标记为 volatile,即使对于在初始阶段可变的基于 Setter 的配置状态,也能提供类似于 final 的可见性保证。如果此类字段在 Bean 创建阶段及其后续初始发布之后发生更改,则在访问时需要将其声明为 volatile 或由通用锁保护。
请注意,单例 Bean 实例(例如 Controller 实例或 Repository 实例)中的此类配置状态的并发访问在容器端进行此类安全的初始发布后是完全线程安全的。这包括在通用单例锁内处理的通用单例 FactoryBean 实例。
对于销毁回调,配置状态保持线程安全,但初始化和销毁之间累积的任何运行时状态应按照通用 Java 指南保留在线程安全结构中(或在简单情况下保留在 volatile 字段中)。
如上所示,更深层的 Lifecycle 集成涉及运行时可变状态,例如 runnable 字段,必须将其声明为 volatile。虽然常见的生命周期回调遵循一定的顺序,例如启动回调保证仅在完全初始化后发生,停止回调仅在初始启动后发生,但这里有一个特殊的案例,即销毁前的常见停止安排:强烈建议任何此类 Bean 中的内部状态也允许在没有先前停止的情况下直接进行销毁回调,因为这可能发生在取消启动后的异常关闭过程中,或者由另一个 Bean 引起的停止超时的情况下。
ApplicationContextAware 和 BeanNameAware
当 ApplicationContext 创建一个实现 org.springframework.context.ApplicationContextAware 接口的对象实例时,该实例将被提供对该 ApplicationContext 的引用。下面的列表显示了 ApplicationContextAware 接口的定义:
public interface ApplicationContextAware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}因此,Bean 可以通过 ApplicationContext 接口或通过将引用强转为该接口的已知子类(例如 ConfigurableApplicationContext,它公开了额外的功能)以编程方式操作创建它们的 ApplicationContext。一种用途是以编程方式检索其他 Bean。有时这种功能很有用。但是,通常情况下,你应该避免使用它,因为这会将代码耦合到 Spring,并且不遵循控制反转风格,在控制反转风格中,协作对象作为属性提供给 Bean。ApplicationContext 的其他方法提供对文件资源的访问、发布应用程序事件以及访问 MessageSource。这些附加功能在 ApplicationContext 的附加能力中进行了描述。
自动装配是获取 ApplicationContext 引用的另一种选择。传统的 constructor 和 byType 自动装配模式(如自动装配协作对象中所述)可以分别为构造函数参数或 Setter 方法参数提供类型为 ApplicationContext 的依赖项。为了获得更多的灵活性,包括自动装配字段和多参数方法的能力,请使用基于注解的自动装配功能。如果这样做,如果相关的字段、构造函数或方法带有 @Autowired 注解,则 ApplicationContext 将被自动装配到期望 ApplicationContext 类型的字段、构造函数参数 or 方法参数中。更多信息请参阅使用 @Autowired。
当 ApplicationContext 创建一个实现 org.springframework.beans.factory.BeanNameAware 接口的类时,该类将被提供对其关联对象定义中定义的名称的引用。下面的列表显示了 BeanNameAware 接口的定义:
public interface BeanNameAware {
void setBeanName(String name) throws BeansException;
}该回调在填充普通 Bean 属性之后但在初始化回调(如 InitializingBean.afterPropertiesSet() 或自定义 init 方法)之前调用。
其他 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 |
再次注意,使用这些接口会将你的代码绑定到 Spring API,并且不遵循控制反转风格。因此,我们建议对需要以编程方式访问容器的基础设施 Bean 使用它们。
补充教学 —— 掌握 Bean 的生命周期全景图
1. Bean 生命周期的“大工程”顺序 这是 Spring 开发者必须刻在脑子里的顺序(简化版):
- 实例化 (Instantiate):
new出一个对象。 - 填充属性 (Populate Properties):注入依赖项。
- Aware 回调:执行
BeanNameAware,BeanFactoryAware,ApplicationContextAware等接口。 - 初始化前 BPP:执行
BeanPostProcessor的postProcessBeforeInitialization。 - 初始化 (Initialize):
- 依次执行:
@PostConstruct->InitializingBean->init-method。
- 依次执行:
- 初始化后 BPP:执行
BeanPostProcessor的postProcessAfterInitialization(AOP 在这里发生)。 - 就绪:Bean 可以被程序使用了。
- 销毁 (Destroy):
- 依次执行:
@PreDestroy->DisposableBean->destroy-method。
- 依次执行:
2. 三种初始化方式,我该选哪个?
- 推荐:
@PostConstruct。它是 Java 标准 (JSR-250),代码最简洁,不依赖 Spring 接口。 - 中性:
init-method(XML/Bean 注解)。当你需要使用第三方库的类,而你又没法修改它的源码去加注解时,用这个。 - 弃用:
InitializingBean。除非你是在写 Spring 框架扩展,否则业务代码里尽量别用。
3. 关于 AOP 的特别警告 文档里提到一个极其重要的点:初始化回调是在原始对象上调用的。 这意味着在 init() 或 afterPropertiesSet() 方法里,如果你尝试调用该 Bean 的另一个被 AOP 拦截(比如有 @Transactional)的方法,事务是不会生效的!因为此时 AOP 代理还没创建完。如果你需要依赖代理后的行为,请使用 SmartInitializingSingleton。
4. 优雅停机 (Graceful Shutdown) 的意义 现在的微服务环境,尤其是部署在 K8s 上的应用,优雅停机非常重要。 注册 Shutdown Hook 确保了在进程退出前,你的 Bean 能把还没写完的缓存刷入磁盘、把还没处理完的 MQ 消息处理掉、把数据库连接正常还给连接池。如果没有它,就像是没存档就直接拔掉游戏机电源。
5. Aware 接口:是药三分毒Aware 接口是一把双刃剑。
- 好处:让你能直接从容器里拿东西,比如
ApplicationContext。 - 坏处:你的类现在死死地绑死在 Spring 上了,没法轻易脱离 Spring 环境做单元测试。
- 建议:只有在开发底层组件时使用。对于普通业务 Bean,通过
@Autowired注入需要的对象(哪怕是注入ApplicationContext)也比实现接口更现代、更解耦。