qinghuan
← Java Web / 全部笔记

单元 65 / Java Web

P1 · 重点提升L2 · 进阶

Spring Bean 何时准备好:初始化与代理调用

跟踪对象从构造、初始化到交给调用者的过程,理解何时能使用依赖,以及哪些调用会经过 AOP 代理。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能沿对象生命周期标记资源准备、代理暴露与业务增强真正发生的位置。

学习理由、准备要求与知识点

为什么学它

理解目标对象、初始化和代理时机,可以解释事务与增强在某些调用中为何失效。

先读这些内容

知识点清单

  • 实例化与依赖注入
  • 初始化回调
  • BeanPostProcessor
  • 目标对象与代理
  • 切点和通知
  • 初始化内部调用限制
  • 单例与原型销毁责任
  • 正常关闭与共享状态

运行前先确认Java17、Spring Framework6.2;源码固定v6.2.11。流程描述常见非特殊Bean路径,教学资源回滚程序不实现容器的全部销毁和代理语义。

01先弄懂它是什么

构造器执行完,只代表 Java 对象已经构造,不代表 Spring 已经准备好把最终 Bean 交给调用者。容器之后还可能注入属性、执行初始化回调和后处理,最后交出的也可能是包装过的代理。区分原始对象、初始化中的对象和最终引用,才能避免过早访问数据库或启动后台线程。

构造器适合接收必需的协作者并建立基本状态。依赖准备好后的初始化可以使用 @PostConstruct,但它由相应后处理器参与执行,不是所有初始化都集中在这里。InitializingBean、指定 initMethod 和 BeanPostProcessor 各有作用时机,学习流程图时要看它描述哪一种创建路径,别把简图当成所有 Bean 都必须完全相同的顺序。

AOP 代理可以理解为业务对象外面的一层处理入口:调用先经过它,符合规则时才会执行事务等额外处理。初始化回调通常直接作用于目标对象;对象用 this 调自己的方法,也不会自动绕到外面的代理。因此,在 @PostConstruct 中调用自己的 @Transactional 方法,不能指望这次内部调用通过代理开启事务。

文中这几个词,先认一认

目标对象
真正保存业务方法实现的那个对象,调用者拿到的可能是包在外面的代理。
后处理器
在 Spring 创建 Bean 的各个阶段参与检查、调整或包装对象的扩展组件。
销毁责任
明确谁负责在什么时候停止对象持有的线程,并关闭文件、连接等资源。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

常见创建流程是:先创建对象、填好依赖,再进入 initializeBean。这个方法先执行部分 Aware 回调,让对象获得容器相关信息;随后运行初始化前处理器、初始化方法和初始化后处理器。

@PostConstruct 由相应的初始化前处理器识别并调用;InitializingBean 和自定义 initMethod 则属于后面的初始化方法部分。它们都参与准备对象,但触发位置不同,不能画成同一个回调。

常见的自动代理创建器会在初始化后,为业务对象加一层代理,因此调用者最后拿到的引用可能不是原对象。也有提前生成代理、在实例化前就直接提供替代对象等特殊路径。排查时先看调用者实际拿到了什么,再看调用有没有经过代理、方法能不能被增强。类代理不能增强 final 方法,JDK 代理通过接口工作;不能只看类上有注解,就认定所有方法都能获得额外处理。

资源初始化必须考虑部分成功。连接A已建立,线程池B启动失败时,应关闭已经拥有的A;关闭某资源又失败时,仍要继续清理其余资源并保存原因。正常容器关闭通常会调用受管单例销毁,prototype后续生命周期一般由使用者负责。强制终止不能保证销毁回调,因此不能把内存回调当成业务持久化保证。

正在加载流程图…

