3.3 Bean生命周期与作用域


3.3 Bean 生命周期与作用域

本节摘要:Bean 从定义到销毁要经过一串固定节点,每个节点都能挂载回调;作用域决定这份 Bean 是全程一份还是每请求一份。两者合起来回答一个问题:请求执行时用到的对象是谁、什么时候生的、能不能装可变状态。

生命周期节点与挂载点

按时间顺序,一个单例 Bean 的一生是:定义注册 → 实例化 → 属性注入 → 感知接口回调 → 前置后处理 → 初始化回调 → 就绪服役 → 容器关闭 → 销毁回调。工程里最常挂代码的是初始化与销毁两处:

@Component public class InventoryCache { private Map<Long, Integer> stock; @PostConstruct // 注入完成后、服役前 public void load() { stock = Map.of(101L, 50, 102L, 30); } @PreDestroy // 容器关闭时 public void flush() { System.out.println("库存缓存写回:" + stock.size() + " 条"); } }

@PostConstruct 的时机常被误解:它在依赖注入完成后执行,所以在里面能安全使用其它 Bean。反过来,把"使用别的 Bean 做初始化"写在构造器里会踩空指针——构造时依赖还没递进来。这是新手排错的高频点,记住顺序即可:先构造、后注入、再初始化。

另一个能感知底层机制的现象:当某个 Bean 需要被代理包裹(比如标注了事务或切面),容器实际注册的是代理对象,生命周期回调发生在代理上,目标是代理内部的成员。这为第 4 章埋下伏笔——代理的介入会带来"自调用失效"的经典问题。

作用域:一份还是每请求一份

默认作用域是单例:整个应用一份,所有请求共享。可选项还有原型(每次索取新建)、请求(每 HTTP 请求一份)、会话(每用户会话一份)等:

@Component @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) public class UserCart { private final List<Long> items = new ArrayList<>(); public void add(Long id) { items.add(id); } public int size() { return items.size(); } }

图 3-3 作用域与请求的对应关系

图 3-3 作用域与请求的对应关系

单例装可变状态的隐患

单例被所有请求共享,意味着它的成员变量是并发现场。经典事故:

@Component public class ReportService { private List<String> lines; // 危险:单例里的可变成员 public String build(Long id) { lines = new ArrayList<>(); lines.add("报表头"); lines.add("数据:" + id); return String.join("\n", lines); // 并发时另一个请求可能已重置 lines } }

两个请求同时进入,lines 互相覆盖,输出错乱且难以复现。修法不是加锁,是把可变状态变成局部变量

public String build(Long id) { var lines = new ArrayList<String>(); // 每次调用私有 lines.add("报表头"); lines.add("数据:" + id); return String.join("\n", lines); }

⚠️ 注入请求级 Bean 到单例必须走代理模式(如上例的 proxyMode),否则启动期就报错或运行期拿到错的对象——短生命周期 Bean 注入长生命周期 Bean,靠的就是这层代理中转。

实验:把生命周期节点按顺序打印出来

背景:文档里那串节点顺序(实例化 → 注入 → 初始化 → 就绪 → 销毁)背了容易忘,亲手打一遍日志就终生难忘。操作:写一个 Bean,把每个阶段能挂的回调全部挂上:

@Component public class LifecycleProbe implements InitializingBean, DisposableBean { private Dependency dep; public LifecycleProbe(Dependency dep) { // 1 实例化+注入 this.dep = dep; System.out.println("1 构造完成,此时依赖已递入"); } @PostConstruct public void postConstruct() { // 2 初始化回调(注解版) System.out.println("2 注解初始化,可安全使用依赖:" + dep.ready()); } @Override public void afterPropertiesSet() { // 3 接口版初始化 System.out.println("3 接口初始化"); } @PreDestroy public void preDestroy() { // 4 销毁前(注解版) System.out.println("4 注解销毁"); } @Override public void destroy() { // 5 接口版销毁 System.out.println("5 接口销毁"); } }

启动后按序输出 1、2、3,正常关停(发关闭信号而非强杀进程)后输出 4、5。解读两点:其一,注解回调先于接口回调,工程里两种别混用,选注解即可;其二,强杀进程时 4、5 都不会执行——销毁回调只在优雅关停下有保证,所以耗时较长的写回操作不能只依赖它,要有启动期自动恢复的兜底。变式:把这个探针注入到控制器里发一次请求,输出顺序不变——证明生命周期与请求无关,装配期就全部完成。

本节要点回顾

  • 先构造、后注入、再初始化:初始化回调里才能安全使用依赖
  • 销毁回调做资源回收:连接、缓存写回都挂这里
  • 作用域即状态策略:可变状态优先局部变量,其次请求级作用域
  • 跨作用域注入要开代理:长持短必须经代理中转

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U