自定义 Bean 的本质

Spring框架提供了一些接口,您可以使用这些接口来自定义Bean的特性。本节将它们分组如下

生命周期回调

要与容器对Bean生命周期的管理进行交互,您可以实现Spring的InitializingBeanDisposableBean接口。容器会分别调用前者的afterPropertiesSet()和后者的destroy(),让Bean在初始化和销毁时执行某些操作。

JSR-250的@PostConstruct@PreDestroy注解通常被认为是在现代Spring应用程序中接收生命周期回调的最佳实践。使用这些注解意味着您的Bean不会耦合到特定于Spring的接口。有关详细信息,请参阅使用@PostConstruct@PreDestroy

如果您不想使用JSR-250注解,但仍然希望消除耦合,请考虑init-methoddestroy-methodBean定义元数据。

在内部,Spring框架使用BeanPostProcessor实现来处理它可以找到的任何回调接口并调用相应的方法。如果您需要Spring默认不提供的自定义功能或其他生命周期行为,您可以自己实现一个BeanPostProcessor。有关更多信息,请参阅容器扩展点

除了初始化和销毁回调之外,Spring管理的对象还可以实现Lifecycle接口,以便这些对象可以参与启动和关闭过程,这由容器自己的生命周期驱动。

本节描述了生命周期回调接口。

初始化回调

org.springframework.beans.factory.InitializingBean接口允许Bean在容器设置Bean上所有必要的属性后执行初始化工作。InitializingBean接口指定了一个方法

void afterPropertiesSet() throws Exception;

我们建议您不要使用InitializingBean接口,因为它会不必要地将代码耦合到Spring。或者,我们建议使用@PostConstruct注解或指定一个POJO初始化方法。在基于XML的配置元数据的情况下,您可以使用init-method属性来指定具有void无参数签名的方法的名称。使用Java配置,您可以使用@BeaninitMethod属性。请参阅接收生命周期回调。考虑以下示例

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
  • Java

  • Kotlin

public class ExampleBean {

	public void init() {
		// do some initialization work
	}
}
class ExampleBean {

	fun init() {
		// do some initialization work
	}
}

前面的示例与以下示例(包含两个列表)的效果几乎完全相同

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
  • Java

  • Kotlin

public class AnotherExampleBean implements InitializingBean {

	@Override
	public void afterPropertiesSet() {
		// do some initialization work
	}
}
class AnotherExampleBean : InitializingBean {

	override fun afterPropertiesSet() {
		// do some initialization work
	}
}

但是,前两个示例中的第一个示例不会将代码耦合到Spring。

请注意,@PostConstruct和初始化方法通常在容器的单例创建锁内执行。只有在从@PostConstruct方法返回后,才会将Bean实例视为完全初始化并准备发布给其他实例。此类单独的初始化方法仅用于验证配置状态,并可能基于给定配置准备一些数据结构,但不进行其他外部Bean访问活动。否则,存在初始化死锁的风险。

对于需要触发昂贵的初始化后活动的场景,例如异步数据库准备步骤,您的Bean应该实现SmartInitializingSingleton.afterSingletonsInstantiated()或依赖于上下文刷新事件:实现ApplicationListener<ContextRefreshedEvent>或声明其注解等效项@EventListener(ContextRefreshedEvent.class)。这些变体在所有常规单例初始化之后出现,因此在任何单例创建锁之外。

或者,您可以实现(Smart)Lifecycle接口并与容器的整体生命周期管理集成,包括自动启动机制、预销毁停止步骤和潜在的停止/重新启动回调(见下文)。

销毁回调

实现org.springframework.beans.factory.DisposableBean接口允许Bean在包含它的容器被销毁时获得回调。DisposableBean接口指定了一个方法

void destroy() throws Exception;