常见路径中初始化与最终代理暴露有先后;提前引用和特殊处理器路径需进一步查看实际源码。
查看流程图源码(Mermaid)
flowchart TD
 A["实例化目标"] --> B["填充依赖"]
 B --> C["Aware与初始化前处理"]
 C --> D["初始化方法"]
 D --> E["初始化后处理"]
 E --> F["暴露对象或代理"]
 G["外部调用者"] --> F
 F --> H["匹配通知与目标业务"]
 C --> I["初始化内部this调用"]
 I --> J["直接目标调用"]

按顺序看,每一步做了什么

  1. 列出Bean持有的资源及其获得、释放责任。
  2. 定位初始化回调由哪个扩展点触发。
  3. 分别记录目标与最终暴露对象,观察代理类型。
  4. 从另一个Bean与this分别调用,比较通知和事务证据。
  5. 验证初始化失败时的清理及正常关闭时的资源释放。

03对照实现代码

initializeBean中的包装边界 (新标签页)↗

Spring Framework v6.2.11 · AbstractAutowireCapableBeanFactory#initializeBean、#doCreateBean

原创教学实现,非源码节选。仅保留前处理、初始化与后处理的对象传递;未实现Aware、PostConstruct发现、synthetic分支、提前引用和代理生成。

核心机制代码 · java
import java.util.function.UnaryOperator;
class SourceModel {
    interface Bean { void initialize(); }
    static Bean initialize(Bean target, UnaryOperator<Bean> before,
            UnaryOperator<Bean> after) {
        // 前处理器可以参与准备,不能假定输入引用永远不变。
        Bean prepared = before.apply(target);
        prepared.initialize(); // 教学初始化动作,不等于所有Spring回调。
        // 后处理器可以返回包装对象,调用者应使用最终引用。
        return after.apply(prepared);
    }
}

doCreateBean在常见路径中完成实例创建、属性填充并调用initializeBean,单看构造器无法判断Bean是否完成管理。

initializeBean先处理部分Aware接口,再应用初始化前处理器,随后invokeInitMethods,最后应用初始化后处理器。

PostConstruct不应被误写为invokeInitMethods直接统一执行的所有内容;它由相关后处理器识别,具体扩展位置影响初始化调用行为。

方法返回 wrappedBean,说明容器最后交出的可能是原对象外面的包装。实例化前的特殊处理和提前暴露引用,还在其他代码路径中;不能只看这一个方法,就断言所有代理都一定在最后才创建。

代码先调用谁,再调用谁

  1. createBean
  2. doCreateBean
  3. populateBean
  4. initializeBean
  5. 初始化后处理返回最终引用

04动手写一个例子

资源启动失败的逆序回滚与清理异常保留

顺序获得若干资源,任何一步失败就逆序关闭此前成功资源;清理失败不阻断其他清理。成功后允许显式关闭一次,重复关闭无副作用。每个资源获得操作负责处理自身未完成的局部初始化。

输入 1
A成功、B获得失败
输出 1
open:A,open:B,close:A
输入 2
A与B成功,B关闭失败
输出 2
仍关闭A,并报告清理异常

资源成功返回后才加入拥有列表,使列表精确表达当前责任。后来失败时,从尾向前清理,符合后建立者可能依赖先建立者的顺序。

如果启动先失败,后来清理资源又失败,应把最初的启动异常保留为主要错误,把清理异常放进它的 suppressed 列表,作为附加原因。否则,最后一次 close 报错可能盖住真正的启动原因。若启动成功、只是显式关闭时出错,则单独收集并报告这些关闭错误。

关闭前先标记整体已关闭,保证重复close不会再次处理同一批资源。真实系统可能需要可重试释放或超时等待,那是额外契约,不能无条件重复执行带副作用的销毁。

为什么这样写是对的owned只包含已经成功获得且由本管理器负责的资源;回滚时每项恰好尝试关闭一次,任何单项失败不跳过其他资源。

时间 O(N)额外空间 O(N)

