解释 Spring 框架中 bean 的生命周期:一个八年 Java 开发的实战视角

解释 Spring 框架中 bean 的生命周期:一个八年 Java 开发的实战视角

刚接触 Spring 那会,我总觉得 bean 的生命周期是块 "看着懂、用着懵" 的软骨头 ------ 流程图背得滚瓜烂熟,一到项目里就踩坑:比如 @PreDestroy 写了没执行,初始化方法里拿不到注入的属性,甚至线上因为 bean 创建顺序导致空指针。直到这八年里调过 N 次类似的 bug、自定义过 BeanPostProcessor、排查过循环依赖引发的初始化异常,才真正把这事儿嚼透:Spring 的 bean 生命周期,本质是 "从创建到销毁" 的标准化流程,每一步都藏着 "解耦扩展" 和 "避坑指南"

今天不聊干巴巴的理论,就从实战角度拆解 bean 的生命周期 ------ 每个阶段讲清楚 "做什么""实际坑点""怎么用",再补上我踩过的真实案例,帮你少走我当年的弯路。

先搞懂核心:为什么需要 "生命周期"?

在没 Spring 之前,我们写 Java 是new Object()直接创建对象,初始化、赋值、销毁全靠自己写代码。但项目大了就乱了:比如 100 个 bean 都要初始化数据库连接,总不能每个 bean 里都写一遍吧?某个 bean 销毁时要释放资源,万一忘了调用怎么办?

Spring 的 bean 生命周期,本质就是把 "对象创建 - 初始化 - 使用 - 销毁" 的过程标准化,并且留出扩展点------ 你不用关心 bean 怎么被创建,只需要告诉 Spring:"我要在初始化时做 XX""销毁时要清 XX 资源",剩下的交给框架。

简单说,生命周期的核心价值是:解耦创建逻辑、统一扩展入口、避免资源泄漏

拆解 bean 生命周期:5 个阶段 + 实战坑点

Spring bean 的生命周期,从 "被 Spring 容器管理" 到 "被销毁",可以分为 5 个核心阶段。我画了张简化流程图(脑子里的):实例化(new对象)→ 属性注入(给字段赋值)→ 初始化(执行自定义逻辑)→ 使用(业务调用)→ 销毁(释放资源)

每个阶段都有细节和坑,咱们逐个拆:

1. 实例化(Instantiation):先有 "壳",再填 "肉"

做什么 :Spring 根据 bean 的定义(比如 @Component、@Bean),调用类的构造器创建对象 ------ 这一步相当于你自己写new UserService(),但由 Spring 帮你执行。

八年开发实战点

  • 构造器注入的 "顺序坑":如果你的 bean 用构造器注入依赖(比如public UserService(OrderService orderService)),Spring 会先去创建依赖的OrderService,再创建UserService。这里要注意:构造器里别写复杂逻辑 !我早年踩过坑:在构造器里调用orderService.query(),但此时OrderService可能还没完成初始化,直接空指针。
  • 多构造器的 "选择坑":如果类有多个构造器,Spring 默认选无参构造;如果想指定有参构造,要么用@Autowired标在构造器上,要么确保只有一个有参构造(Spring 会自动用)。别同时给多个构造器加@Autowired,否则 Spring 会懵,直接抛异常。
  • 循环依赖的 "避坑点":构造器注入最容易触发循环依赖(比如 A 依赖 B,B 依赖 A),Spring 解决不了这种情况,会直接报错。此时建议改用 setter 注入或字段注入 ------ 不是说构造器注入不好,而是要根据场景选,核心服务用构造器注入(强制依赖),非核心用字段注入(可选依赖)。

2. 属性注入(Populate):给 "壳" 填 "肉"

做什么 :实例化后,Spring 会给 bean 的属性赋值 ------ 比如给加了@Autowired@Resource的字段赋值,或者调用 setter 方法注入依赖。

八年开发实战点

  • @Autowired vs @Resource:别再搞混了!@Autowired是 Spring 注解,按 "类型" 注入;@Resource是 JSR 规范注解,按 "名称" 注入(找不到再按类型)。实战中,如果一个接口有多个实现类(比如PaymentServiceAlipayServiceWechatPayService),用@Resource(name = "alipayService")@Autowired + @Qualifier更简洁。

  • 注入的 "时机坑":属性注入是在实例化之后、初始化之前。所以千万别在构造器里用注入的属性!比如:

    java 复制代码
    @Service
    public class UserService {
        @Autowired
        private OrderService orderService;
    
        // 错误:此时orderService还没注入,是null
        public UserService() {
            orderService.query(); 
        }
    }

    解决办法:要么把依赖放进构造器注入,要么把逻辑移到初始化方法里。

  • 字段注入的 "测试坑":很多人喜欢用字段注入(方便),但写单元测试时会麻烦 ------ 因为字段是 private 的,没法直接 mock。八年经验是:核心服务用构造器注入(利于测试),工具类用字段注入(省事),平衡效率和可测试性。