我们建议您不要使用DisposableBean回调接口,因为它会不必要地将代码耦合到Spring。或者,我们建议使用@PreDestroy注解或指定Bean定义支持的通用方法。使用基于XML的配置元数据,您可以在<bean/>上使用destroy-method属性。使用Java配置,您可以使用@BeandestroyMethod属性。请参阅接收生命周期回调。考虑以下定义

<bean id="exampleDestructionBean" class="examples.ExampleBean" destroy-method="cleanup"/>
  • Java

  • Kotlin

public class ExampleBean {

	public void cleanup() {
		// do some destruction work (like releasing pooled connections)
	}
}
class ExampleBean {

	fun cleanup() {
		// do some destruction work (like releasing pooled connections)
	}
}

前面的定义与以下定义的效果几乎完全相同

<bean id="exampleDestructionBean" class="examples.AnotherExampleBean"/>
  • Java

  • Kotlin

public class AnotherExampleBean implements DisposableBean {

	@Override
	public void destroy() {
		// do some destruction work (like releasing pooled connections)
	}
}
class AnotherExampleBean : DisposableBean {

	override fun destroy() {
		// do some destruction work (like releasing pooled connections)
	}
}

但是,前两个定义中的第一个定义不会将代码耦合到Spring。

请注意,Spring还支持销毁方法的推断,检测公共的closeshutdown方法。这是Java配置类中@Bean方法的默认行为,并自动匹配java.lang.AutoCloseablejava.io.Closeable实现,也不会将销毁逻辑耦合到Spring。

对于使用XML的销毁方法推断,您可以为<bean>元素的destroy-method属性分配一个特殊的(inferred)值,这指示Spring自动检测Bean类上的公共closeshutdown方法以用于特定的Bean定义。您还可以将此特殊的(inferred)值设置为<beans>元素的default-destroy-method属性,以将此行为应用于一整套Bean定义(请参阅默认初始化和销毁方法)。

对于扩展的关闭阶段,您可以实现Lifecycle接口并在调用任何单例Bean的销毁方法之前接收早期停止信号。您还可以实现SmartLifecycle以进行时间限制的停止步骤,在该步骤中,容器将在继续执行销毁方法之前等待所有此类停止处理完成。

默认初始化和销毁方法

当您编写不使用特定于Spring的InitializingBeanDisposableBean回调接口的初始化和销毁方法回调时,您通常会编写名称为init()initialize()dispose()等等的方法。理想情况下,此类生命周期回调方法的名称在一个项目中是标准化的,以便所有开发人员都使用相同的名称并确保一致性。

您可以配置Spring容器以“查找”每个Bean上命名的初始化和销毁回调方法名称。这意味着,作为应用程序开发人员,您可以编写应用程序类并使用名为init()的初始化回调,而无需为每个Bean定义配置init-method="init"属性。Spring IoC容器在创建Bean时(并根据标准生命周期回调契约前面所述)调用该方法。此功能还为初始化和销毁方法回调强制执行一致的命名约定。

假设您的初始化回调方法名为init(),而您的销毁回调方法名为destroy()。然后您的类类似于以下示例中的类

  • Java

  • Kotlin

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.");
		}
	}
}
class DefaultBlogService : BlogService {

	private var blogDao: BlogDao? = null