每个资源至多获得一次并关闭一次;拥有列表和异常链最坏各N项。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 72 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    record Spec(String name, boolean failOpen, boolean failClose) {}
    static class Resource {
        final Spec spec; final List<String> log;
        Resource(Spec spec,List<String> log) { this.spec=spec; this.log=log; }
        void close() {
            log.add("close:"+spec.name());
            if (spec.failClose()) throw new IllegalStateException("close:"+spec.name());
        }
    }
    static class Group implements AutoCloseable {
        final List<Resource> owned=new ArrayList<>();
        boolean closed;
        static Group open(List<Spec> specs,List<String> log) {
            Group group=new Group();
            try {
                for (Spec s:specs) {
                    log.add("open:"+s.name());
                    if (s.failOpen()) throw new IllegalStateException("open:"+s.name());
                    // 只有完整获得后才转移资源所有权。
                    group.owned.add(new Resource(s,log));
                }
                return group;
            } catch (RuntimeException primary) {
                group.release(primary);
                throw primary; // 保持最初失败,清理原因附加其上。
            }
        }
        void release(RuntimeException primary) {
            if (closed) return;
            closed=true;
            for (int i=owned.size()-1;i>=0;i--) {
                try { owned.get(i).close(); }
                // 单项关闭失败不能阻止更早资源的释放。
                catch (RuntimeException error) { primary.addSuppressed(error); }
            }
        }
        public void close() {
            if (closed) return;
            RuntimeException failures=new IllegalStateException("cleanup");
            release(failures);
            if (failures.getSuppressed().length>0) throw failures;
        }
    }
    static Spec ok(String name) { return new Spec(name,false,false); }
    static void eq(Object expected,Object actual) {
        if (!Objects.equals(expected,actual)) throw new AssertionError(actual);
    }
    public static void main(String[] args) {
        List<String> log=new ArrayList<>();
        Group g=Group.open(List.of(ok("A"),ok("B")),log);
        g.close(); g.close();
        eq(List.of("open:A","open:B","close:B","close:A"),log);
        log.clear();
        try { Group.open(List.of(ok("A"),new Spec("B",true,false)),log); }
        catch (IllegalStateException e) { eq("open:B",e.getMessage()); }
        eq(List.of("open:A","open:B","close:A"),log);
        log.clear();
        try {
            Group.open(List.of(new Spec("A",false,true),new Spec("B",true,false)),log);
            throw new AssertionError("expected startup failure");
        } catch (IllegalStateException e) { eq(1,e.getSuppressed().length); }
        log.clear();
        Group h=Group.open(List.of(ok("A"),new Spec("B",false,true)),log);
        try { h.close(); throw new AssertionError("expected close failure"); }
        catch (IllegalStateException e) { eq(1,e.getSuppressed().length); }
        eq(List.of("open:A","open:B","close:B","close:A"),log);
        Group.open(List.of(),new ArrayList<>()).close();
        System.out.println("PASS 5 resource lifecycle scenarios");
    }
}

这些特殊情况也要试一试

  • 空资源组可以关闭,重复关闭不重复产生动作。
  • 当前获得操作失败且尚未返回时,它负责自身局部清理;管理器只清理已接管项。
  • 关闭异常以suppressed保留,其他资源仍继续逆序清理。
进阶选读 · 有依赖资源图的关闭与异步排空

场景HTTP入口、任务队列和数据库池共同关闭,需要先停止新工作,再等待存量任务完成。

做法区分停止接收、排空、释放三个阶段,按依赖图逆序处理;为每阶段设置有界等待与未完成工作记录,不能无限等待一个故障资源。

复杂度图排序O(V+E),实际时间取决于排空预算与资源关闭耗时。

好处与代价有界关闭可能留下未完成工作,业务可靠性必须依靠持久化和恢复协议,不能仅靠销毁回调。

05项目里怎么用

