在Spring Bean 生命周期中加入钩子函数 (翻译)
原文地址:https://reflectoring.io/spring-bean-lifecycle
1 前言
提供一个控制反转功能是 Spring框架的核心功能之一。 Spring 在其应用程序上下文中编排并管理这些beans的生命周期。 在本教程中,我们将研究这些 bean 的生命周期以及如何在其生命周期中加入这些钩子函数。
代码示例
他的文章附有 GitHub 上的工作代码示例。
2 什么是 Spring Bean?
让我们从基础开始。 在创建、编排和销毁方面受 Spring 的 ApplicationContext
控制的每个对象都称为 Spring Bean。
定义 Spring bean 最常见的方法是使用 @Component
注解:
@Component
class MySpringBean {
//...
}
如果启用了 Spring 的自动扫描,则会在应用程序上下文中添加一个 MySpringBean
对象。
另一种方法是使用 Spring 的 配置类结合@Bean
注解进行配置:
@Configuration
class MySpringConfiguration {
@Bean
public MySpringBean mySpringBean() {
return new MySpringBean();
}
}
3 Spring Bean的生命周期
当我们观赛 Spring bean 的生命周期时,我们可以看到从对象实例化到销毁的多个阶段。
为简单起见,我将它们分为创建和销毁两阶段:
接下来让我更详细地解释上图中的这些阶段。
1) Bean 的创建阶段
-
Instantiation(实例化):这是一个Bean一切开始的地方。 Spring 实例化 bean 对象,就像我们手动创建 Java 对象实例一样。
-
Populating Properties(填充属性):在实例化对象后,Spring 会扫描实现 其
Aware
接口,回调Aware
的回调方法, 并开始为其相关属性进行设值。 -
Pre-Initialization (预初始化):Spring Bean后处理器
BeanPostProcessor
在这个阶段开始工作,spring回调初始化前处理方法postProcessBeforeInitialization()
。 此外,@PostConstruct
注解的方法在其之后被调用。 -
AfterPropertiesSet (标准初始化): Spring 执行实现 InitializingBean 接口 的 afterPropertiesSet() 方法。这是正式的初始化。
-
Custom Initialization(自定义初始化):Spring 调用我们用
@Bean
注解的initMethod
属性指定的初始化方法。 -
Post-Initialization (后初始化):Spring Bean后处理器
BeanPostProcessor
在这个阶段再次工作。 此阶段触发其初始化后处理方法postProcessAfterInitialization()
。
2) Bean 的销毁阶段
- Pre-Destroy(预销毁): Spring 在此阶段触发
@PreDestroy
注解标记的方法。 - Destroy(标准销毁):Spring 执行bean实现
DisposableBean
接口的destroy()
方法。 - Custom Destruction(自定义销毁): Spring 在此阶段触发
@Bean
注解的destroyMethod
属性指定的方法。Spring将 在最后一个阶段运行它们。
4 如何在Bean 生命周期中加入钩子函数?
在 Spring 应用程序中,我们有多种方法可以将钩子函数加入到 bean 生命周期的各个阶段。
让我们看看一些例子。
1) 使用spring提供的回调接口
我们可以实现 Spring 的 InitializingBean 接口在标准初始化阶段运行自定义操作:
@Component
class MySpringBean implements InitializingBean {
@Override
public void afterPropertiesSet() {
//...
}
}
同样,我们可以实现 DisposableBean 接口让 Spring 在标准销毁阶段调用 destroy() 方法:
@Component
class MySpringBean implements DisposableBean {
@Override
public void destroy() {
//...
}
}
2) 使用JSR-250注解
Spring官方支持 JSR-250规范中的@PostConstruct
and @PreDestroy
注解。
因此,我们可以使用它们在预初始化和销毁阶段加入钩子函数
@Component
class MySpringBean {
@PostConstruct
public void postConstruct() {
//...
}
@PreDestroy
public void preDestroy() {
//...
}
}
3) 使用@Bean
注解的相关属性
此外,当我们定义 Spring Bean 时,我们可以在 Java 配置类中设置 @Bean
注解的 initMethod
和 destroyMethod
属性:
@Configuration
class MySpringConfiguration {
@Bean(initMethod = "onInitialize", destroyMethod = "onDestroy")
public MySpringBean mySpringBean() {
return new MySpringBean();
}
}
我们应该注意,如果我们的 bean 中有一个名为 close()
或 shutdown()
的公共方法,那么默认情况下它会被当成自定义销毁方法(不用显式指定):
@Component
class MySpringBean {
public void close() {
//...
}
}
但是,如果我们不希望这种行为,我们可以将@Bean
注解的 destroyMethod
属性设为空字符串 (即 destroyMethod=""
),以此来禁用这种默认行为。
@Configuration
class MySpringConfiguration {
@Bean(destroyMethod = "")
public MySpringBean mySpringBean() {
return new MySpringBean();
}
}
XML 配置
对于遗留应用程序,我们可能仍会在 XML 配置中保留一些 bean。 幸运的是,我们仍然可以在 XML Bean 定义中配置这些属性。
4) 使用Bean后处理器 BeanPostProcessor
我们可以利用 BeanPostProcessor
接口在 Spring bean 初始化之前或之后运行任何自定义操作,甚至返回修改后的 bean:
class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
//...
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
//...
return bean;
}
}
BeanPostProcessor
不是针对一个特定的 Bean
我们应该注意,Spring 的BeanPostProcessors
是针对 spring 上下文中定义的所有 bean 。
5)使用 Aware
接口
另一种方法是使用 Aware
接口:
@Component
class MySpringBean implements BeanNameAware, ApplicationContextAware {
@Override
public void setBeanName(String name) {
//...
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
//...
}
}
我们可以使用一些 Aware 接口将 Spring 上下文的某些特性(如BeanFactory、BeanName等)注入到我们的 bean 中。
5 为什么我们需要在Bean 生命周期中加入钩子函数?
当我们需要根据新的需求扩展我们的软件时,对于找到最佳实践来保持我们的代码库的长期可维护性而言至关重要。
在 Spring 框架中,在大多数情况下,添加钩子函数到 bean 生命周期是扩展我们的应用程序的好方法。
1) 获取Bean的相关属性
用法之一是在运行时获取 bean 属性(如 bean 名称)。 例如,当我们需要做一些日志记录时:
@Component
class NamedSpringBean implements BeanNameAware {
Logger logger = LoggerFactory.getLogger(NamedSpringBean.class);
public void setBeanName(String name) {
logger.info(name + " created.");
}
}
2)动态改变 Spring Bean 实例
在某些情况下,我们需要以编程方式定义 Spring bean。 当我们需要在运行时重新创建和更改 bean 实例时,这可能是一个比较实用的解决方案。
接下来让我们创建一个 IpToLocationService
服务实例,该服务实例能够按需动态更新 IpDatabaseRepository
到最新版本:
@Service
class IpToLocationService implements BeanFactoryAware {
DefaultListableBeanFactory listableBeanFactory;
IpDatabaseRepository ipDatabaseRepository;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
listableBeanFactory = (DefaultListableBeanFactory) beanFactory;
updateIpDatabase();
}
public void updateIpDatabase(){
String updateUrl = "https://download.acme.com/ip-database-latest.mdb";
AbstractBeanDefinition definition = BeanDefinitionBuilder
.genericBeanDefinition(IpDatabaseRepository.class)
.addPropertyValue("file", updateUrl)
.getBeanDefinition();
listableBeanFactory
.registerBeanDefinition("ipDatabaseRepository", definition);
ipDatabaseRepository = listableBeanFactory
.getBean(IpDatabaseRepository.class);
}
}
我们可以在 BeanFactoryAware
接口的辅助下访问 到BeanFactory
实例。 因此,我们使用最新的数据库文件动态创建我们的 IpDatabaseRepository
bean,并通过将其注册到 Spring 上下文来更新我们的 bean 定义。
此外,我们在 setBeanFactory()
方法中获取 BeanFactory
实例后立即调用我们的 updateIpDatabase()
方法。 因此,我们可以在 Spring 上下文启动之初就创建 IpDatabaseRepository bean 的一个实例。
3) 从 Spring 上下文的外部访问 Bean
另一种情况是从 Spring 上下文外部访问 ApplicationContext
或 BeanFactory
实例。
例如,我们可能希望将 BeanFactory
注入到非 Spring 容器管理的类中,以便能够访问该类中的 Spring bean 或其他的一些配置。 Spring 和 Quartz 库之间的集成是展示这种用法的一个很好的例子:
class AutowireCapableJobFactory
extends SpringBeanJobFactory implements ApplicationContextAware {
private AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle)
throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}
在这个例子中,我们使用 ApplicationContextAware 接口来访问 BeanFactory
,并使用 BeanFactory
自动装配最初不受 Spring 管理的 Job bean 中的依赖项。
此外,常见的 Spring - Jersey 之间的集成是另一个有代表性的例子:
Configuration
class JerseyConfig extends ResourceConfig {
@Autowired
private ApplicationContext applicationContext;
@PostConstruct
public void registerResources() {
applicationContext.getBeansWithAnnotation(Path.class).values()
.forEach(this::register);
}
}
用Spring @Configuration
注解将 Jersey 的 ResourceConfig
标记为 一个配置类,我们注入 ApplicationContext
实例,并以此实例查找由 Jersey 的 @Path
注解标记的所有 bean,然后应用程序在启动时可注册这些bean。
6 Spring钩子函数的执行顺序
下面我们写一个Spring bean来查看其生命周期各个阶段钩子函数的执行顺序:
class MySpringBean implements BeanNameAware, ApplicationContextAware,
InitializingBean, DisposableBean {
private String message;
public void sendMessage(String message) {
this.message = message;
}
public String getMessage() {
return this.message;
}
@Override
public void setBeanName(String name) {
System.out.println("--- setBeanName executed ---");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
System.out.println("--- setApplicationContext executed ---");
}
@PostConstruct
public void postConstruct() {
System.out.println("--- @PostConstruct executed ---");
}
@Override
public void afterPropertiesSet() {
System.out.println("--- afterPropertiesSet executed ---");
}
public void initMethod() {
System.out.println("--- init-method executed ---");
}
@PreDestroy
public void preDestroy() {
System.out.println("--- @PreDestroy executed ---");
}
@Override
public void destroy() throws Exception {
System.out.println("--- destroy executed ---");
}
public void destroyMethod() {
System.out.println("--- destroy-method executed ---");
}
}
此外,我们还创建了一个 BeanPostProcessor
,它在Bean的初始化之前和之后会被回调:
class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof MySpringBean) {
System.out.println("--- postProcessBeforeInitialization executed ---");
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof MySpringBean) {
System.out.println("--- postProcessAfterInitialization executed ---");
}
return bean;
}
}
接下来,我们编写一个 Spring 配置类来定义我们的 bean:
@Configuration
class MySpringConfiguration {
@Bean
public MyBeanPostProcessor myBeanPostProcessor(){
return new MyBeanPostProcessor();
}
@Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")
public MySpringBean mySpringBean(){
return new MySpringBean();
}
}
最后,我们使用@SpringBootTest
注解来编写一个测试类:
@SpringBootTest
class BeanLifecycleApplicationTests {
@Autowired
public MySpringBean mySpringBean;
@Test
public void testMySpringBeanLifecycle() {
String message = "Hello World";
mySpringBean.sendMessage(message);
assertThat(mySpringBean.getMessage()).isEqualTo(message);
}
}
看我们的最终结果,我们的测试方法记录了Bean生命周期之间钩子函数的执行先后顺序:
--- setBeanName executed ---
--- setApplicationContext executed ---
--- postProcessBeforeInitialization executed ---
--- @PostConstruct executed ---
--- afterPropertiesSet executed ---
--- init-method executed ---
--- postProcessAfterInitialization executed ---
...
--- @PreDestroy executed ---
--- destroy executed ---
--- destroy-method executed ---
7 总结
在本教程中,我们了解了spring bean的各个生命周期、如何以及为什么在Bean 生命周期中加入钩子函数。
Spring bean 生命周期中有多个阶段,以及多个回调的时机。 我们可以通过 bean 上的相关回调注解或实现类型于 BeanPostProcessor
这种公用接口,以此在bean的各个生命周期加入钩子函数。
尽管每个方法都有其特有的用途,但我们应注意如何将这些 Spring 接口与我们自己的代码连接到一起。
另一方面,@PostConstruct
和@PreDestroy
注解是Java API 的一部分(不是spring框架的注解)。这两个注解不与spring框架强相关(在其他的JavaEE框架中也能运行,起作用),没有任何耦合性,(相对于@Bean注解)我们认为它们是Bean生命周期回调的最佳实践。