3. 初始化(Initialization):最容易踩坑的 "扩展阶段"

实例化 + 属性注入后,bean 还不能直接用 ------ 比如要初始化数据库连接、加载配置文件、初始化缓存。Spring 把这个阶段拆成 3 步,还留出了扩展点,是整个生命周期的核心。

流程顺序(重点记!):BeanPostProcessor前置处理 → 自定义初始化逻辑 → BeanPostProcessor后置处理

(1)BeanPostProcessor 前置处理:全局 "预处理"

做什么 :Spring 会遍历所有BeanPostProcessor,调用postProcessBeforeInitialization方法 ------ 这是个 "全局钩子",可以对所有 bean 做统一处理。

实战场景

  • 我早年做过一个需求:给所有加了@Loggable注解的 bean,在初始化前自动生成日志代理。就是通过自定义BeanPostProcessor,在前置处理里判断 bean 是否有注解,有就生成代理对象。
  • 注意:BeanPostProcessor是 "全局生效" 的,如果你只想要某个 bean 生效,记得在方法里加判断(比如if (bean instanceof UserService)),别影响其他 bean。
(2)自定义初始化逻辑:3 种方式 + 优先级

这是我们最常用的部分,Spring 提供了 3 种方式,优先级一定要记清(踩过坑的都懂):

  1. @PostConstruct(JSR 规范注解)
  2. InitializingBean接口(Spring 原生接口)
  3. init-method(XML 或 @Bean 的属性)

八年开发经验

  • 优先用@PostConstruct:它是 JSR 规范,不依赖 Spring,后续换成其他框架(比如 Jakarta EE)也不用改代码。而InitializingBean是接口,会让你的 bean 和 Spring 耦合,不推荐。
  • 初始化逻辑别太复杂:比如别在@PostConstruct里调用远程接口(万一网络不通,bean 创建失败,整个容器启动不了)。如果有复杂逻辑,建议异步执行(比如用@Async)。
  • 真实坑点:我曾遇到过 "初始化时拿不到注入的属性"------ 排查发现,属性用了@Lazy(延迟注入),而@PostConstruct执行时,延迟注入的 bean 还没创建。解决办法:要么去掉@Lazy,要么在使用时再初始化。
(3)BeanPostProcessor 后置处理:AOP 的 "关键一步"

做什么 :调用postProcessAfterInitialization方法 ------ 这是 Spring 实现 AOP 的核心步骤!比如你给 bean 加了@Transactional,Spring 会在这里生成代理对象,替换原来的 bean。

实战注意

  • 代理对象的 "坑":如果你的 bean 被 AOP 代理了,那么this关键字调用的方法,不会触发切面(因为this是原始对象,不是代理对象)。比如:

    typescript 复制代码
    @Service
    public class UserService {
        @Transactional
        public void addUser() {
            // 错误:this调用不会触发事务
            this.updateUser(); 
        }
    
        @Transactional
        public void updateUser() {}
    }

    解决办法:要么用ApplicationContext获取代理对象,要么用@Autowired自己注入自己(虽然看起来怪,但实战中常用)。

  • BeanPostProcessor的执行顺序:如果有多个BeanPostProcessor,用@Order注解指定顺序(数字越小越先执行)。比如 Spring 自带的AutowiredAnnotationBeanPostProcessor(处理@Autowired)和你自定义的,要确保顺序正确,否则会导致注入失败。

4. 使用阶段:别忽略 "作用域" 的影响

初始化完成后,bean 就可以被业务代码使用了(比如@Autowired注入后调用方法)。但这里有个容易被忽略的点:bean 的作用域会影响生命周期

八年开发常用的 2 种作用域:

  • 单例(singleton):默认作用域,容器启动时创建(除非加了@Lazy),整个容器里只有一个实例,销毁时随容器关闭。
  • 多例(prototype):每次getBean(或注入)时创建新实例,Spring 不管理多例 bean 的销毁