不要在构造器或初始化回调中无限等待网络服务。需要启动时确认的必要条件应有明确超时和失败信息;可延迟加载的资源也要对首次使用失败做受控处理。初始化里触发带事务的业务动作时,使用适当启动机制调用另一个已受管Bean,并确认实际经过代理。

代理问题应从调用路径验证。类上存在注解不证明通知已执行,this调用、手动new、方法可见性和final限制都可能改变结果。可以记录通知入口和实际数据库事务行为,避免只用代理类名作为全部证据;代理存在也不代表每个方法都匹配切点。

正常关闭资源和恢复业务,是两类工作。线程、连接和文件应及时关闭,订单与消息处理结果则应可靠保存并能重试。应用被强制结束时,@PreDestroy 不保证完整执行,不能靠它兜底保存关键业务。prototype 对象后续持有的资源,也应由使用方通过明确关闭接口管理。

容易踩的坑

  • 把构造完成当成最终代理已经对外暴露。
  • 在@PostConstruct内部this调用期待新代理事务。
  • 认为prototype资源一定由容器在使用完自动销毁。
  • 清理异常覆盖主异常,导致真正启动原因丢失。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
构造器建立必需状态依赖显式,普通测试容易。不适合漫长网络初始化。纯对象关系与本地不变量。
初始化回调依赖已经可用,可准备资源。影响启动时延与失败路径。有界必要资源准备。
就绪后业务编排可通过其他受管Bean使用增强能力。需明确失败对就绪状态的影响。需要容器完整能力的启动用例。

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01目标对象与暴露对象必定相同吗?

展开参考答案与推导

不一定,后处理器可能包装代理或提供其他引用。

02PostConstruct等同于所有初始化方法吗?

展开参考答案与推导

不是,它由相关后处理能力识别,InitializingBean和自定义initMethod另有执行位置。

03正常销毁回调能保证强制终止时订单落库吗?

展开参考答案与推导

不能,业务持久性需要独立存储与恢复机制。

换个情况,还会处理吗

01初始化里调用自己的事务方法无效怎么办?

展开参考答案与推导

检查this路径与代理时机,改为适当启动阶段调用另一个受管Bean并验证事务。

02创建A成功、创建B失败,A泄露怎么办?

展开参考答案与推导

明确资源所有权并在失败路径逆序清理已接管资源。

03close异常掩盖启动错误怎么办?

展开参考答案与推导

保留启动异常作为主原因,清理失败作为suppressed附加并继续清理。

写代码试一试

01怎样验证重复关闭无副作用?

输入 / 条件
资源组close两次
输出 / 预期
每项close日志一次

提示 先标记关闭状态。

展开参考答案与推导

比较两次调用后的轨迹与一次调用一致。

02清理B失败后A是否还要关闭?

输入 / 条件
A、B已获得,B.close失败
输出 / 预期
仍出现close:A

提示 每项单独捕获异常。

展开参考答案与推导

把关闭放入循环内try/catch,汇总错误后再报告。

03如何支持关闭预算?

输入 / 条件
某资源迟迟不结束
输出 / 预期
到预算后报告未完成项

提示 不要无限阻塞关闭链。

展开参考答案与推导

将停止、排空和释放拆分并设置截止时间,保留未完成资源与业务恢复信息。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能沿对象生命周期标记资源准备、代理暴露与业务增强真正发生的位置。

本篇重点加练

重点核验代理对象与资源所有者,避免容器关闭后仍保留不应继续使用的对象。

  • 目标对象与代理
  • 单例与原型销毁责任

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    为一个构造器注入依赖且具有初始化回调的 Bean 画生命周期,标出 BeanPostProcessor 能观察或替换对象的阶段,以及需要核实的实际框架配置。

    提交内容
    阶段图和扩展点职责表。
    怎样算完成
    • 构造注入初始化不合并成一步
    • 观察对象与最终暴露对象区分

    练到的知识点实例化与依赖注入 · 初始化回调 · BeanPostProcessor

  2. 作业 02排错实验

    给服务公开方法添加计时通知,通过容器取得对象并与显式 new 的对象分别调用;记录代理类型、切点匹配和调用次数。

    提交内容
    最小 AOP 工程、自写调用检查与运行记录。
    怎样算完成
    • 通知是否执行有直接证据
    • 切点条件与调用入口清楚

    练到的知识点目标对象与代理 · 切点和通知