	// this is (unsurprisingly) the initialization callback method
	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类具有此类方法,则会在适当的时间调用它。

您可以类似地配置销毁方法回调(在 XML 中),方法是在顶级 <beans/> 元素上使用 default-destroy-method 属性。

如果现有的 Bean 类已经具有与约定不符的名称的回调方法,则可以通过(在 XML 中)使用 <bean/> 本身的 init-methoddestroy-method 属性来指定方法名称,从而覆盖默认值。

Spring 容器保证在 Bean 获得所有依赖项后立即调用已配置的初始化回调。因此,初始化回调是在原始 Bean 引用上调用的,这意味着 AOP 拦截器等尚未应用于 Bean。目标 Bean 首先完全创建,然后应用 AOP 代理(例如)及其拦截器链。如果目标 Bean 和代理分别定义,则您的代码甚至可以与原始目标 Bean 交互,绕过代理。因此,将拦截器应用于 init 方法是不一致的,因为这样做会将目标 Bean 的生命周期与其代理或拦截器耦合,并在您的代码直接与原始目标 Bean 交互时留下奇怪的语义。

组合生命周期机制

从 Spring 2.5 开始,您有三种选择来控制 Bean 生命周期行为

如果为一个 Bean 配置了多个生命周期机制,并且每个机制都配置了不同的方法名称,则每个配置的方法都将按此注释之后列出的顺序运行。但是,如果为这些生命周期机制中的多个机制配置了相同的方法名称(例如,初始化方法的 init()),则该方法将运行一次,如上一节所述。

为同一 Bean 配置的多个生命周期机制(具有不同的初始化方法)将按如下方式调用

  1. 使用 @PostConstruct 注解的方法

  2. InitializingBean 回调接口定义的 afterPropertiesSet()

  3. 自定义配置的 init() 方法

销毁方法按相同的顺序调用

  1. 使用 @PreDestroy 注解的方法

  2. DisposableBean 回调接口定义的 destroy()

  3. 自定义配置的 destroy() 方法

启动和关闭回调

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);
}

启动时,阶段最低的对象首先启动。停止时,遵循相反的顺序。因此,实现 SmartLifecycle 且其 getPhase() 方法返回 Integer.MIN_VALUE 的对象将是最先启动和最后停止的对象。在频谱的另一端,阶段值为 Integer.MAX_VALUE 表示该对象应最后启动并首先停止(可能是因为它依赖于其他进程正在运行)。在考虑阶段值时,还必须知道任何不实现 SmartLifecycle 的“正常”Lifecycle 对象的默认阶段为 0。因此,任何负阶段值都表示对象应在这些标准组件之前启动(并在其之后停止)。任何正阶段值都是相反的。

SmartLifecycle 定义的 stop 方法接受回调。任何实现都必须在该实现的关闭过程完成后调用该回调的 run() 方法。这使得在必要时能够异步关闭,因为 LifecycleProcessor 接口的默认实现 DefaultLifecycleProcessor 会等待每个阶段中的一组对象调用该回调,最多等待其超时值。每个阶段的默认超时时间为 30 秒。您可以通过在上下文中定义名为 lifecycleProcessor 的 Bean 来覆盖默认的生命周期处理器实例。如果您只想修改超时时间,则定义以下内容就足够了

<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
	<!-- timeout value in milliseconds -->
	<property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

如前所述,LifecycleProcessor 接口为上下文的刷新和关闭定义了回调方法。后者驱动关闭过程,就像显式调用了 stop() 一样,但它发生在上下文关闭时。“刷新”回调另一方面,启用了 SmartLifecycle Bean 的另一个功能。当上下文刷新时(在所有对象实例化和初始化之后),将调用该回调。此时,默认的生命周期处理器会检查每个 SmartLifecycle 对象的 isAutoStartup() 方法返回的布尔值。如果为 true,则该对象将在此时启动,而不是等待显式调用上下文或其自身的 start() 方法(与上下文刷新不同,对于标准上下文实现,上下文启动不会自动发生)。“阶段”值和任何“depends-on”关系决定了如前所述的启动顺序。

在非 Web 应用程序中优雅地关闭 Spring IoC 容器

本节仅适用于非 Web 应用程序。Spring 的基于 Web 的 ApplicationContext 实现已经有了代码来在相关 Web 应用程序关闭时优雅地关闭 Spring IoC 容器。

如果您在非 Web 应用程序环境(例如,在富客户端桌面环境中)中使用 Spring 的 IoC 容器,请向 JVM 注册一个关闭钩子。这样做可确保优雅关闭并调用单例 Bean 上的相关销毁方法,以便释放所有资源。您仍然必须正确配置和实现这些销毁回调。

