解释 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,还是自定义扩展,都能更得心应手。

相关推荐
苏三说技术10 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎11 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode11 小时前
Redis 在生产项目的使用
前端·后端
用户5598224812211 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode11 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战11 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha11 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn11 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户7623524259111 小时前
ShardingJDBC
后端
行者全栈架构师11 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端