综合应用

  1. 作业 03面试场景

    初始化回调调用本对象带事务的导入方法,团队认为启动时必然受事务保护。设计核验代理形成时机、调用路径和实际事务状态的实验。

    提交内容
    诊断图与验收证据清单。
    怎样算完成
    • 初始化内部调用单独分析
    • 不由注解静态存在推断行为

    练到的知识点初始化内部调用限制

  2. 作业 04设计评审

    一个 singleton 管理连接资源,prototype 创建临时文件;应用既可能正常关闭也可能被强制结束。制定创建者、使用者和释放者的职责。

    提交内容
    资源责任表和关闭合同。
    怎样算完成
    • 作用域不代替资源所有权
    • 正常与异常终止的保证分开

    练到的知识点单例与原型销毁责任 · 正常关闭与共享状态

  3. 作业 05编程实践

    实现启动三项本地资源的管理器,第二或第三项可注入失败,释放操作也可注入失败;验证已成功创建资源的最终状态和原始失败信息。

    提交内容
    Java 实现、自写获取/清理失败及重复关闭测试、运行证据。
    怎样算完成
    • 资源释放次数可观察
    • 失败信息可追溯
    • 未创建资源不被误释放

    练到的知识点实例化与依赖注入 · 初始化回调 · 单例与原型销毁责任 · 正常关闭与共享状态

  4. 作业 06排错实验

    建立一个有关闭回调的服务,通过代理暴露并由调用方保存引用;正常关闭容器后执行合同允许的观察,记录目标、代理与资源状态。

    提交内容
    本地工程、自写生命周期检查和运行证据。
    怎样算完成
    • 对象身份与资源可用性分别核对
    • 关闭后操作不产生外部副作用

    练到的知识点目标对象与代理 · 单例与原型销毁责任

  5. 作业 07设计评审

    测试直接 new 服务,运行时却从容器取得代理,双方行为不同。制定哪些测试验证业务方法、哪些验证通知与代理边界的分工。

    提交内容
    测试范围图和代表场景。
    怎样算完成
    • 目标逻辑与增强行为都有证据
    • 没有用一层测试替代另一层

    练到的知识点目标对象与代理

  6. 作业 08面试场景

    两个 Bean 都包装同一个第三方客户端,并各自注册销毁回调;其中一个还允许独立提前关闭。制定资源所有权和重复关闭政策,说明需要哪些调用证据。

    提交内容
    共享资源图、关闭事件序列与验收条件。
    怎样算完成
    • 多拥有者歧义得到明确处理
    • 独立关闭与容器关闭均被覆盖

    练到的知识点单例与原型销毁责任

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    完成并发生命周期后:热重载替换一个共享客户端,旧请求仍在使用旧实例。设计发布新实例、停止新借用与关闭旧资源的合同。

    提交内容
    切换时序图与并发验收条件。
    怎样算完成
    • 对象发布与销毁边界分别控制
    • 新旧实例身份可追踪

    练到的知识点BeanPostProcessor · 目标对象与代理 · 切点和通知 · 正常关闭与共享状态

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    某 Bean 从普通对象改为代理对象,又开始包装第三方连接资源。审查比较身份、缓存引用和销毁操作的原假设,提出升级验收。

    提交内容
    变更影响表和生命周期测试矩阵。
    怎样算完成
    • 外层对象与底层资源关系明确
    • 升级结论不只依赖启动成功

    练到的知识点目标对象与代理 · 单例与原型销毁责任

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