要注册关闭钩子,请调用 ConfigurableApplicationContext 接口上声明的 registerShutdownHook() 方法,如下面的示例所示

  • Java

  • Kotlin

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...
	}
}
import org.springframework.context.support.ClassPathXmlApplicationContext

fun main() {
	val ctx = 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...
}

线程安全性和可见性

Spring 核心容器以线程安全的方式发布创建的单例实例,通过单例锁保护访问并保证在其他线程中的可见性。

因此,应用程序提供的 Bean 类无需关心其初始化状态的可见性。常规配置字段无需标记为 volatile,只要它们仅在初始化阶段发生变异,即可提供类似于 final 的可见性保证,即使对于在该初始阶段可变的基于 setter 的配置状态也是如此。如果此类字段在 Bean 创建阶段及其随后的初始发布之后发生更改,则在访问时需要将其声明为 volatile 或由公共锁保护。

请注意,对单例 Bean 实例中此类配置状态的并发访问(例如,对于控制器实例或存储库实例)在容器端进行此类安全初始发布后是完全线程安全的。这包括在通用单例锁中处理的通用单例 FactoryBean 实例。

对于销毁回调,配置状态保持线程安全,但初始化和销毁之间累积的任何运行时状态应根据常见的 Java 指南保存在线程安全结构(或在简单情况下保存在 volatile 字段中)。

如上所示的更深入的 Lifecycle 集成涉及运行时可变状态,例如 runnable 字段,该字段必须声明为 volatile。虽然常见的生命周期回调遵循一定的顺序,例如,保证启动回调仅在完全初始化之后发生,停止回调仅在初始启动之后发生,但常见的停止前销毁安排有一个特殊情况:强烈建议任何此类 Bean 中的内部状态也允许立即进行销毁回调,而无需先进行停止,因为这可能会在取消引导后非正常关闭期间发生,或者在由另一个 Bean 引起的停止超时的情况下发生。

ApplicationContextAwareBeanNameAware

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 引用的另一种方法。传统构造函数byType自动装配模式(如自动装配协作者中所述)可以分别为构造函数参数或 setter 方法参数提供类型为 ApplicationContext 的依赖项。为了获得更大的灵活性,包括能够自动装配字段和多参数方法,请使用基于注解的自动装配功能。如果这样做,如果相关的字段、构造函数或方法带有 @Autowired 注解,则 ApplicationContext 会自动装配到期望 ApplicationContext 类型的字段、构造函数参数或方法参数中。有关更多信息,请参阅使用 @Autowired

ApplicationContext 创建一个实现了 org.springframework.beans.factory.BeanNameAware 接口的类时,该类会获得与其关联的对象定义中定义的名称的引用。以下清单显示了 BeanNameAware 接口的定义。

public interface BeanNameAware {

	void setBeanName(String name) throws BeansException;
}

回调是在填充普通 bean 属性之后但初始化回调(例如 InitializingBean.afterPropertiesSet() 或自定义 init 方法)之前调用的。

其他 Aware 接口

除了 ApplicationContextAwareBeanNameAware(在前面讨论过)之外,Spring 还提供了一系列 Aware 回调接口,使 bean 能够向容器表明它们需要某种基础设施依赖项。通常情况下,名称指示依赖项类型。下表总结了最重要的 Aware 接口。

表 1. Aware 接口
名称 注入的依赖项 解释在…​

ApplicationContextAware

声明 ApplicationContext

ApplicationContextAwareBeanNameAware

ApplicationEventPublisherAware

封闭 ApplicationContext 的事件发布者。

ApplicationContext 的其他功能

BeanClassLoaderAware

用于加载 bean 类的类加载器。

实例化 Bean

BeanFactoryAware

声明 BeanFactory

BeanFactory API

BeanNameAware

声明 bean 的名称。

ApplicationContextAwareBeanNameAware

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。