本节摘要: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(); } }

单例被所有请求共享,意味着它的成员变量是并发现场。经典事故:
@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 都不会执行——销毁回调只在优雅关停下有保证,所以耗时较长的写回操作不能只依赖它,要有启动期自动恢复的兜底。变式:把这个探针注入到控制器里发一次请求,输出顺序不变——证明生命周期与请求无关,装配期就全部完成。