多例的 "销毁坑" :我曾在多例 bean 里写了@PreDestroy,想在销毁时释放数据库连接,结果发现完全不执行。后来才明白:多例 bean 创建后,Spring 就不管了,销毁得自己处理(比如用BeanFactory手动管理,或在业务代码里调用销毁方法)。所以实战中,除非明确需要多例(比如每个请求需要独立状态),否则尽量用单例(性能好,Spring 能管理生命周期)。

5. 销毁阶段:别让 "资源泄漏" 找上门

当容器关闭时(比如 Web 项目停 Tomcat、命令行项目调用ApplicationContext.close()),单例 bean 会进入销毁阶段。流程顺序和初始化对应:@PreDestroy(JSR 规范) → DisposableBean接口 → destroy-method(XML 或 @Bean 属性)

实战避坑点

  • 命令行项目要手动 close:如果是写小工具(比如数据导出脚本),用ClassPathXmlApplicationContextAnnotationConfigApplicationContext,一定要在最后调用close()方法,否则@PreDestroy不执行,资源(比如数据库连接池、线程池)会泄漏。
  • 销毁逻辑别抛异常:如果@PreDestroy里抛了未捕获异常,会导致其他 bean 的销毁逻辑被中断。所以销毁时要捕获异常,或者用try-finally确保资源释放。
  • 真实案例:早年线上有个服务,停服后数据库连接还没释放,导致连接池满了。排查发现是@PreDestroy里调用connection.close()时抛了 NullPointerException(连接对象为 null),没处理异常,导致后续的连接释放逻辑没执行。解决办法:加 null 判断和异常捕获,确保close()方法一定被调用。

八年开发总结:3 个 "少踩坑" 原则

讲完生命周期的细节,再给大家分享 3 个我实战中总结的原则,帮你少走弯路:

1. 优先用 "标准注解",减少 Spring 耦合

比如用@PostConstruct/@PreDestroy代替InitializingBean/DisposableBean,用@Autowired/@Resource代替 XML 配置。这样你的代码不仅能在 Spring 里跑,换成其他框架(比如 Quarkus)也不用大改,灵活性更高。

2. 别在 "构造器" 和 "初始化" 里写复杂逻辑

  • 构造器:只做 "简单赋值",别调用依赖的 bean 方法(此时属性还没注入),别远程调用(万一失败,容器启动不了)。
  • 初始化:复杂逻辑(比如加载大数据、远程调用)尽量异步执行,或者放在单独的方法里,由业务触发(比如initCache(),在第一次调用时初始化)。

3. 自定义扩展时,"理解再动手"

比如自定义BeanPostProcessorBeanFactoryPostProcessor时,先搞懂它们的执行时机,别随便改 bean 的状态。我早年曾自定义BeanPostProcessor,在前置处理里修改了 bean 的属性,导致后续注入的依赖被覆盖,排查了半天才找到原因。记住:Spring 的扩展点很强大,但也很容易 "踩雷",理解原理再用。

最后:生命周期的本质是 "可控"

现在再看 Spring bean 的生命周期,我已经不把它当 "流程图" 了 ------ 而是把它看作 Spring 的 "设计思路":通过标准化流程,让 bean 的创建、初始化、销毁都 "可控",同时留出扩展点,让我们能按需定制。

其实不光是 bean 的生命周期,Spring 的很多特性(比如 AOP、事务管理),本质都是在 "解耦" 和 "可控" 之间找平衡。理解了这一点,不管是排查 bug,还是自定义扩展,都能更得心应手。

相关推荐
尤老师FPGA4 小时前
LVDS系列32:Xilinx 7系 ADC LVDS接口参考设计(三)
android·java·ui
自由的疯4 小时前
Java 如何学习 Jenkins?
java·架构
自由的疯4 小时前
Java ‌认识Docker
java·架构
Forfun_tt4 小时前
xss-labs pass-10
java·前端·xss
往事随风去5 小时前
那个让老板闭嘴、让性能翻倍的“黑科技”:基准测试最全指南
后端·测试
又是忙碌的一天5 小时前
java基础 -----底层
java·基础
李广坤5 小时前
JAVA线程池详解
后端
调试人生的显微镜5 小时前
深入剖析 iOS 26 系统流畅度,多工具协同监控与性能优化实践
后端
蹦跑的蜗牛5 小时前
Spring Boot使用Redis实现消息队列
spring boot·redis·后端