一、面试官:聊一聊Bean的生命周期 ?
觉得有用的话,点赞收藏就是对硬核干货最好的认可~谢谢啦~
我的回答: 额,这个详细的步骤还是挺多的,我之前看过一些源码,我就挑几个主要的步骤说一下,大概流程是这样的。 首先通过一个非常重要的类BeanDefinition获取bean的定义信息,里面封装了bean的所有信息,比如,类的全路径,是否是延迟加载,是否是单例等。
- 第一步在创建bean的时候,就是调用构造函数实例化bean。
- 第二步是bean的依赖注入,比如一些set方法注入,像平时开发用的
@Autowired都是这一步完成。- 第三步是处理Aware接口,如果某一个bean实现了Aware接口(包括BeanNameAware、BeanFactoryAware、ApplicationContextAware)就会重写方法执行。
- 第四步是bean的后置处理器BeanPostProcessor的postProcessBeforeInitialization方法运行,这个是前置增强。
- 第五步是初始化方法执行,比如实现了接口InitializingBean或者自定义了方法init-method标签或@PostContruct。
- 第六步是执行了bean的后置处理器BeanPostProcessor的postProcessAfterInitialization方法运行,这个是后置增强主要是对bean进行增强,有可能在这里产生代理对象。
- 到此,spring容器的bean已经创建完毕了,业务逻辑运行。
- 最后一步是销毁bean,比如自定义的方法destroy-method或@PreDestroy执行。
在 Spring 框架构建的后端世界里,Bean 的生命周期宛如一场精心编排的交响乐,每个音符都精准地落在各自的节拍上,共同演绎出整个应用的稳定与高效。理解 Bean 生命周期,就如同掌握了这场交响乐的总谱,是深入驾驭 Spring 框架的关键密钥。
从面试的角度来看,这是一道绕不开的高频考题。想象一下,面试官抛出 “如何解决 Spring 中的循环依赖?” 或者 “InitializingBean 和 @PostConstruct 的执行顺序是怎样的?” 这类问题时,若你能条理清晰地从 Bean 生命周期的角度阐述,必定能在面试中脱颖而出,展现出扎实的技术功底。
而在生产环境中,Bean 生命周期的重要性更是不言而喻。比如,对于一些资源密集型的 Bean,合理利用其生命周期进行延迟加载,能显著提升系统的启动速度,避免资源的过早占用与浪费。当系统需要关闭时,正确地在 Bean 销毁阶段释放数据库连接、文件句柄等资源,就像演出结束后有序地清理舞台,能有效避免内存泄漏,确保系统的优雅谢幕,为下一次的启动做好充分准备 。
接下来,让我们深入 Spring 的源码世界,结合实战中的具体案例,层层拆解 Bean 生命周期的四大核心阶段,探寻其中的关键扩展点,同时为大家奉上实用的避坑指南,助你在 Spring 开发的道路上一路畅通。
二、Bean 生命周期核心阶段解析
(一)实例化阶段:Bean 的「诞生」时刻
当 Spring 容器启动时,实例化阶段便拉开帷幕,这是 Bean 生命周期的起点,Spring 会依据配置文件或注解所提供的类信息,通过反射机制来创建 Bean 实例。在这个过程中,主要存在两种实例化模式。一种是默认使用无参构造器进行实例化,就像我们创建一个简单的 Java 对象new MyBean()一样,Spring 会调用 Bean 类的无参构造函数来创建对象;另一种则是通过工厂方法或构造器注入的方式来创建实例 ,例如在配置文件中定义一个工厂方法,Spring 会调用该方法来获取 Bean 实例,或者通过构造器注入依赖来创建 Bean 实例。
从核心源码的角度来看,这一过程主要涉及到AbstractAutowireCapableBeanFactory.createBeanInstance()方法。在这个方法里,Spring 会优先解析带有@Autowired注解的构造器。自 Spring4.3 版本起,还支持推断主构造器,这使得依赖注入更加智能和便捷。当一个 Bean 存在多个构造器时,Spring 能够自动推断出合适的构造器进行依赖注入,无需开发者显式指定。
在实例化阶段,还有一个关键动作值得我们关注。对于单例 Bean,在其实例化后,会立即将其暴露至三级缓存(singletonFactories)中。这一操作至关重要,它为后续解决循环依赖问题奠定了基础。当两个或多个 Bean 之间存在相互依赖时,通过三级缓存机制,Spring 能够提前暴露未完全初始化的 Bean 引用,从而打破循环依赖的僵局。
与此同时,Spring 还会触发InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()扩展点。这一扩展点为开发者提供了一个在 Bean 实例化之前进行自定义逻辑处理的机会。比如,我们可以在这个扩展点中根据条件决定是否创建某个 Bean,或者对 Bean 的创建过程进行一些额外的初始化操作 。
(二)依赖注入阶段:填充「成长养分」
完成实例化后,Bean 就进入了依赖注入阶段。在这个阶段,Spring 会通过强大的 DI(Dependency Injection)机制,为 Bean 注入其所需的依赖属性,确保 Bean 在后续的使用中能够正常工作。依赖注入的方式主要有三种:字段注入、Setter 注入和构造器注入。字段注入是通过在字段上使用@Autowired注解,让 Spring 自动将依赖的 Bean 注入到该字段中;Setter 注入则是通过调用 Bean 的 Setter 方法来完成依赖注入;构造器注入则是在 Bean 的构造函数中传入依赖的 Bean。在实际开发中,我们通常推荐使用构造器注入,因为它能够保证依赖的不可变性,使得 Bean 在创建后其依赖关系就已经确定,不会在后续的使用中发生变化 。
关于注入优先级,构造器注入的优先级最高,其次是@Autowired字段注入,最后是 Setter 注入。当一个 Bean 同时存在多种注入方式时,Spring 会按照这个优先级顺序来进行依赖注入。这一规则有助于我们在复杂的依赖关系中,明确依赖注入的顺序,避免出现依赖注入错误。
从核心源码层面分析,依赖注入主要涉及到AbstractAutowireCapableBeanFactory.populateBean()方法。在这个方法中,Spring 会解析@Autowired、@Value等注解,并将依赖对象注入到相应的属性中。同时,它还会处理 XML 配置中的标签属性值,确保 Bean 的属性能够正确赋值。
在依赖注入过程中,循环依赖是一个需要重点关注的问题。对于单例 Bean,Spring 通过巧妙的三级缓存机制来解决 setter 注入循环依赖问题。具体来说,Spring 会在实例化 Bean 后,将其原始对象引用提前暴露到三级缓存中,当其他 Bean 依赖该 Bean 时,就可以从缓存中获取到这个早期暴露的引用,从而避免了循环依赖导致的死锁问题。然而,对于构造器注入循环依赖,Spring 则无法通过缓存机制来解决,这种情况下会抛出BeanCurrentlyInCreationException异常,提示开发者存在循环依赖问题。在实际开发中,我们应该尽量避免构造器注入循环依赖的情况,通过合理的设计来打破循环依赖,例如引入中间层来解耦依赖关系 。
(三)初始化阶段:Bean 的「成人礼」
当依赖注入完成后,Bean 就迎来了初始化阶段。这一阶段是 Bean 生命周期中的重要环节,它标志着 Bean 已经具备了完整的依赖关系,可以开始执行一些初始化逻辑,为后续的使用做好充分准备。在初始化阶段,Spring 提供了三大扩展方式,开发者可以根据实际需求选择合适的方式来进行初始化操作。
首先是 Aware 接口回调,这是一种让 Bean 感知容器相关信息的方式。通过实现BeanNameAware接口,Bean 可以获取到自身在容器中的名称;实现ApplicationContextAware接口,Bean 则可以注入 Spring 容器上下文。例如,当我们需要在 Bean 中获取其他 Bean 实例或者获取容器的配置信息时,就可以通过实现ApplicationContextAware接口来获取ApplicationContext实例,进而调用其方法来实现相应的功能 。
其次是 JSR - 250 标准注解,其中@PostConstruct注解标记的方法会在 Bean 实例化并完成依赖注入后被调用,执行一些初始化逻辑。这个注解的优先级高于 Spring 接口,也就是说,如果一个 Bean 同时实现了InitializingBean接口和使用了@PostConstruct注解,@PostConstruct注解标记的方法会先被执行。
最后是 Spring 专属接口,InitializingBean.afterPropertiesSet()方法会在依赖注入完成后被调用,用于执行初始化操作。此外,我们还可以在 XML 配置或@Bean注解中指定init - method自定义方法,Spring 会在 Bean 初始化时调用这个自定义方法。
从核心源码角度来看,初始化阶段主要由AbstractAutowireCapableBeanFactory.initializeBean()方法来完成。在这个方法中,Spring 会依次调用上述三种扩展方式所对应的方法,完成 Bean 的初始化过程。同时,BeanPostProcessor作为 Spring 的扩展利器,在初始化前后发挥着重要作用。它可以在初始化前后拦截处理 Bean,例如在初始化前生成 AOP 代理,为 Bean 添加事务、日志等切面功能;在初始化后对 Bean 进行一些额外的修饰,如添加事务拦截器,增强 Bean 的功能 。
(四)销毁阶段:Bean 的「优雅退场」
当 Spring 容器关闭时,Bean 就进入了销毁阶段。这一阶段的主要作用是释放 Bean 所占用的资源,如数据库连接、线程池等,确保系统在关闭时能够优雅地释放资源,避免出现资源泄漏等问题。在销毁阶段,Spring 提供了两种配置方式来定义销毁逻辑。
一种是 JSR - 250 标准中的@PreDestroy注解,通过在方法上标记该注解,Spring 会在容器销毁 Bean 之前调用这个方法,执行清理操作。另一种是 Spring 接口,实现DisposableBean.destroy()方法,同样可以在容器销毁 Bean 时被调用。此外,我们还可以在 XML 配置或@Bean注解中指定destroy - method自定义方法,Spring 会在 Bean 销毁时调用这个自定义方法。
需要注意的是,Bean 的销毁阶段存在作用域差异。只有单例 Bean 是由容器统一管理销毁的,当容器关闭时,Spring 会自动调用单例 Bean 的销毁方法;而对于原型 Bean,由于其生命周期不受容器管理,每次获取都是一个新的实例,因此需要使用者手动清理,在不再使用原型 Bean 时,调用其销毁方法来释放资源 。在实际开发中,我们要根据 Bean 的作用域来正确处理销毁逻辑,确保资源的有效管理和释放。
三、实战案例:自定义生命周期控制
(一)注解驱动实现
以一个数据库连接池配置为例,展示注解驱动下 Bean 生命周期的控制。在 Spring Boot 项目中,我们通常会使用@Configuration和@Bean注解来配置 Bean。
首先,创建一个配置类DataSourceConfig,定义一个DataSource的 Bean。假设我们使用的是 HikariCP 连接池:
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/yourdb");
config.setUsername("yourusername");
config.setPassword("yourpassword");
HikariDataSource dataSource = new HikariDataSource(config);
return dataSource;
}
}
在上述代码中,@Bean注解定义了一个DataSource类型的 Bean,initMethod = "init"指定了初始化方法,destroyMethod = "close"指定了销毁方法。当 Spring 容器启动时,会调用HikariDataSource的init方法(如果存在)进行初始化操作,在容器关闭时,会调用close方法释放数据库连接资源。
同时,我们还可以使用@PostConstruct和@PreDestroy注解来实现更细粒度的生命周期控制。例如,在DataSource的包装类中:
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.sql.DataSource;
@Component
public class DataSourceWrapper {
private DataSource dataSource;
public DataSourceWrapper(DataSource dataSource) {
this.dataSource = dataSource;
}
@PostConstruct
public void init() {
// 可以在此处添加额外的初始化逻辑,比如记录日志
System.out.println("DataSourceWrapper initialized");
}
@PreDestroy
public void destroy() {
// 可以在此处添加额外的销毁逻辑,比如关闭一些辅助资源
System.out.println("DataSourceWrapper destroyed");
}
}
这样,在DataSourceWrapper实例化并完成依赖注入后,@PostConstruct注解标记的init方法会被调用;在容器销毁DataSourceWrapper之前,@PreDestroy注解标记的destroy方法会被调用。
(二)接口实现与 XML 配置
接下来,通过接口实现和 XML 配置的方式来控制 Bean 的生命周期。创建一个简单的服务类UserService,实现InitializingBean和DisposableBean接口:
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class UserService implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// 初始化逻辑,比如加载用户权限配置
System.out.println("UserService initialized");
}
@Override
public void destroy() throws Exception {
// 销毁逻辑,比如清理缓存
System.out.println("UserService destroyed");
}
}
然后,在 XML 配置文件applicationContext.xml中配置这个 Bean:
<bean id="userService" class="com.example.demo.service.UserService"/>
当 Spring 容器加载这个配置文件并创建userService Bean 时,会在依赖注入完成后调用afterPropertiesSet方法进行初始化;在容器关闭时,会调用destroy方法执行销毁操作。
此外,我们还可以在 XML 配置中指定自定义的初始化和销毁方法。假设UserService类中有自定义的initMethod和destroyMethod方法:
public class UserService {
public void initMethod() {
System.out.println("Custom init method");
}
public void destroyMethod() {
System.out.println("Custom destroy method");
}
}
在 XML 配置中指定这两个方法:
<bean id="userService" class="com.example.demo.service.UserService"
init-method="initMethod" destroy-method="destroyMethod"/>
这样,Spring 容器在创建和销毁userService Bean 时,会分别调用initMethod和destroyMethod方法 ,实现了对 Bean 生命周期的自定义控制。通过这两个实战案例,我们可以看到在 Spring 中,无论是使用注解驱动还是传统的接口实现与 XML 配置方式,都能够灵活地控制 Bean 的生命周期,满足不同场景下的业务需求。
四、高频问题与避坑指南
(一)初始化顺序之谜
在 Spring 中,@PostConstruct注解标记的方法、InitializingBean.afterPropertiesSet()方法以及自定义的init - method方法的执行顺序常常让人困惑。根据 Spring 官方文档,@PostConstruct方法优先执行,其次是InitializingBean.afterPropertiesSet()方法,最后才是自定义的init - method方法。这是因为@PostConstruct注解是 JSR - 250 标准的一部分,Spring 在处理生命周期时会优先遵循标准规范 。如果在实际开发中不了解这个顺序,可能会导致一些依赖关系的错误,比如在InitializingBean.afterPropertiesSet()方法中依赖了@PostConstruct方法中才初始化的数据,就会出现空指针异常。因此,在编写初始化逻辑时,务必清楚这三者的执行顺序,建议优先使用@PostConstruct注解 (代码更简洁,无 Spring 接口依赖)。
(二)循环依赖三大限制
- 仅支持单例 Bean 的 setter / 字段注入,构造器注入无法解决:Spring 的三级缓存机制是解决循环依赖的关键,但它仅适用于单例 Bean 的 setter / 字段注入。对于构造器注入,由于在实例化 Bean 时就需要完全确定其依赖关系,无法提前暴露未完全初始化的 Bean 引用,所以无法解决循环依赖问题。例如,当两个 Bean 通过构造器相互依赖时,Spring 在创建其中一个 Bean 时,会因为无法获取到另一个 Bean 的实例而陷入死循环,最终抛出BeanCurrentlyInCreationException异常。
- 原型 Bean 因不缓存实例,无法处理循环依赖:原型 Bean 的作用域决定了每次从容器中获取都是一个新的实例,Spring 不会对原型 Bean 进行缓存。这就导致在处理循环依赖时,无法利用缓存机制来提前暴露 Bean 引用,从而无法解决循环依赖问题。比如,当两个原型 Bean 相互依赖时,每次获取其中一个 Bean 都会尝试创建新的实例,而新实例又依赖另一个 Bean,如此循环往复,最终导致内存溢出。
- 三级缓存机制可能导致 AOP 代理提前暴露,引发代理对象不完整问题:在解决循环依赖的过程中,由于需要提前暴露 Bean 的引用,可能会导致 AOP 代理提前暴露。如果此时 AOP 代理的初始化还未完全完成,就会出现代理对象不完整的问题。例如,在使用@Transactional注解开启事务时,如果 AOP 代理提前暴露,可能会导致事务切面功能无法正常生效,从而引发数据一致性问题。在实际开发中,需要注意这个问题,尽量避免在循环依赖中使用 AOP 代理 ,或者在配置 AOP 时,确保代理对象的初始化完整。
(三)销毁方法失效排查
- 确保容器通过 ConfigurableApplicationContext.close () 正常关闭:Spring 容器的关闭方式直接影响到 Bean 的销毁过程。如果容器没有正常关闭,例如在 Web 应用中没有正确配置 Servlet 容器的关闭钩子,或者在独立应用中没有调用ConfigurableApplicationContext.close()方法,那么 Bean 的销毁方法将不会被执行。这可能会导致资源无法及时释放,如数据库连接未关闭,从而引发资源泄漏问题。因此,在开发过程中,务必确保容器能够正常关闭,在 Web 应用中,可以通过配置 Servlet 容器的关闭钩子来确保容器关闭时调用 Spring 容器的关闭方法;在独立应用中,要在程序结束时显式调用ConfigurableApplicationContext.close()方法 。
- 检查作用域是否为单例(原型 Bean 无销毁回调) :只有单例 Bean 是由 Spring 容器统一管理销毁的,原型 Bean 的生命周期不受容器控制,每次获取都是一个新的实例,容器不会在关闭时调用其销毁方法。如果误将原型 Bean 配置为有销毁逻辑,而在容器关闭时发现销毁方法未执行,就需要检查 Bean 的作用域是否配置正确。例如,在配置文件中,将一个需要频繁创建和销毁的 Bean 配置为单例,同时又在其类中定义了销毁方法,期望在容器关闭时执行销毁操作,这是不合理的。对于原型 Bean,需要在使用完后手动调用其销毁方法来释放资源 。
- 避免在销毁方法中调用容器内其他 Bean(可能已销毁) :在 Bean 的销毁方法中,如果调用了容器内其他 Bean,而这些 Bean 可能已经在之前被销毁,就会导致空指针异常或其他错误。比如,在一个 Bean 的destroy方法中,调用了另一个 Bean 的方法来进行资源清理,但此时另一个 Bean 已经被销毁,就会出现调用失败的情况。因此,在编写销毁方法时,要避免调用容器内其他 Bean,尽量只在销毁方法中进行本 Bean 自身资源的清理工作 ,如关闭文件句柄、释放线程资源等。
五、总结:掌握生命周期的核心价值
理解 Bean 生命周期,本质是理解 Spring 容器的对象管理哲学:从实例化的「控制反转」到销毁时的「资源托管」,每个阶段都提供了强大的扩展能力。通过合理使用@PostConstruct、@PreDestroy等工具,开发者能精准控制 Bean 的行为,避免依赖注入时机错误、资源泄漏等问题,让 Spring 框架的威力真正为业务赋能。:当 Bean 生命周期遇上 Web 请求(如 Request 作用域),又会产生哪些特殊行为?欢迎在评论区讨论你的实战经验!