IoC(控制反转)容器、Bean(对象实例)生命周期与循环依赖
1. 学习定位与面试主线
本篇对应知识图谱 2.3.1。目标不是背诵一串回调,而是建立三个彼此独立又能衔接的模型:ApplicationContext(应用上下文)刷新负责准备容器,Bean(对象实例)创建负责把一份元数据变成可用对象,循环依赖处理负责在特定前提下暴露早期引用。面试时先说边界,再沿源码方法、对象状态和失败证据展开。
flowchart LR
A["IoC(控制反转)设计"] --> B["BeanDefinition(Bean 定义)注册与合并"]
B --> C["ApplicationContext(应用上下文)刷新"]
C --> D["BeanFactoryPostProcessor(Bean 工厂后置处理器)"]
D --> E["实例化与依赖解析"]
E --> F["Aware(感知接口)与初始化"]
F --> G["BeanPostProcessor(Bean 后置处理器)与代理"]
G --> H["作用域、销毁与运行"]
E --> I["三级缓存与早期引用"]
I --> J{"能否形成一致最终引用?"}
J -->|"能"| K["完整单例进入一级缓存"]
J -->|"不能"| L["创建失败或重构依赖"]图解:节点从设计思想走到元数据、容器刷新和对象创建,箭头表示前一阶段为后一阶段提供前提;右侧分支展示早期引用最终必须收敛为同一对象身份。正常路径产出可使用单例,失败路径保留创建异常并终止刷新。业务结论是:容器机制可以管理复杂性,但不能替代依赖方向和领域边界设计。
2. 容器设计与元数据
2.1 IoC(控制反转)、DI(依赖注入)与设计思想
IoC(控制反转)反转的是对象创建、装配、生命周期和扩展控制权。传统代码由业务对象主动创建依赖,调用者既承担业务职责又承担装配职责;容器模式让业务对象只声明所需能力,外部组合根根据元数据完成组装。DI(依赖注入)是 IoC(控制反转)的常见实现,构造器、工厂方法、属性和集合都可以成为注入通道。
flowchart TD
R["业务请求"] --> S["OrderService(订单服务)"]
S -->|"声明依赖"| P["PaymentPort(支付端口)"]
S -->|"声明依赖"| I["InventoryPort(库存端口)"]
C["容器组合根"] -->|"构造器注入"| S
C -->|"选择实现"| P
C -->|"选择实现"| I
P -->|"缺失或有多个候选"| F["依赖解析失败"]图解:业务服务只指向抽象端口,容器组合根承担实现选择;箭头表示依赖声明和实例装配。正常路径在启动期发现配置问题,失败路径因缺失或歧义候选而拒绝创建。结论是依赖显式、失败前置和替换成本降低,而不是“消除所有耦合”。
| 注入方式 | 创建时点 | 优势 | 风险与适用边界 |
|---|---|---|---|
| 构造器注入 | 实例化时 | 必需依赖显式,可保持不可变,便于测试 | 参数过多暴露职责膨胀;构造器循环直接失败 |
| 静态工厂方法 | 工厂调用时 | 可封装复杂选择和兼容第三方类型 | 工厂逻辑过重会隐藏依赖和失败原因 |
| 实例工厂方法 | 先获得工厂对象 | 适配已有工厂或渠道客户端构造器 | 工厂自身又形成依赖链 |
| 属性注入 | 实例化后 | 可表达可选依赖,并支持部分早期引用 | 对象存在短暂不完整状态,循环可能被掩盖 |
| 集合注入 | 依赖解析时 | 适合支付渠道策略、校验器和处理链 | 顺序、重复实现和空集合语义必须明确 |
数字数据演绎 1:构造器注入如何提前暴露错误
OrderApplicationService(订单应用服务)需要支付、库存和履约三个端口。T0 编译期可见 3 个构造参数;T1 测试直接传入 3 个替身;T2 启动时发现支付端口存在 2 个候选且没有主候选,容器立即失败。若使用字段注入并在测试中手工创建对象,错误可能到第一笔支付请求才出现。业务收益是把未知运行错误前移为确定启动错误。
热门面试题
问题(基础题):IoC(控制反转)和 DI(依赖注入)有什么区别?
- 考点:设计原则与实现手段。
- 回答思路:先定义控制权,再说明注入只是装配方法。
- 详细答案:IoC(控制反转)是一种职责重新分配:对象创建、依赖选择和生命周期由外部容器管理;DI(依赖注入)是在构造器、工厂方法或属性阶段把依赖传入对象。前者回答“谁控制”,后者回答“怎样组装”。容器还可提供作用域、事件和扩展链,因此二者不能简单画等号。
- 进阶追问:用了容器就一定低耦合吗?
- 进阶回答:不一定。业务类若依赖大量基础设施类型、共享可变状态或双向调用,容器只是隐藏创建代码;仍需接口隔离、依赖倒置和职责拆分。
问题(原理题):为什么构造器注入通常优于字段注入?
- 考点:对象不变量、可测试性和失败时点。
- 回答思路:围绕完整状态、不可变依赖和循环暴露回答。
- 详细答案:构造器注入要求必需依赖在实例化时齐全,对象一旦构造成功就满足基本不变量;字段可保持不可变,测试无需启动容器即可显式传入替身。字段注入让对象先被创建再变完整,隐藏依赖并使循环更容易被容器兼容。构造器参数过多也会及时提示类的职责可能过重。
- 进阶追问:所有依赖都必须放构造器吗?
- 进阶回答:必需依赖优先放构造器;真正可选且存在明确缺省行为的依赖可用属性或提供默认实现,但不能用可选语义掩盖必需业务能力。
问题(项目题):支付渠道接入如何利用 DI(依赖注入)避免大量条件分支?
- 考点:策略集合、稳定标识和配置失败。
- 回答思路:把每个渠道实现注入集合,再构建不可变路由表。
- 详细答案:每个渠道实现统一支付端口并提供稳定渠道标识,容器把实现集合注入路由器;路由器初始化时校验标识唯一、必需配置和能力矩阵,然后生成不可变映射。请求只按渠道标识选择实现,不在业务方法堆叠条件分支。重复标识应让启动失败,缺少非核心渠道可显式降级并告警。
- 进阶追问:单例渠道客户端就天然线程安全吗?
- 进阶回答:不是。作用域只约束实例数量;客户端必须不保存请求级可变字段,并确认连接池、签名器和令牌刷新逻辑支持并发。
2.2 BeanDefinition(Bean 定义)注册、解析与合并
BeanDefinition(Bean 定义)是容器的配方,不是对象本身。它记录类、作用域、构造参数、属性、工厂方法、初始化与销毁方法、懒加载和依赖关系。定义可来自组件扫描、配置类、导入、注册器或程序化注册。创建对象前,getMergedBeanDefinition会把父定义、子定义和装饰元数据合成为本次创建使用的定义快照。
flowchart TD
A["配置类/扫描/导入/注册器"] --> B["BeanDefinition(Bean 定义)读取"]
B --> C["名称生成与别名登记"]
C --> D{"名称是否冲突?"}
D -->|"可覆盖且策略允许"| E["替换或合并登记"]
D -->|"禁止覆盖"| X["启动失败并报告来源"]
D -->|"无冲突"| R["定义注册表"]
E --> R
R --> M["合并父子定义与作用域元数据"]
M --> V["校验并供创建流程消费"]图解:多种来源汇聚到定义注册表,名称冲突产生正常登记或失败分支;合并箭头表示创建前形成统一视图。结论是排查重复对象或属性未生效时,应先看定义来源和合并结果,而不是直接怀疑构造器。
| 元数据 | 典型来源 | 创建阶段影响 | 常见故障证据 |
|---|---|---|---|
| 类型与工厂方法 | 配置类、扫描 | 决定实例化策略 | 工厂方法异常或返回空值 |
| 作用域 | 注解、定义注册 | 决定缓存与销毁策略 | 原型误当单例、请求域离线访问 |
| 构造参数 | 配置、候选解析 | 选择构造器 | 候选歧义、类型转换失败 |
| 属性值 | 占位符、绑定、定义修改 | 属性填充 | 占位符未解析、值类型不匹配 |
| 依赖顺序 | 显式依赖声明 | 调整初始化和销毁顺序 | 实际业务依赖未声明仍可能过早调用 |
| 初始化/销毁方法 | 注解或配置 | 决定回调 | 方法不存在、异常导致刷新失败 |
数字数据演绎 2:重复定义如何改变启动结果
T0 基础模块注册名称为 paymentClient 的沙箱实现;T1 生产配置又注册同名真实实现;T2 若禁止覆盖,启动在注册阶段失败并显示两个来源;若允许覆盖,最终实现依赖处理顺序,升级后顺序变化可能把生产切回沙箱。正确方案是条件明确、名称稳定并用能力测试验证唯一候选,而不是依赖覆盖顺序。
热门面试题
问题(基础题):BeanDefinition(Bean 定义)和 Bean(对象实例)是什么关系?
- 考点:元数据与运行对象的边界。
- 回答思路:用“配方”和“成品”解释,并补充作用域。
- 详细答案:BeanDefinition(Bean 定义)描述如何创建和管理对象,包含类型、构造参数、作用域和回调;Bean(对象实例)是容器按该定义实际创建的运行对象。同一原型定义可产生多个实例,一个单例定义通常对应一个容器内缓存实例。修改定义发生在实例化前,不能等同于直接修改已经创建的对象。
- 进阶追问:定义注册后还能修改吗?
- 进阶回答:工厂后置处理阶段可以修改;普通单例开始创建后再改定义会造成部分对象使用旧值、部分使用新值,不应作为动态配置手段。
问题(原理题):为什么需要合并 BeanDefinition(Bean 定义)?
- 考点:继承定义、装饰元数据和创建快照。
- 回答思路:说明注册形态与创建所需完整形态不同。
- 详细答案:注册表中的定义可能是抽象父定义、子定义或经过配置类增强的定义,创建流程需要一份包含继承属性、作用域和方法元数据的完整视图。合并把这些来源归一,缓存结果并在定义变化时失效。源码阅读时应区分原始注册定义和合并定义,否则容易误判属性来源。
- 进阶追问:合并定义会创建对象吗?
- 进阶回答:不会,它仍然是元数据处理;真正对象创建从单例获取和
createBean路径开始。
问题(故障题):线上升级后出现两个同类型实现,如何定位?
- 考点:注册来源、条件评估、候选优先级。
- 回答思路:从定义注册表和条件报告取证,而非盲目加主候选。
- 详细答案:先记录候选名称、类型、资源来源和配置类,再检查扫描范围、导入和自动配置条件;确认是否因依赖升级新增默认实现。随后核对名称覆盖策略、主候选和限定符。修复应让条件互斥或注入点显式选择,并增加启动期唯一性校验,避免只加优先级掩盖重复注册。
- 进阶追问:为什么不直接允许覆盖?
- 进阶回答:覆盖把配置错误变成顺序依赖,升级、测试和生产环境可能得到不同实现,故障更隐蔽。
2.3 ApplicationContext(应用上下文)刷新主流程
refresh是容器级事务式编排,不等于创建某一个 Bean(对象实例)。它准备环境和工厂、加载定义、调用工厂后置处理器、注册对象后置处理器、初始化事件与资源、执行子类刷新、注册监听器、预实例化非懒加载单例,最后发布刷新完成事件。任何关键步骤失败都会进入异常清理路径,销毁本次已创建单例并重置活动状态。
flowchart TD
A["prepareRefresh:环境与状态"] --> B["obtainFreshBeanFactory:获得工厂"]
B --> C["prepareBeanFactory:基础设施注册"]
C --> D["postProcessBeanFactory:子类扩展"]
D --> E["invokeBeanFactoryPostProcessors:处理定义"]
E --> F["registerBeanPostProcessors:注册实例扩展链"]
F --> G["初始化消息源与事件广播器"]
G --> H["onRefresh:子类启动设施"]
H --> I["注册监听器"]
I --> J["finishBeanFactoryInitialization:预实例化单例"]
J --> K["finishRefresh:发布完成事件"]
E -->|"异常"| X["销毁已创建单例并取消刷新"]
J -->|"初始化异常"| X图解:上到下是容器刷新主干,定义扩展早于实例扩展,预实例化靠后;异常箭头汇入统一清理。正常路径发布完成事件后才具备稳定上下文,失败路径不应继续接流量。业务结论是启动探测、配置校验和资源创建必须有明确失败策略。
| 刷新阶段 | 处理对象 | 为什么在此时发生 | 不当操作 |
|---|---|---|---|
| 环境准备 | 属性源、活动配置 | 后续定义解析要使用 | 在此执行长时间远程调用 |
| 定义加载 | 对象元数据 | 先构建可修改注册表 | 依赖业务单例 |
| 工厂后置处理 | 对象定义 | 实例化前最后统一修改 | 提前获取普通对象 |
| 对象后置处理器注册 | 扩展器实例 | 后续每个对象都要经过完整链 | 注册太晚导致对象漏处理 |
| 预实例化 | 非懒加载单例 | 尽早验证依赖和初始化 | 构造器阻塞外部系统 |
| 完成事件 | 已刷新上下文 | 通知运行期组件 | 把事件当作跨系统可靠消息 |
数字数据演绎 3:刷新慢如何定位
一次启动总耗时 42s:定义扫描 1.4s,工厂后置处理 0.8s,对象后置处理器注册 0.3s,预实例化 38s。在 38s 中,支付客户端初始化探测占 30s,Runner(执行器)任务恢复占 6s。据此把非核心渠道探测改为 2s 超时并降级,把任务恢复移到就绪后受控执行,启动降至 7s;这比盲目减少扫描包更有证据。
热门面试题
问题(基础题):请概括 ApplicationContext(应用上下文)的刷新流程。
- 考点:容器级阶段顺序。
- 回答思路:按环境、定义、扩展器、单例、事件和失败清理回答。
- 详细答案:刷新先准备环境并取得对象工厂,随后加载和处理定义;先执行 BeanFactoryPostProcessor(Bean 工厂后置处理器),再注册 BeanPostProcessor(Bean 后置处理器)。之后准备消息、事件等基础设施,预实例化非懒加载单例,成功才发布完成事件。任何关键异常都销毁已创建单例并标记刷新失败。
- 进阶追问:刷新完成是否代表所有对象都已创建?
- 进阶回答:不代表。懒加载对象、原型对象和部分自定义作用域对象可能尚未创建;完成只说明本轮必须初始化的基础设施和非懒加载单例已成功。
问题(原理题):为什么工厂后置处理器必须早于对象后置处理器?
- 考点:元数据阶段和实例阶段。
- 回答思路:用输入输出依赖解释顺序。
- 详细答案:工厂后置处理器修改的是创建配方,必须在普通对象实例化前完成;对象后置处理器消费实例并可能生成代理,必须先注册完整链再创建业务对象。若顺序反过来,早期对象会按未修订定义创建,且错过注入、生命周期或代理处理,导致同一容器中行为不一致。
- 进阶追问:能否在工厂后置处理器中获取业务对象?
- 进阶回答:技术上某些路径可以触发,但会造成过早实例化并错过后续处理器,除非非常理解阶段契约,否则应避免。
问题(故障题):启动失败后为什么要销毁已经创建的单例?
- 考点:资源泄漏和上下文原子性。
- 回答思路:说明半初始化容器不能服务,已获取资源必须回收。
- 详细答案:刷新中可能已经创建线程池、连接池和文件句柄;若后续对象失败而不销毁,这些资源会泄漏,甚至继续执行任务。统一销毁让上下文呈现“成功可用或失败关闭”的状态。排查时应保留最初创建异常,销毁异常只能作为附加证据,不能覆盖根因。
- 进阶追问:可以捕获初始化异常继续启动吗?
- 进阶回答:只有该能力明确可选、健康状态与流量路由能反映降级且没有破坏核心不变量时才可;核心支付或库存组件不应静默忽略。
2.4 BeanFactoryPostProcessor(Bean 工厂后置处理器)与 BeanPostProcessor(Bean 后置处理器)
两类扩展点名字相近但阶段不同。BeanFactoryPostProcessor(Bean 工厂后置处理器)面向定义注册表,适合占位符解析、扫描补充和元数据调整;BeanPostProcessor(Bean 后置处理器)面向每个实例,覆盖实例化前后、属性处理、初始化前后和销毁等扩展协议,注入注解、生命周期注解和自动代理都依赖它。
flowchart LR
D["BeanDefinition(Bean 定义)注册表"] --> F["BeanFactoryPostProcessor(Bean 工厂后置处理器)"]
F --> M["修订后的定义"]
M --> C["创建原始对象"]
C --> P1["BeanPostProcessor(Bean 后置处理器)初始化前"]
P1 --> I["初始化回调"]
I --> P2["BeanPostProcessor(Bean 后置处理器)初始化后"]
P2 -->|"原对象"| O["最终 Bean(对象实例)"]
P2 -->|"包装"| A["代理 Bean(对象实例)"]图解:左侧扩展元数据,右侧扩展实例;箭头展示实例扩展可以替换最终引用。失败路径常见于处理器自身过早获取对象,造成扩展链不完整。结论是扩展器要保持轻量、明确顺序并避免业务副作用。
| 维度 | BeanFactoryPostProcessor(Bean 工厂后置处理器) | BeanPostProcessor(Bean 后置处理器) |
|---|---|---|
| 输入 | 对象工厂与定义注册表 | 已实例化对象和名称 |
| 时机 | 普通对象创建前 | 实例化、属性或初始化阶段 |
| 可改变 | 类型元数据、属性、作用域、定义集合 | 实例状态或最终返回引用 |
| 典型用途 | 配置占位符、配置类解析、自定义注册 | 注入、构造后回调、自动代理 |
| 主要风险 | 提前创建对象、定义顺序依赖 | 重复包装、过早依赖、代理不一致 |
数字数据演绎 4:处理器注册过晚的行为分叉
容器共有 120 个业务对象。一个自定义审计处理器在 T1 之前被某工厂扩展器通过 getBean提前触发 8 个对象;T2 审计处理器才完成注册;最终 112 个对象有审计代理,8 个没有。接口测试可能只命中后者之一。修复不是给 8 个对象单独补代理,而是移除早期获取并验证所有候选都经过完整处理器链。
热门面试题
问题(基础题):两类后置处理器最核心的区别是什么?
- 考点:定义与实例。
- 回答思路:先说处理对象,再说调用时点和能力。
- 详细答案:BeanFactoryPostProcessor(Bean 工厂后置处理器)在普通对象创建前处理 BeanDefinition(Bean 定义)或注册表;BeanPostProcessor(Bean 后置处理器)在对象创建过程中处理实例,并可返回代理。一个改变“怎样创建”,一个改变“创建出的对象怎样被加工或暴露”。
- 进阶追问:哪个可以实现自动代理?
- 进阶回答:自动代理主要依赖 BeanPostProcessor(Bean 后置处理器)体系,因为它能在实例初始化后返回包装对象;定义扩展只能准备元数据。
问题(原理题):为什么处理器内部随意调用
getBean危险?- 考点:过早实例化和链不完整。
- 回答思路:沿注册顺序说明对象会错过哪些能力。
- 详细答案:处理器注册阶段尚未保证完整扩展链就绪,主动获取依赖会提前创建对象;该对象可能错过后续注入处理、生命周期回调或自动代理,还可能触发循环。最终同类型对象出现有的被包装、有的没有。正确做法是缩小处理器依赖,延迟查找或使用基础设施级依赖。
- 进阶追问:处理器也会被普通处理器处理吗?
- 进阶回答:其创建阶段特殊,不能假设能享受完整普通对象生命周期;因此处理器自身更应简单并避免依赖业务对象。
问题(项目题):幂等注解更适合通过哪类扩展实现?
- 考点:代理和业务边界。
- 回答思路:用实例后置处理发现候选并创建拦截代理。
- 详细答案:可由对象后置处理器识别带幂等标记的方法并构建代理,调用时提取业务键、查幂等记录并执行业务。但幂等结果仍需数据库唯一约束或状态机兜底,不能只靠进程内切面。处理器必须避免重复代理,并明确与事务代理的顺序。
- 进阶追问:为什么不改 BeanDefinition(Bean 定义)直接实现?
- 进阶回答:定义可登记候选信息,但方法调用拦截需要运行时代理链;真正的执行逻辑仍属于对象实例扩展。
3. 单个对象创建与生命周期
3.1 实例化策略、构造器选择与 FactoryBean(工厂对象)
单例获取从缓存检查开始,未命中才进入创建。createBean先解析类型和覆盖方法,再给实例化前处理器机会;doCreateBean选择构造器、静态工厂或实例工厂方法创建原始对象。FactoryBean(工厂对象)本身也是 Bean(对象实例),名称正常获取时通常返回其产品,带特殊前缀获取时才返回工厂本身;产品是否单例由工厂契约决定。
flowchart TD
A["getBean:按名称和类型请求"] --> B{"一级缓存命中?"}
B -->|"是"| R["返回完整单例"]
B -->|"否"| C["合并定义并处理依赖顺序"]
C --> D{"实例化方式"}
D -->|"构造器"| E["解析参数并调用构造器"]
D -->|"静态工厂"| F["调用静态工厂方法"]
D -->|"实例工厂"| G["先获得工厂对象再调用"]
E --> H{"是否 FactoryBean(工厂对象)?"}
F --> H
G --> H
H -->|"否"| I["进入属性填充"]
H -->|"是且请求产品"| J["调用 getObject 并按契约缓存"]图解:缓存命中是快速路径,未命中才解析实例化策略;工厂对象分支区分工厂和产品。失败路径包括构造器歧义、参数无法解析和工厂方法异常。结论是面试时必须说明“对象从哪里来”不只一种路径。
| 创建方式 | 关键输入 | 常见用途 | 失败边界 |
|---|---|---|---|
| 默认构造器 | 类型与无参构造器 | 简单组件 | 无可用构造器 |
| 自动装配构造器 | 候选参数和优先规则 | 推荐的必需依赖注入 | 候选歧义、参数循环 |
| 静态工厂方法 | 类型上的工厂方法 | 封装第三方创建 | 方法重载选择歧义 |
| 实例工厂方法 | 工厂 Bean(对象实例) | 依赖已有工厂状态 | 工厂自身创建失败 |
| FactoryBean(工厂对象)产品 | 工厂的 getObject | 复杂代理、客户端或框架产品 | 工厂与产品类型/作用域混淆 |
数字数据演绎 5:工厂与产品的实例数量
容器中只有 1 个支付客户端 FactoryBean(工厂对象)。若它声明产品为单例,连续 10000 次获取都复用 1 个渠道客户端和连接池;若产品为非单例,则可能创建 10000 个客户端,带来连接池和线程资源爆炸。监控不能只数工厂对象,还要核对产品缓存契约和实际资源数量。
热门面试题
问题(基础题):实例化和初始化有什么区别?
- 考点:创建原始对象与生命周期回调。
- 回答思路:指出两者之间还有属性填充和感知回调。
- 详细答案:实例化是通过构造器或工厂方法得到原始对象;初始化发生在依赖注入和 Aware(感知接口)回调之后,包括构造后回调、初始化接口、自定义初始化方法以及初始化前后处理。对象已实例化不代表可投入使用,初始化失败会让创建整体失败。
- 进阶追问:代理通常在哪个阶段产生?
- 进阶回答:常见自动代理在初始化后处理阶段返回;循环依赖时还可能通过早期引用扩展提前产生并在最终阶段复用。
问题(原理题):FactoryBean(工厂对象)和普通工厂方法有什么区别?
- 考点:容器协议和产品解引用。
- 回答思路:说明前者是容器识别的特殊对象契约。
- 详细答案:普通工厂方法只是 BeanDefinition(Bean 定义)的一种实例化策略;FactoryBean(工厂对象)本身进入容器,并通过统一协议生产另一个对象,容器还处理产品类型预测和单例缓存。按普通名称通常得到产品,按特殊解引用语义得到工厂本身。
- 进阶追问:工厂是单例,产品一定单例吗?
- 进阶回答:不一定,产品作用由 FactoryBean(工厂对象)的契约决定;必须分别讨论工厂实例和产品实例。
问题(故障题):构造器里发起远程调用有什么风险?
- 考点:刷新阻塞、半初始化和重试放大。
- 回答思路:从不可控延迟和失败清理说明。
- 详细答案:构造器发生在对象尚未进入完整生命周期时,远程超时会阻塞容器刷新,异常信息还容易被包装为创建失败;若多个对象重复探测,会放大下游压力。应把本地必要校验放初始化,远程探测设严格超时并按核心性决定失败或降级,非关键预热可在就绪后受控执行。
- 进阶追问:把调用移到构造后回调就安全了吗?
- 进阶回答:只改善对象已注入的前提,仍会阻塞刷新且代理可能尚未最终完成;还需评估时限、失败策略和流量就绪条件。
3.2 依赖解析、属性填充与候选选择
属性填充不仅是反射赋值。容器先执行实例化后处理,解析注入点形成依赖描述,再按类型、限定标识、主候选、优先级、名称和可选语义筛选候选;集合注入会收集并排序多个实现。解析依赖可能递归触发其他 Bean(对象实例)创建,并登记依赖关系用于销毁顺序和循环检查。
flowchart TD
A["注入点形成依赖描述"] --> B["按类型搜索候选"]
B --> C{"候选数量"}
C -->|"0"| D{"是否可选或有默认值?"}
D -->|"否"| X["缺失依赖,创建失败"]
D -->|"是"| O["注入空值、延迟提供器或默认实现"]
C -->|"1"| S["选择唯一候选"]
C -->|"多个"| Q["限定标识/主候选/优先级/名称筛选"]
Q -->|"仍歧义"| Y["歧义依赖,创建失败"]
Q -->|"唯一"| S
S --> R["递归获取候选并登记依赖关系"]
R --> P["属性写入或参数传入"]图解:候选数决定正常、可选或失败分支,筛选箭头给出优先规则;递归获取说明注入可能启动新的创建链。业务结论是候选选择应在配置层显式表达,不能依赖偶然名称。
| 依赖形态 | 解析结果 | 适合场景 | 风险 |
|---|---|---|---|
| 单对象必需依赖 | 唯一候选 | 核心仓储、支付端口 | 多候选歧义直接失败 |
| 可选依赖 | 空值或包装缺席 | 非核心增强能力 | 把核心能力误设可选导致静默降级 |
| 延迟提供器 | 使用时再获取 | 打破初始化时点耦合 | 运行期才暴露配置错误 |
| 列表或映射 | 全部候选并排序 | 渠道策略、校验链 | 顺序与重复标识需校验 |
| 限定候选 | 按语义标识筛选 | 国内/海外支付实现 | 字符串标识漂移 |
数字数据演绎 6:集合注入顺序影响结果
库存扣减链包含校验、幂等、额度和审计 4 个处理器。错误顺序把审计放在幂等前,重复请求 100 次会写 100 条尝试审计;调整为幂等判定后再业务审计,只写 1 条成功事实和 99 条聚合重复指标。容器能注入集合,但业务必须定义稳定顺序、唯一标识和重复处理语义。
热门面试题
问题(基础题):按类型注入出现多个候选时怎样选择?
- 考点:限定标识、主候选、优先级和名称。
- 回答思路:说明不是简单“随便取一个”。
- 详细答案:容器先找类型匹配候选,再结合注入点限定标识、主候选、优先级和名称缩小范围;集合依赖会保留多个并排序。若单值依赖仍有多个等价候选,应明确报歧义而不是随机选择。项目中应以业务语义限定,而不是依赖类名巧合。
- 进阶追问:加主候选总能解决吗?
- 进阶回答:只能给默认选择;不同业务点需要不同实现时仍应使用语义限定或路由器,主候选不能掩盖配置重复。
问题(原理题):为什么依赖注入会触发循环依赖?
- 考点:递归创建链。
- 回答思路:从 A 填充 B、B 填充 A 展开。
- 详细答案:创建 A 到属性填充时解析 B,若 B 未存在就递归创建 B;B 填充时又解析 A,于是回到正在创建的单例。单例属性注入可在 A 已实例化后通过早期引用中断递归;构造器注入在 A 尚未实例化前就要求 B,没有可暴露对象,因此失败。
- 进阶追问:登记依赖关系只为检查循环吗?
- 进阶回答:还用于销毁顺序、依赖创建顺序和异常诊断,表示对象间实际容器依赖。
问题(项目题):多个海外仓适配器如何避免候选歧义?
- 考点:策略映射、配置校验和能力差异。
- 回答思路:集合注入后建立业务标识映射。
- 详细答案:每个适配器声明仓库渠道码和能力,集合注入到路由器;初始化时校验渠道码唯一、必需能力和启用配置,构建不可变映射。订单履约按已保存渠道码路由,不能用实现类名。缺失启用渠道时启动失败或显式关闭该渠道,避免运行时随机选实现。
- 进阶追问:动态新增渠道必须重启吗?
- 进阶回答:代码实现新增通常需要发布;仅路由权重可动态配置,但更新要有版本、校验和回滚,不能运行中任意替换共享对象。
3.3 Aware(感知接口)、初始化回调、代理与销毁
属性填充后,容器按约定执行名称、工厂和上下文等 Aware(感知接口)回调,再经过初始化前处理、PostConstruct(构造后回调)、InitializingBean(初始化接口)、自定义初始化方法和初始化后处理。自动代理常在最后返回包装引用。关闭时,先执行销毁前处理,再执行 PreDestroy(销毁前回调)、DisposableBean(销毁接口)和自定义销毁方法;实际顺序还受适配器和依赖关系控制。

flowchart TD
A["属性填充完成"] --> B["Aware(感知接口)回调"]
B --> C["BeanPostProcessor(Bean 后置处理器)初始化前"]
C --> D["PostConstruct(构造后回调)"]
D --> E["InitializingBean(初始化接口)"]
E --> F["自定义初始化方法"]
F --> G["BeanPostProcessor(Bean 后置处理器)初始化后"]
G -->|"无需代理"| H["暴露原对象"]
G -->|"匹配通知器"| I["暴露代理"]
H --> J["运行"]
I --> J
J --> K["PreDestroy(销毁前回调)"]
K --> L["DisposableBean(销毁接口)与自定义销毁"]图解:属性注入后才进入感知与初始化链,代理分支决定最终暴露引用;运行结束进入销毁链。失败路径是任一初始化回调抛异常,当前对象不会投入使用。业务结论是初始化只做有界、可失败说明的资源准备,销毁必须幂等。
| 顺序 | 回调或扩展 | 适合工作 | 不适合工作 |
|---|---|---|---|
| 1 | Aware(感知接口) | 获取容器基础设施标识 | 让领域对象普遍依赖容器 |
| 2 | 初始化前处理 | 注解生命周期、统一校验 | 隐式远程副作用 |
| 3 | 构造后回调 | 本地配置校验、轻量资源准备 | 依赖自身最终代理调用 |
| 4 | 初始化接口 | 框架型显式初始化 | 普通业务类无必要耦合接口 |
| 5 | 自定义初始化 | 配置化兼容旧组件 | 多处重复同一初始化 |
| 6 | 初始化后处理 | 代理、包装、最终校验 | 每次重复创建昂贵资源 |
| 7 | 销毁链 | 停任务、拒新请求、关资源 | 无期限等待或非幂等释放 |
数字数据演绎 7:初始化与优雅关闭窗口
履约客户端有 40 个活动连接和 12 个在途请求。收到关闭信号后先把就绪状态置为拒绝新流量,等待 20s;18s 内完成 11 个请求,剩余 1 个按请求标识记录未知状态并交补偿查询,然后关闭连接池。若先关连接再摘流量,全部 12 个请求都可能得到连接重置,产生重复重试。
热门面试题
问题(基础题):完整初始化顺序怎样讲?
- 考点:感知、前处理、三类初始化和后处理。
- 回答思路:说明依赖已填充,并指出最终代理时点。
- 详细答案:属性填充后先执行 Aware(感知接口)回调;随后对象后置处理器初始化前处理,其中通常触发生命周期注解;再执行初始化接口和自定义初始化方法;最后对象后置处理器初始化后处理,可返回代理。最终注册和注入的引用可能不是原始对象。
- 进阶追问:三类初始化回调都应同时使用吗?
- 进阶回答:通常选一种清晰方式即可;同时使用会增加顺序认知和重复执行风险,框架组件才更常用接口契约。
问题(原理题):为什么构造后回调里调用自身事务方法常不生效?
- 考点:代理形成时点和内部调用。
- 回答思路:同时解释初始化阶段与自调用旁路。
- 详细答案:构造后回调发生在初始化前处理阶段,最终事务代理通常尚未由初始化后处理返回;而对象内部直接调用自身方法也不经过外部代理。需要事务的启动逻辑应放到另一个已完成代理的对象中,或在上下文就绪后通过容器取得的代理调用,并保证失败可恢复。
- 进阶追问:就绪事件一定可靠执行一次吗?
- 进阶回答:不是跨系统可靠消息,进程崩溃可中断且多实例会各执行;重要任务要持久化状态、抢占所有权并幂等。
问题(项目题):支付客户端的销毁回调应该做什么?
- 考点:流量摘除、在途请求和资源释放。
- 回答思路:先停止接收,再等待有界窗口,最后关闭资源。
- 详细答案:先标记实例不就绪并停止新任务,等待有界时间让在途请求完成;未知结果按原业务键记录并交后续查询,之后关闭连接池、刷新线程和指标上报器。销毁操作必须可重复,不能因单个资源关闭异常跳过其余资源,并应记录剩余在途数量。
- 进阶追问:prototype(原型作用域)对象也会自动销毁吗?
- 进阶回答:容器通常负责创建和初始化,但不完整跟踪每个原型实例的终止时点;持有资源的原型对象应由调用方显式关闭。
3.4 singleton(单例作用域)、prototype(原型作用域)与线程安全
singleton(单例作用域)表示一个对象工厂中同名定义通常只有一个共享实例,不表示进程全局唯一,更不保证线程安全。prototype(原型作用域)表示每次解析通常创建新实例;单例直接注入原型时,注入动作只发生一次,后续仍复用当时那个原型引用,需要提供器或作用域代理才能按使用时获取新对象。
flowchart LR
C1["ApplicationContext(应用上下文)A"] --> S1["singleton(单例作用域)实例 X1"]
C2["ApplicationContext(应用上下文)B"] --> S2["singleton(单例作用域)实例 X2"]
S1 -->|"直接注入一次"| P1["prototype(原型作用域)实例 P1"]
S1 -->|"通过提供器每次获取"| P2["prototype(原型作用域)P2"]
S1 -->|"下次获取"| P3["prototype(原型作用域)P3"]
S1 -->|"共享可变字段"| R["并发竞态"]图解:两个上下文各有自己的单例,证明它不是语言级全局单例;直接注入原型只产生一次,提供器分支才按次创建。共享字段箭头指出线程安全失败路径。结论是无状态服务适合单例,请求状态应留在方法局部或专用作用域。
| 作用域/访问方式 | 实例数量 | 销毁责任 | 线程安全结论 |
|---|---|---|---|
| singleton(单例作用域) | 每个工厂每个名称通常 1 个 | 容器跟踪并销毁 | 必须自行保证共享状态安全 |
| prototype(原型作用域) | 每次解析新建 | 调用方负责终止资源 | 实例隔离不等于外部资源隔离 |
| 单例直接注入原型 | 创建单例时得到 1 个 | 容易误判 | 实际仍共享该引用 |
| 单例通过提供器取原型 | 每次调用可新建 | 调用方管理 | 请求间可隔离,但创建成本增加 |
| 作用域代理 | 注入稳定代理,调用时解析目标 | 由作用域管理器决定 | 异步线程可能缺失上下文 |
数字数据演绎 8:单例可变字段造成串单
一个单例履约服务把 currentOrderId放成员字段。线程甲写 A100 后暂停,线程乙写 B200 并生成面单,线程甲恢复时读取到 B200,导致 A 订单关联 B 面单。把订单号保持在方法参数和不可变上下文后,1000 并发请求不再共享状态。容器作用域没有制造竞态,错误来自业务对象保存请求级可变数据。
热门面试题
问题(基础题):Spring(Java 应用框架)单例和设计模式单例有什么不同?
- 考点:作用域边界。
- 回答思路:一个是容器缓存语义,一个是类级实例控制。
- 详细答案:singleton(单例作用域)以对象工厂和名称为边界,同一进程多个上下文可各有实例;设计模式单例通常由类自身保证某个类加载器范围只有一个实例。容器单例还参与注入、代理和销毁。两者都不自动保证业务线程安全。
- 进阶追问:多个名称指向同类型会怎样?
- 进阶回答:每个定义名称可对应独立单例;按类型注入还可能因此产生候选歧义。
问题(原理题):为什么单例注入原型后没有每次得到新实例?
- 考点:注入发生时点。
- 回答思路:单例只创建和注入一次。
- 详细答案:创建单例时解析原型依赖并把当时实例写入字段,单例之后从一级缓存复用,不再执行属性注入。因此原型语义只在“解析依赖”时生效。若每次业务调用都需新对象,应注入提供器或作用域代理并明确释放责任。
- 进阶追问:用原型就能解决线程安全问题吗?
- 进阶回答:只能隔离对象内部状态;共享数据库、静态变量、连接或缓存仍可能竞态,且频繁创建有成本。
问题(项目题):WMS(仓储管理系统)服务类应怎样设计为安全单例?
- 考点:无状态、不可变依赖和并发资源。
- 回答思路:请求状态局部化,共享资源使用线程安全实现。
- 详细答案:订单号、仓库号和库存版本通过参数或不可变命令对象传递,不放成员字段;依赖在构造后不替换;缓存和计数器使用有明确并发语义的组件;数据库正确性由条件更新和唯一约束保证。线程安全审查还要覆盖第三方客户端和拦截器,而不仅是服务类。
- 进阶追问:使用同步关键字包住方法可以吗?
- 进阶回答:只能限制单进程并发并降低吞吐,无法保护多实例共享库存;应优先移除共享状态并在权威存储建立原子约束。
3.5 懒加载、初始化失败与优雅销毁
懒加载把创建从刷新期推迟到首次解析,能缩短启动或避开暂时不需要的组件,但也把配置错误和首次延迟推到运行期。初始化异常必须区分核心能力和可选能力:核心不变量依赖的组件应让启动失败;可选渠道可降级,但必须暴露健康状态。销毁阶段按依赖关系关闭,并对在途任务提供有界排空、状态持久化和补偿入口。
flowchart TD
A["容器刷新"] --> B{"是否懒加载?"}
B -->|"否"| C["启动期创建与校验"]
B -->|"是"| D["仅注册定义"]
D --> E["首次请求触发创建"]
C --> F{"初始化成功?"}
E --> F
F -->|"是"| G["投入使用"]
F -->|"核心能力失败"| X["启动或请求失败,阻止流量"]
F -->|"可选能力失败"| Y["显式降级、告警和重试预算"]
G --> H["关闭:拒新流量"]
H --> I["排空在途并保存未知状态"]
I --> J["按依赖顺序释放资源"]图解:创建时点分为启动和首次使用,初始化结果按核心性分流;关闭路径先拒绝新流量再释放。结论是懒加载是时点策略,不是错误恢复机制,优雅关闭是业务收敛过程而非简单执行关闭方法。
| 决策 | 收益 | 代价 | 适用证据 |
|---|---|---|---|
| 启动期创建 | 错误前置,首请求稳定 | 启动更慢,依赖同时受压 | 核心支付、库存和数据库组件 |
| 懒加载 | 降低启动工作量 | 首请求抖动、错误后移 | 低频非核心报表适配器 |
| 初始化失败即退出 | 避免半可用状态 | 可用实例数量减少 | 无组件就无法守住不变量 |
| 显式降级 | 保留其他能力 | 需健康路由和恢复逻辑 | 渠道可独立关闭 |
| 有界优雅关闭 | 减少中断和重复 | 需截止时间和未知态处理 | 支付、履约和异步任务 |
数字数据演绎 9:懒加载的首请求代价
报表适配器初始化需要 4.8s。改懒加载后启动从 12s 降到 7.2s,但首个导出请求超时阈值只有 3s,于是首次必失败并触发 3 次重试。最终采用就绪后预热且限制并发,启动探针不等待报表,预热完成前接口返回明确“准备中”,避免用用户请求承担初始化。
热门面试题
问题(基础题):懒加载有什么优缺点?
- 考点:错误时点与首请求延迟。
- 回答思路:不要只说启动快。
- 详细答案:懒加载减少刷新期创建量,适合低频可选能力;代价是依赖缺失、配置错误和初始化耗时推到首次使用,用户请求可能承受冷启动。它还会改变循环依赖和监控表现。核心组件通常更适合启动期校验,非核心组件要配合预热和明确降级。
- 进阶追问:全局开启懒加载是否推荐?
- 进阶回答:不应仅为启动数字全局开启;它可能把大量确定错误后移。应按组件价值和冷启动预算选择。
问题(原理题):初始化失败后容器为什么可能整体刷新失败?
- 考点:非懒加载单例和上下文一致状态。
- 回答思路:说明必需单例是刷新完成条件。
- 详细答案:预实例化非懒加载单例是刷新步骤之一,任一必需对象无法创建意味着依赖图不完整;容器不能发布完成状态,于是销毁本轮已创建单例并抛出根异常。若业务要容忍某组件失败,应在设计上把它定义成可选并提供健康、降级和恢复机制,而不是吞异常。
- 进阶追问:捕获异常返回空对象可以吗?
- 进阶回答:通常会把显式启动失败变成后续空指针或错误业务结果;应返回有明确失败语义的降级实现并限制路由。
问题(项目题):Runner(执行器)调度任务在关闭时怎样处理?
- 考点:停止领取、租约、在途状态和恢复。
- 回答思路:先停止新任务,再处理租约和可恢复状态。
- 详细答案:实例先从调度选主或任务领取中退出,停止拉取新任务;在途任务在截止时间内完成并续写心跳,超时则保存检查点、释放或等待租约过期,由其他实例按任务键幂等接管。关闭不能直接把处理中改成功,也不能无限等待阻塞发布。
- 进阶追问:进程被强杀怎么办?
- 进阶回答:依靠持久任务状态、租约过期、幂等执行和补偿扫描恢复,不能只依赖销毁回调。
4. 循环依赖、代理一致性与设计边界
4.1 三级缓存与属性循环依赖全过程
单例三级缓存分别是完整单例缓存、早期单例缓存和早期引用工厂缓存。创建 A 后、填充属性前,容器把可生成 A 早期引用的工厂放入三级缓存;A 需要 B,于是创建 B;B 又需要 A 时,先查一级、再查二级,最后调用三级工厂获得 A 的早期引用并移入二级。B 完成后注入 A,A 完成初始化后进入一级并清除早期条目。

sequenceDiagram
participant C as Container(容器)
participant A as ServiceA(服务 A)
participant B as ServiceB(服务 B)
participant L3 as singletonFactories(三级缓存)
participant L2 as earlySingletonObjects(二级缓存)
participant L1 as singletonObjects(一级缓存)
C->>A: 实例化原始 A
C->>L3: 放入 A 的早期引用工厂
C->>B: A 填充属性时请求 B
C->>B: 实例化原始 B 并暴露工厂
C->>C: B 填充属性时请求 A
C->>L1: 查完整 A,未命中
C->>L2: 查早期 A,未命中
C->>L3: 调用工厂生成 A 的早期引用
L3-->>L2: 移入早期 A,删除三级条目
L2-->>B: 注入早期 A
B-->>C: B 完成并进入一级缓存
C-->>A: 注入完整 B
A-->>L1: A 完成并进入一级缓存图解:时序从 A 实例化开始,缓存箭头展示三级到二级再到一级的迁移;正常路径要求 A 已有原始实例。若不允许早期暴露、不是单例或仍在构造阶段,则走失败路径。业务结论是缓存只是延迟闭合对象图,不解决领域双向职责。
| 缓存 | 保存内容 | 放入时点 | 移除时点 | 核心目的 |
|---|---|---|---|---|
| singletonObjects(一级缓存) | 完成初始化的最终单例 | 创建成功后 | 销毁或失败清理 | 正常共享与快速获取 |
| earlySingletonObjects(二级缓存) | 已实际暴露的早期引用 | 三级工厂首次被调用后 | 最终完成或失败 | 后续循环获取返回同一引用 |
| singletonFactories(三级缓存) | 延迟生成早期引用的工厂 | 实例化后、属性填充前 | 被调用、完成或失败 | 仅在需要时介入早期代理 |
数字数据演绎 10:A/B 缓存状态逐时刻变化
T0 三层均空;T1 原始 A 创建,三级为 {A:factory};T2 原始 B 创建,三级为 {A:factory,B:factory};T3 B 请求 A,调用 A 工厂,三级只剩 B,二级为 {A:A'};T4 B 完成,一级为 {B}并清除 B 早期条目;T5 A 完成,一级为 {A:A',B}且二三级清空。任何失败都必须清除当前名称相关缓存,防止返回半对象。
热门面试题
问题(基础题):三级缓存分别存什么?
- 考点:完整对象、早期引用和引用工厂。
- 回答思路:结合放入和迁移时点回答。
- 详细答案:一级保存完成初始化的最终单例;二级保存已经暴露的早期对象或早期代理;三级保存能延迟生成早期引用的工厂。真正发生循环时才调用三级工厂并把结果移到二级,完成后进入一级并清除早期条目。
- 进阶追问:三级缓存是三个独立生命周期吗?
- 进阶回答:不是,它们是同一个单例在不同完成度下的引用管理状态,必须最终收敛或完整清理。
问题(原理题):为什么第三级不能简单替换成直接存原始对象?
- 考点:延迟生成早期代理和身份一致性。
- 回答思路:说明只有真的循环时才需要早期引用。
- 详细答案:工厂把早期引用生成推迟到循环真正发生时,并给自动代理创建器机会返回早期代理;若直接存原始对象,依赖方可能拿到原对象,而容器最终返回代理,事务或安全行为分叉。二级缓存则确保工厂只执行一次,后续都拿同一早期引用。
- 进阶追问:没有代理时两级缓存够吗?
- 进阶回答:从纯对象闭环角度可以设计更简化结构,但框架统一扩展协议需要延迟介入点;回答应基于现有容器语义,而不是只算对象数量。
问题(故障题):循环对象初始化失败后缓存怎样处理?
- 考点:半对象泄漏和失败清理。
- 回答思路:强调不能让早期引用继续被正常获取。
- 详细答案:创建失败要从一级、二级、三级和正在创建集合中移除该名称,并销毁已登记的相关单例;原异常应保留依赖路径。若外部线程已经不当持有早期引用,框架无法让它变回完整对象,因此对象创建阶段不应发布自身或启动业务线程。
- 进阶追问:为什么初始化中发布事件危险?
- 进阶回答:监听方可能在对象尚未最终代理和注册前回调它,造成半初始化对象逃逸和并发可见性问题。
4.2 早期代理、最终代理与对象身份一致性
循环依赖遇到 AOP(面向切面编程)候选时,早期引用扩展必须决定是否提前返回代理。自动代理创建器记录早期代理引用,初始化后阶段若发现已经为该对象生成早期代理,应复用同一引用而不是再包一层。否则 B 持有早期对象,调用方持有最终代理,或者出现双重代理,导致事务、监控和安全行为不一致。
flowchart TD
A["原始对象 A"] --> F["三级缓存早期引用工厂"]
F --> P{"A 是否需要代理?"}
P -->|"否"| E["早期原对象 A"]
P -->|"是"| Q["早期代理 A'"]
E --> B["注入依赖对象 B"]
Q --> B
A --> I["A 完成初始化"]
I --> C{"是否已有早期代理?"}
C -->|"是"| R["复用 A' 作为最终单例"]
C -->|"否"| N["按正常规则创建代理或返回原对象"]
B -->|"若持有 A 而最终为 A'"| X["身份分叉与拦截失效"]图解:早期工厂按代理需要产生 A 或 A’,最终阶段检查是否已经提前代理;复用是正常路径,B 持有原对象而一级缓存放代理是失败路径。结论是早期暴露不仅要“有对象”,还要保持最终语义一致。
| 场景 | B 持有引用 | 一级缓存引用 | 结果 |
|---|---|---|---|
| 无代理 | 原始 A | 原始 A | 身份一致 |
| 早期正确代理 | A’ | 同一个 A’ | 拦截一致 |
| 直接暴露原对象 | 原始 A | A’ | B 的调用绕过拦截 |
| 重复创建代理 | A’ | A” | 身份、通知链或相等判断异常 |
| 多个创建器顺序冲突 | 未知包装层 | 不同包装层 | 启动异常或运行行为不稳定 |
热门面试题
问题(基础题):什么是早期代理?
- 考点:循环依赖时的提前包装。
- 回答思路:指出它不是额外业务代理,而是最终代理的提前引用。
- 详细答案:当循环依赖方需要正在创建的对象,而该对象又匹配自动代理规则时,早期引用工厂可提前返回代理。这个代理应当就是最终对外使用的同一引用,完成初始化后放入一级缓存,而不是再创建另一个代理。
- 进阶追问:所有代理都要提前创建吗?
- 进阶回答:不需要。只有真正发生早期引用获取时才走该路径,正常无循环对象仍在初始化后阶段代理。
问题(原理题):为什么早期引用和最终引用不一致会导致事务失效?
- 考点:调用是否经过代理。
- 回答思路:用 B 调 A 的路径说明。
- 详细答案:若 B 在循环处理中注入原始 A,而容器最终对外放入事务代理 A’,外部调用经过拦截,B 内部对 A 的调用却直达目标对象。同一方法因调用入口不同表现不同,审计和事务边界分裂。身份一致性要求所有依赖都持有相同最终语义引用。
- 进阶追问:比较对象引用能发现问题吗?
- 进阶回答:能作为证据之一,但还要检查代理类型、通知链和调用路径;重复代理有时引用不同但都能拦截,只是顺序仍可能错误。
问题(故障题):多个自动代理创建器为什么危险?
- 考点:基础设施冲突和包装顺序。
- 回答思路:说明候选识别、早期代理和最终代理都可能重复。
- 详细答案:多个创建器可能分别认为自己负责同一对象,在早期和最终阶段产生不同包装层;通知顺序、类型判断和缓存记录不再一致,甚至触发原始对象被注入错误。应统一代理基础设施、明确处理器顺序并避免重复开启相近能力,升级后检查实际代理链。
- 进阶追问:加顺序值就能彻底解决吗?
- 进阶回答:只能确定调用次序,不能消除职责重叠;更可靠的是合并或移除重复创建器并验证单一最终代理。
4.3 构造器循环、prototype(原型作用域)循环与异步边界
三级缓存有严格前提:单例、允许循环引用、已经实例化并允许早期暴露、同一对象工厂线程内可观察创建状态。构造器闭环在实例化前互相等待;prototype(原型作用域)没有稳定单例缓存和全局完成态;跨线程异步创建缺少同一创建链的顺序保证。它们不能被“三级缓存万能论”覆盖。
flowchart TD
A["发现依赖闭环"] --> B{"双方是否 singleton(单例作用域)?"}
B -->|"否"| P["prototype(原型作用域)循环:失败"]
B -->|"是"| C{"至少一方能先实例化再属性注入?"}
C -->|"否"| K["构造器循环:失败"]
C -->|"是"| D{"允许循环引用且同一创建协调?"}
D -->|"否"| X["配置禁止或异步边界:失败"]
D -->|"是"| E["三级缓存尝试早期引用"]
E --> F{"代理身份能一致?"}
F -->|"是"| S["完成但登记设计债务"]
F -->|"否"| Y["拒绝创建并重构"]图解:从作用域、实例化时点、配置和代理一致性逐层判断,只有全部满足才可能闭合;其余箭头都是明确失败分支。结论是“能启动”只是兼容结果,仍应审查双向依赖的业务含义。
| 循环类型 | 三级缓存可否解决 | 根本原因 | 推荐动作 |
|---|---|---|---|
| 单例属性注入 A↔B | 特定条件下可以 | 双方可先实例化 | 优先重构,临时兼容要监控 |
| 单例构造器 A↔B | 不可以 | 没有任何原始实例可暴露 | 调整依赖方向或抽取第三服务 |
| prototype(原型作用域)A↔B | 不可以 | 无稳定单例完成态和缓存 | 重新设计对象所有权 |
| 异步线程互相等待 | 不应依赖 | 创建状态与上下文跨线程 | 禁止初始化异步互等,显式编排 |
| 早期原对象与最终代理不同 | 不应接受 | 拦截语义分叉 | 统一代理或取消循环 |
热门面试题
问题(基础题):为什么构造器循环依赖解决不了?
- 考点:原始实例出现时点。
- 回答思路:说明三级工厂只有实例化后才能登记。
- 详细答案:创建 A 前必须先解析构造参数 B,创建 B 又先需要 A;此时 A、B 都没有完成实例化,容器没有原始对象可放入早期引用工厂,因此无法利用三级缓存。属性注入不同,它允许先构造空壳对象再填依赖。
- 进阶追问:使用延迟代理能绕过吗?
- 进阶回答:某些依赖可推迟真实解析,但只是把失败时点后移,并可能隐藏错误职责;应先考虑重构依赖方向。
问题(原理题):为什么 prototype(原型作用域)循环不能按单例方式处理?
- 考点:实例身份与缓存生命周期。
- 回答思路:原型每次解析产生新对象,没有稳定共享引用。
- 详细答案:单例循环依赖依靠名称对应一个最终身份,并把早期引用逐层升级;原型每次请求都可能创建新实例,若缓存早期对象会破坏原型语义,也无法确定何时完成和销毁。容器只检测当前原型创建链并报告循环,设计上应明确谁拥有谁。
- 进阶追问:把其中一个改成单例可以吗?
- 进阶回答:可能改变状态隔离和并发语义,不能只为启动而改作用域;需确认业务生命周期和线程安全。
问题(项目题):异步任务初始化时互相等待如何治理?
- 考点:启动编排、超时和死锁证据。
- 回答思路:禁止对象初始化中提交并等待依赖自身的任务。
- 详细答案:初始化阶段只登记任务能力,不在构造或回调中提交到同一受限线程池并同步等待。需要恢复的 Runner(执行器)任务在上下文就绪后由独立协调器启动,带超时、租约和幂等状态。线程栈若显示刷新线程等待任务、任务又等待未完成对象,就是阶段循环而非三级缓存问题。
- 进阶追问:增加线程池大小能解决吗?
- 进阶回答:只能偶然缓解资源等待,依赖闭环仍存在;负载变化后会再次发生,应拆开启动阶段和任务执行阶段。
4.4 源码关键方法、版本边界与重构决策
源码阅读应沿“入口、状态、扩展点、失败清理”四条线。容器刷新看 AbstractApplicationContext.refresh;单例获取看 DefaultSingletonBeanRegistry.getSingleton;对象创建看 AbstractAutowireCapableBeanFactory.doCreateBean;属性填充看 populateBean;初始化看 initializeBean;早期引用看 getEarlyBeanReference;销毁看 destroySingletons。方法名是导航,不应脱离具体版本背实现细节。
Spring Boot(快速开发框架)从 2.6 起默认禁止循环引用,2.7 延续该策略;是否允许由应用配置和底层框架能力共同决定。现代项目应把启动失败视为依赖设计审查入口。Spring(Java 应用框架) Framework(核心框架)5.3 与 6.x 在命名空间、基线版本和内部实现上存在差异,回答时需明确项目版本,不把内部字段当稳定公共契约。
flowchart TD
A["启动异常或循环报告"] --> B["确认具体版本、配置和依赖路径"]
B --> C["定位 refresh/getBean/doCreateBean 阶段"]
C --> D{"属于合法基础设施循环?"}
D -->|"否,业务双向职责"| E["抽取第三服务或上层编排"]
D -->|"通知关系"| F["改为事件或端口回调"]
D -->|"只因初始化时点"| G["评估延迟提供器并记录风险"]
D -->|"框架兼容且可证明"| H["临时开启并验证代理身份"]
H --> I["建立移除期限、启动测试和监控"]
E --> J["单向依赖图"]
F --> J
G --> J图解:先确认版本和创建阶段,再按业务语义分流;默认路径是形成单向依赖,临时兼容必须带验证和退出期限。结论是配置开关是迁移工具,不是长期架构方案。
| 关键方法/边界 | 主要职责 | 阅读时观察 | 不应作出的假设 |
|---|---|---|---|
refresh | 编排容器刷新 | 阶段顺序、异常清理 | 等同单对象生命周期 |
getBean/doGetBean | 名称转换、缓存、作用域与依赖 | 快速路径和递归创建 | 每次都创建新对象 |
getSingleton | 单例缓存与创建协调 | 三层缓存和正在创建状态 | 缓存字段是公共扩展接口 |
doCreateBean | 实例化、早期暴露、填充、初始化 | 代理一致性检查 | 所有循环都可解决 |
populateBean | 属性与依赖处理 | 候选解析和递归链 | 只是简单字段赋值 |
initializeBean | 感知与初始化处理 | 回调与代理时点 | 构造后回调已拥有最终代理 |
| Spring Boot(快速开发框架)2.6+ 默认策略 | 默认拒绝循环引用 | 精确版本和配置来源 | 底层框架完全删除能力 |
热门面试题
问题(基础题):阅读 Bean(对象实例)生命周期源码从哪些方法入手?
- 考点:入口和职责分层。
- 回答思路:从刷新到获取、创建、填充、初始化和销毁列主线。
- 详细答案:先用
refresh理解容器阶段,再沿doGetBean和单例注册表看缓存;进入doCreateBean后分别跟实例化、populateBean、initializeBean和早期引用;关闭看单例销毁。每一步同时记录输入状态、扩展点和异常清理,避免只背方法名。 - 进阶追问:为什么不直接从构造器断点开始?
- 进阶回答:构造器只能看到单对象局部,无法解释定义来源、处理器注册顺序、缓存命中和刷新失败清理。
问题(原理题):Spring Boot(快速开发框架)2.6+ 默认禁止循环引用意味着什么?
- 考点:应用策略与底层能力区分。
- 回答思路:说明默认行为改变,不等于机制不存在。
- 详细答案:应用默认把循环当设计错误并在启动时失败,促使团队重构;底层 Spring(Java 应用框架) Framework(核心框架)在特定单例属性注入场景仍有早期引用机制。是否临时开启要核对具体版本和配置,且构造器、原型或代理不一致问题仍不能因此解决。
- 进阶追问:生产事故时能否先开启开关?
- 进阶回答:可作为有回滚窗口的临时迁移措施,但要验证代理身份、列出依赖链、设置移除期限并补启动测试,不能默认为永久修复。
问题(项目题):支付服务与账务服务互相注入应怎样重构?
- 考点:领域方向、编排和可靠事件。
- 回答思路:把双向协调上移或改为事实事件。
- 详细答案:由上层支付应用服务编排支付确认与本地状态,或在支付状态和 Outbox(发件箱)同事务后发布“支付已确认”事实,账务模块幂等生成分录;账务结果以独立状态或事件反馈。双方不直接持有彼此服务对象,失败由状态机、查询和对账闭环。
- 进阶追问:同一进程也必须使用 MQ(消息队列)吗?
- 进阶回答:不必。强不变量可由上层编排和本地事务完成;需要跨边界异步时才使用可靠事件,不能为消除对象循环盲目增加分布式成本。
5. 项目排查与复习清单
5.1 启动、循环依赖与半初始化对象排查
排查顺序应从第一原因而非最外层包装异常开始:确认版本和配置,找最深创建异常,画对象依赖链,判断发生在定义、实例化、填充、初始化还是代理阶段;再检查作用域、注入方式、早期引用和代理身份。禁止通过反复增加懒加载、允许覆盖或允许循环引用来压掉异常,因为这会改变失败时点却不消除根因。
flowchart TD
A["启动或首次请求创建失败"] --> B["保存完整异常、版本、配置与条件报告"]
B --> C["定位最深 BeanCreationException(对象创建异常)"]
C --> D{"失败阶段"}
D -->|"定义"| E["查扫描、导入、覆盖和属性来源"]
D -->|"实例化"| F["查构造器、工厂方法和参数"]
D -->|"填充"| G["画候选与循环依赖链"]
D -->|"初始化"| H["查回调、远程调用和资源"]
D -->|"代理"| I["查处理器顺序、早期代理和重复包装"]
G --> J{"能否形成单向业务依赖?"}
J -->|"能"| K["重构并加启动回归"]
J -->|"暂不能"| L["受控兼容、监控和退出期限"]图解:证据采集后按创建阶段分流,循环分支优先重构;受控兼容是失败兜底而非正常目标。业务结论是排查结束必须得到单向依赖、明确降级或可验证临时措施,而不是仅让应用启动。
| 现象 | 首要证据 | 常见根因 | 修复与回归 |
|---|---|---|---|
| 定义重复 | 候选来源和条件报告 | 扫描与自动配置重叠 | 条件互斥、唯一性启动测试 |
| 构造器循环 | 最深依赖路径 | 双向必需依赖 | 抽取第三服务或上层编排 |
| 初始化超时 | 阶段耗时和线程栈 | 回调远程调用 | 有界超时、就绪后预热 |
| 部分方法无事务 | 实际引用类型和调用路径 | 原对象逃逸或自调用 | 统一代理入口,移除循环 |
| 关闭卡住 | 在途数量与销毁线程栈 | 无界等待或任务继续领取 | 拒新、截止时间、租约恢复 |
热门面试题
问题(基础题):循环依赖异常第一步看什么?
- 考点:证据与依赖链。
- 回答思路:确认最深根因、版本、作用域和注入方式。
- 详细答案:保留完整异常链,找到最深的正在创建对象和依赖路径,确认是构造器还是属性注入、单例还是原型,再核对是否默认禁止循环。先画 A 到 B 再回 A 的业务调用目的,不能看到错误就直接开启兼容开关。
- 进阶追问:异常栈太长怎么办?
- 进阶回答:按对象名称提取创建链,结合启动日志和定义来源缩小;外层异常多为包装,最深原因和首次出现的正在创建提示更关键。
问题(原理题):怎样证明发生了原对象逃逸?
- 考点:对象身份、代理类型和调用行为。
- 回答思路:比较注入引用和容器最终引用,并观察通知链。
- 详细答案:在安全诊断环境记录 B 中 A 的引用类型、身份标识和容器按名称取得的最终引用,检查是否为同一代理及其通知链;再用一条可回滚测试调用验证事务或审计是否经过拦截。还要查初始化中是否把
this注册到静态集合、监听器或线程。 - 进阶追问:生产能直接打印对象全部信息吗?
- 进阶回答:不应泄露密钥或大对象,只记录对象名称、类型、代理类别和匿名身份标识,并受诊断开关控制。
问题(项目题):如何把本篇知识用于 WMS(仓储管理系统)启动治理?
- 考点:配置校验、依赖方向、就绪和关闭。
- 回答思路:形成启动检查和发布验收清单。
- 详细答案:核心库存端口、数据库和消息发布能力启动期校验;海外仓非核心适配器可独立降级;禁止服务双向注入,状态协调交上层应用服务;记录各刷新阶段耗时;就绪前不接流量,关闭先停止领任务并排空。回归覆盖定义唯一、无循环、代理生效和资源释放。
- 进阶追问:最重要的业务验收是什么?
- 进阶回答:启动与滚动关闭期间库存守恒、任务不重复、未知外部结果可查询恢复,而不仅是进程健康。
5.2 章节收口与题库使用说明
上面的知识小节到此结束。下面的综合题库用于跨小节复述,不再作为章节级六字段题统计范围。
6. 综合口述题库
以下 28 道题用于 3 至 5 分钟完整口述。每题按结论、机制、失败边界、项目证据和验证闭环组织,并提供追问直答。
问题(综合题):请从设计思想、运行机制和项目边界完整解释 IoC(控制反转)与 DI(依赖注入)。
口述答案:结论:IoC(控制反转)不是把
new换成注解,而是把对象创建、实现选择、依赖组装、作用域和生命周期控制从业务类转移到外部组合根;DI(依赖注入)是容器把依赖传入对象的主要实现方式。约束是业务对象仍然存在语义耦合,容器只能让依赖显式和可替换,不能自动修复职责过重、共享状态或双向调用。机制上,容器先把配置、扫描和导入解析成 BeanDefinition(Bean 定义),再按构造器或工厂方法实例化,根据类型、限定标识、主候选和顺序解析依赖,完成属性填充、初始化、代理和销毁。构造器注入适合必需依赖,因为对象创建成功即满足不变量;属性注入只适合真正可选能力,否则会隐藏不完整状态。失败边界包括候选缺失、多个候选歧义、构造器循环、处理器提前创建对象以及单例保存请求级状态。项目中,WMS(仓储管理系统)订单应用服务只依赖库存、支付和履约端口,渠道实现通过集合注入形成不可变路由表;重复渠道标识在启动期失败,而不是首笔订单随机路由。验证闭环包括无容器单元测试、启动期候选唯一性测试、依赖图审查、代理生效测试以及滚动关闭资源释放。这样回答既说明“为什么”,也说明容器能力边界和工程证据。在架构评审中,我还会把每个依赖的创建者、失败策略和替换条件写进组合根清单;若业务代码仍大量主动查询容器,就判定控制权并未真正外移。
还会抽查一次不启动容器的业务测试,证明核心规则不依赖全局上下文才能运行。
- 追问 1:IoC(控制反转)等于服务定位器吗?直接回答:不等于;服务定位器由业务代码主动查询依赖,会隐藏必需依赖,DI(依赖注入)由外部显式提供。
- 追问 2:用了接口就一定解耦吗?直接回答:不一定,接口若暴露错误领域语义或调用方向仍双向,耦合只是换了形式。
- 追问 3:为什么组合根应靠近应用入口?直接回答:实现选择和环境差异集中后,业务对象不需要知道部署配置,测试也能替换依赖。
- 关联专题:IoC(控制反转)与 DI(依赖注入)主线
问题(综合题):为什么高级工程实践通常推荐构造器注入,它又有哪些不能被忽略的边界?
口述答案:结论:构造器注入的核心价值是把必需依赖纳入对象创建不变量,而不只是“官方推荐”或便于测试。机制上,容器选择构造器前会为每个参数建立依赖描述,按类型和限定规则选择候选;所有参数解析成功才调用构造器,因此对象一旦被其他代码看到,其必需能力已经齐全,依赖字段还可保持不可变。它让缺失候选、歧义候选和构造器循环在启动期暴露,也使单元测试可以直接传入替身,无需通过反射修改私有字段。边界是构造器参数过多不会因为注入方式改变而消失,它通常说明类同时承担编排、校验、持久化和外部适配,应拆职责;可选能力若放构造器,应提供有明确语义的默认实现,而不是到处判空。构造器不能执行网络探测、批量数据加载或启动线程,因为此时对象还没完成属性处理、生命周期回调和代理,异常会阻塞整个刷新。项目中,支付应用服务的支付仓储、幂等仓储和渠道路由器是必需依赖;非核心埋点使用无操作实现而非空值。验证时检查构造参数数量、依赖方向、候选唯一性、构造耗时和循环路径,并用启动失败测试证明配置错误不会拖到交易请求阶段。构造器循环是设计信号,应优先抽取上层编排或第三服务,而不是换字段注入让容器勉强启动。
落地时会统计构造耗时、参数所属业务域和最近变更共现;若一个参数变化总牵动无关用例,就以职责证据拆分,而不是换回字段注入隐藏问题。
最后还要把容器依赖图与实际运行调用图对照,防止声明单向、运行期又通过上下文反向查找。
- 追问 1:构造器参数多少算过多?直接回答:没有绝对数字,但参数持续增长且来自多个业务域时,应结合共同变化和职责审查,而不是机械设阈值。
- 追问 2:可选依赖怎样表达更清楚?直接回答:优先注入明确默认实现或提供器,并让缺席行为成为业务契约,避免裸空值扩散。
- 追问 3:字段设为不可变有什么收益?直接回答:依赖不会在运行中被替换,降低并发可见性和对象状态推理成本。
- 关联专题:注入方式对比
问题(综合题):BeanDefinition(Bean 定义)在容器中承担什么职责,注册、合并和实例化怎样衔接?
口述答案:结论:BeanDefinition(Bean 定义)是创建与管理对象的元数据配方,Bean(对象实例)才是运行时成品;把二者区分开,才能解释配置覆盖、后置处理和不同作用域。定义可由组件扫描、配置类、导入、注册器或程序化接口产生,注册时确定名称、别名和来源,保存类型、工厂方法、构造参数、属性、作用域、懒加载、初始化和销毁方法。创建前,容器会取得合并定义,把父子定义、装饰信息和默认作用域整理为本次创建使用的完整视图,然后校验类型并进入单例或其他作用域流程。BeanFactoryPostProcessor(Bean 工厂后置处理器)介入的是实例创建前的定义阶段,适合解析占位符或新增定义;它不应通过主动获取普通对象制造过早实例化。失败边界包括同名定义覆盖顺序不稳定、扫描与自动配置重复、父定义属性被误覆盖、运行中修改定义造成一部分单例使用旧配方以及类型预测与工厂产品混淆。项目中,支付渠道实现的启用条件和配置绑定应在定义阶段决定,重复
paymentClient名称在启动期报告两个来源,不能允许后注册者悄悄覆盖。验证闭环是导出候选名称、定义资源、条件评估和合并属性,执行唯一性启动测试,再核对最终实例类型与配置。源码阅读要从注册表到getMergedBeanDefinition再到createBean,不能把“定义已存在”误认为“对象已经创建”。迁移或升级时还要对比注册定义、合并定义和最终实例三份证据,确认条件配置、继承属性与产品类型没有在环境间漂移。
评审结论必须落到拆分类或保留理由,不能只记录“参数较多但暂时可接受”。
- 追问 1:一个定义一定只产生一个对象吗?直接回答:不一定,singleton(单例作用域)通常缓存一个,prototype(原型作用域)每次解析可产生新实例。
- 追问 2:定义合并会执行构造器吗?直接回答:不会,合并仍是元数据阶段,构造器在对象实例化路径中调用。
- 追问 3:为什么禁止同名覆盖更稳?直接回答:它把配置冲突变成确定启动失败,避免处理顺序和环境差异改变实际实现。
- 关联专题:BeanDefinition(Bean 定义)注册与合并
问题(综合题):请完整解释 ApplicationContext(应用上下文)刷新,并说明它为什么不能等同于单个 Bean(对象实例)生命周期。
口述答案:结论:
refresh是容器级编排,负责把环境、定义、扩展器、基础设施和必需单例组织成一个可用上下文;单个 Bean(对象实例)生命周期只是其中预实例化或按需获取时触发的局部流程。刷新先准备环境和活动状态,取得或重建 BeanFactory(Bean 工厂),注册基础设施并允许子类调整;随后执行 BeanFactoryPostProcessor(Bean 工厂后置处理器)处理元数据,再注册完整 BeanPostProcessor(Bean 后置处理器)链。之后初始化消息源、事件广播器等设施,执行子类刷新逻辑,注册监听器,最后预实例化非懒加载单例并发布完成事件。顺序的设计原因是先把配方修订完、再把实例扩展链装好,普通对象才不会漏过注入、生命周期或代理。失败边界是任一关键定义处理、对象初始化或基础设施启动异常都会取消刷新,销毁本轮已创建单例并保持上下文不可用;销毁异常只能附加,不能掩盖首个根因。懒加载和原型对象可能在刷新完成后仍未创建,所以“刷新成功”不表示所有定义都已实例化。项目中可记录各阶段耗时,把支付渠道远程探测从刷新主线程移为有界预热,并让核心库存依赖失败时拒绝就绪。验证闭环包含阶段耗时、完成事件、就绪探针、失败清理后的线程和连接数量,以及滚动发布期间是否接收了过早流量。生产验收还要故意让中间单例初始化失败,确认完成事件未发布、就绪探针未放行、已创建线程和连接归零,才能证明刷新边界真的闭合。
- 追问 1:刷新完成事件能当可靠业务消息吗?直接回答:不能,它是进程内生命周期通知,崩溃可中断,多实例也会分别收到。
- 追问 2:所有单例都在刷新时创建吗?直接回答:不是,懒加载单例、按条件未注册对象和特殊作用域对象时点不同。
- 追问 3:刷新失败为什么要统一销毁?直接回答:防止半可用上下文和线程、连接、文件句柄泄漏,并保持“成功可用或失败关闭”的边界。
- 关联专题:ApplicationContext(应用上下文)刷新
问题(综合题):BeanFactoryPostProcessor(Bean 工厂后置处理器)和 BeanPostProcessor(Bean 后置处理器)如何区分,误用会造成什么后果?
口述答案:结论:两者的分界是元数据与实例。BeanFactoryPostProcessor(Bean 工厂后置处理器)在普通对象创建前读取或修改 BeanDefinition(Bean 定义)和注册表,决定“按什么配方创建”;BeanPostProcessor(Bean 后置处理器)参与每个对象的实例化、属性、初始化和销毁过程,决定“实例怎样被加工和最终暴露”。前者常用于配置占位符、配置类解析和自定义扫描,后者支撑依赖注入注解、PostConstruct(构造后回调)和 AOP(面向切面编程)自动代理。顺序上必须先完成工厂后置处理,再注册所有实例后置处理器,最后创建普通单例,否则早期对象会使用未修订定义或漏过后续处理器。失败边界是扩展器内部随意调用
getBean,触发业务对象过早实例化;这样部分对象有审计、事务或安全代理,部分没有,形成最难排查的行为分叉。另一个风险是多个自动代理创建器重复包装,早期代理与最终代理身份不一致。项目中,幂等标记可以由实例后置处理体系识别并创建拦截代理,但真正防重复仍需唯一约束和状态机;配置解密则应在定义或属性处理阶段完成,不能把远程解密副作用散落到每个对象。验证时列出处理器顺序、对象创建时间和最终代理链,对所有候选执行拦截回归,并确认扩展器没有依赖普通业务对象。我还会建立处理器注册快照,在依赖升级前后比较类型、顺序和处理对象数量;差异未经解释就阻止发布,避免部分对象悄悄漏过拦截。
- 追问 1:哪个处理器能直接返回代理?直接回答:BeanPostProcessor(Bean 后置处理器)体系可在实例初始化后返回包装引用。
- 追问 2:工厂后置处理器能新增定义吗?直接回答:专门的注册表扩展可以在更早阶段新增定义,但仍应避免创建普通实例。
- 追问 3:只设置处理器顺序能避免重复代理吗?直接回答:不能,顺序只决定先后,还要消除职责重叠并验证最终只有一条代理链。
- 关联专题:两类后置处理器
问题(综合题):请按阶段完整讲 Bean(对象实例)生命周期,并解释每个扩展点为什么位于那个位置。
口述答案:结论:完整回答必须先区分定义阶段与实例阶段,再按实例化、早期暴露、属性填充、感知、初始化前、初始化回调、初始化后、使用和销毁展开。容器先取得合并 BeanDefinition(Bean 定义),通过构造器或工厂方法得到原始对象;单例若允许早期引用,会在属性填充前登记引用工厂。随后解析并注入依赖,因为初始化逻辑通常需要完整依赖。Aware(感知接口)回调再提供对象名称、工厂或上下文等基础设施能力;初始化前处理器触发生命周期注解,之后执行 InitializingBean(初始化接口)和自定义初始化方法;初始化后处理器可返回 AOP(面向切面编程)代理,最终引用才进入完整单例缓存。关闭时按依赖关系执行销毁前处理、PreDestroy(销毁前回调)、DisposableBean(销毁接口)和自定义销毁。设计原因是元数据必须先稳定、依赖必须在业务初始化前齐全、代理必须包住已经初始化的目标,而销毁要先停止依赖者再释放被依赖资源。失败边界包括构造器远程调用、初始化中发布自身、回调调用自身事务方法、异常被吞后半可用以及销毁无限等待。项目中支付客户端初始化只做本地配置校验和有界资源构建,非核心远程探测放就绪后;关闭先摘流量、排空在途、记录未知结果再关连接池。验证使用生命周期日志、代理类型、资源计数和失败注入,而不是只看回调是否打印。
为了证明顺序而不是背诵,我会对一个普通对象和一个代理对象记录阶段事件,注入失败与初始化失败分别演练,再核对最终缓存和销毁注册。
- 追问 1:实例化和初始化之间有什么阶段?直接回答:还包括早期引用登记、实例化后处理、属性填充和 Aware(感知接口)回调。
- 追问 2:构造后回调一定早于初始化接口吗?直接回答:常见处理链中由初始化前处理触发,因此在初始化接口和自定义初始化方法之前。
- 追问 3:最终 Bean(对象实例)一定是原始对象吗?直接回答:不一定,初始化后处理可返回代理或包装对象。
- 追问 4:原型对象谁销毁?直接回答:容器通常不完整跟踪其终止时点,持有资源时由调用方显式释放。
- 关联专题:完整生命周期与扩展点
问题(综合题):实例化和初始化为什么必须区分,混淆二者会造成哪些线上误判?
口述答案:结论:实例化只表示通过构造器或工厂方法得到原始对象,初始化表示依赖已经填充并执行约定回调、校验和包装;两者之间存在大量决定对象能否安全使用的步骤。机制上,
doCreateBean先选择实例化策略,单例可能登记早期引用工厂,再由populateBean解析依赖和写入属性,之后initializeBean执行 Aware(感知接口)、初始化前后处理和初始化方法。最终对象还可能被替换成代理。因此在构造器中访问属性注入依赖会得到未完成状态,在构造后回调中调用自身事务方法也可能因最终代理尚未形成且属于内部调用而失效。循环依赖处理暴露的是“已实例化但未完整初始化”的早期引用,它只能用于闭合对象图,不能被业务线程当成就绪对象。失败边界包括初始化抛异常后早期对象已经注册到静态集合、事件监听器或线程,造成半对象逃逸;监控若只统计构造器成功会误报组件健康。项目中,海外仓客户端构造器只保存不可变配置,初始化阶段校验签名算法和超时,最终代理完成后才由路由器对外提供;若校验失败,渠道保持不可用而不接单。验证应分别记录构造、注入、初始化和代理完成时间,故障注入校验失败后检查单例缓存、线程和外部注册都已清理,并通过真实代理调用验证拦截链。代码审查还要禁止构造器和初始化回调把自身放入静态集合、注册外部回调或启动业务线程,从源头避免半初始化对象逃逸到容器控制之外。
- 追问 1:早期引用可以执行业务吗?直接回答:不应,它可能缺少依赖、初始化状态或最终代理,仅是容器内部兼容机制。
- 追问 2:初始化方法适合建表吗?直接回答:通常不适合把高风险数据库变更放应用对象初始化,应使用可审计迁移流程。
- 追问 3:构造器成功能否上报健康?直接回答:不能,至少要等必需依赖、初始化和运行设施完成,并由就绪状态统一决定。
- 关联专题:实例化与初始化边界
问题(综合题):依赖解析是怎样完成的,多候选、可选依赖和集合注入分别有哪些工程风险?
口述答案:结论:依赖解析不是按类型随便取一个对象,而是将注入点建模为依赖描述,结合类型、泛型、限定标识、主候选、优先级、名称和可选语义筛选,并递归获取候选。单值必需依赖没有候选就失败,多个等价候选仍无法消歧也失败;可选包装允许缺席,集合或映射会收集全部候选并按规则排序。解析候选可能触发对象创建,因此它也是循环依赖和启动耗时的重要入口;容器还会登记依赖关系,供创建顺序、销毁顺序和诊断使用。工程边界是主候选只适合表达全局默认,不能替代不同业务点的语义选择;可选依赖若用于库存校验等核心能力,会把配置错误变成静默绕过;集合注入若无稳定顺序和唯一业务标识,会让升级后处理链变化。项目中,海外仓适配器以渠道码和能力声明组成集合,路由器初始化时校验渠道码唯一、配置完整和排序稳定,再生成不可变映射;不以实现类名路由。验证闭环包括无候选、重复候选、顺序冲突和禁用渠道四组启动测试,打印候选来源而非对象敏感内容,并对真实订单验证持久化渠道码可稳定回放。若解析链触发循环,应回到业务依赖方向审查,不能用可选包装或延迟查找把错误推迟到运行期。
对于集合策略,还要把业务标识唯一、顺序稳定和空集合语义做成启动断言;这样新增实现导致的行为变化会在发布前暴露,而不是在线上随机出现。
对历史订单还要按已保存渠道标识重放,证明新增实现不会改变旧业务的路由结果。
- 追问 1:主候选与限定标识谁更具体?直接回答:限定标识表达注入点明确语义,主候选只在没有更具体限定时提供默认选择。
- 追问 2:集合为空是否一定失败?直接回答:取决于注入契约;核心处理链应显式校验非空,不能依赖容器默认行为。
- 追问 3:延迟提供器的主要风险是什么?直接回答:把候选缺失和创建异常推到业务请求阶段,并可能增加每次查找成本和隐式依赖。
- 关联专题:依赖解析与候选选择
问题(综合题):FactoryBean(工厂对象)、普通 Bean(对象实例)和工厂方法应如何区分?
口述答案:结论:普通 Bean(对象实例)是容器直接管理的对象;工厂方法是一种实例化配方;FactoryBean(工厂对象)则是容器识别的特殊对象协议,它自己被管理,同时通过
getObject生产另一个产品。机制上,静态或实例工厂方法记录在 BeanDefinition(Bean 定义)中,创建时调用指定方法得到当前定义的实例;FactoryBean(工厂对象)先按普通生命周期创建工厂,按正常名称获取时容器通常返回其产品,显式解引用时才返回工厂本身,并根据产品单例契约决定是否缓存产品。类型预测、按类型查找和懒加载都可能同时涉及工厂类型与产品类型,因此排查“对象类型不对”时必须先确认请求的是哪一层。失败边界包括误以为工厂单例则产品必单例、产品每次新建导致连接池爆炸、getObject执行远程副作用、产品类型声明不准确影响候选解析,以及工厂自身依赖产品形成隐蔽循环。项目中可用 FactoryBean(工厂对象)封装复杂第三方客户端代理,但应让产品为线程安全单例、配置不可变、资源由工厂销毁链关闭;请求级参数绝不能保存在产品共享字段。验证时分别统计工厂实例数、产品实例数、连接与线程资源,测试正常名称和解引用语义,并在重复获取、并发获取和初始化失败场景下确认缓存与清理符合契约。容量验收应连续并发获取产品,核对工厂数、产品数、连接池数和销毁次数;只有资源数量符合单例契约,才说明没有把复杂创建变成隐蔽泄漏。
此外要用同一业务键重复启动和重放,确认集合排序变化不会改变已存在订单的处理实现。
- 追问 1:工厂方法一定需要 FactoryBean(工厂对象)吗?直接回答:不需要,配置类中的普通工厂方法已经能表达多数第三方对象创建。
- 追问 2:产品的销毁由谁负责?直接回答:取决于容器和工厂契约,持有资源时应明确注册销毁或由工厂统一关闭,不能凭名称推断。
- 追问 3:为什么类型预测重要?直接回答:容器在实例尚未创建时需要完成候选筛选和处理器判断,错误类型信息会影响注入和代理。
- 关联专题:FactoryBean(工厂对象)与实例化策略
问题(综合题):singleton(单例作用域)为什么不等于线程安全,如何设计安全的单例业务服务?
口述答案:结论:singleton(单例作用域)只说明同一 BeanFactory(Bean 工厂)内同名定义通常缓存一个共享实例,不说明对象字段访问具备互斥、可见性或原子性;多个上下文还可各有一个实例。因为单例被所有请求线程共享,只要保存订单号、临时集合、计数流程或可变格式器,就可能发生串单和竞态。安全设计首先让服务无状态:请求数据通过参数和不可变命令传递,方法局部变量不跨请求共享;依赖在构造后保持稳定,共享客户端必须由其文档保证并发安全。其次,跨实例正确性不能靠对象锁,库存扣减要用数据库条件更新、唯一约束或权威状态机;本地缓存和计数器要明确一致性与原子操作语义。最后,代理、拦截器和回调也属于共享对象,不能只审查服务类。项目中若履约服务把 currentOrderId放成员字段,线程 A 写 A100 后暂停,线程 B 写 B200,A 恢复就可能生成错误面单;改为参数传递并以订单号建立幂等约束后,问题才真正消失。验证闭环是并发测试、竞态检测、代码扫描共享字段、压测下业务守恒和多实例测试;不能因为单机压测没复现就认定安全。用同步方法包住全部逻辑会降低吞吐且无法覆盖多实例共享数据库,通常不是首选。
我还会在至少两个应用实例下重复并发用例,因为单进程对象锁可能让测试通过,却无法约束另一实例或数据库旁路写;最终仍以业务守恒为准。
多租户压测还必须使用相同业务键交叉访问,确认共享缓存和成员状态没有串租户。
- 追问 1:不可变依赖一定线程安全吗?直接回答:依赖引用不变只减少一种风险,被引用对象内部仍需具备并发安全契约。
- 追问 2:方法局部变量绝对安全吗?直接回答:变量引用局部,但若指向共享可变对象或逃逸到异步线程,仍可能竞态。
- 追问 3:单例对象锁能防库存超卖吗?直接回答:只能限制当前进程,无法覆盖多实例和旁路写,必须在权威存储建立原子约束。
- 关联专题:singleton(单例作用域)与线程安全
- 问题(综合题):prototype(原型作用域)的创建、注入与销毁有哪些容易误解的地方?
口述答案:结论:prototype(原型作用域)表示每次向容器解析该定义通常创建一个新实例,不表示每次调用持有者的方法都自动新建,也不表示容器会完整管理实例终止。单例对象创建时若直接注入一个原型依赖,注入只发生一次,之后单例从缓存复用,字段里仍是当时的同一个原型;若确需按请求或按任务新建,应通过提供器或作用域代理在使用时解析,并明确创建成本。容器会为原型执行实例化、依赖注入和初始化,但通常不跟踪每个实例何时不再使用,因此连接、文件和线程等资源必须由调用方通过显式关闭协议释放。线程安全方面,实例分离只隔离对象内部状态,共享数据库、静态变量和外部客户端仍可能竞态;把原本应无状态的服务改为原型还会增加内存和初始化成本。原型循环依赖不能照搬三级缓存,因为没有“名称对应一个最终共享身份”的稳定目标,缓存早期对象反而破坏每次新建语义。项目中批量导出任务可为每个任务创建短生命周期上下文对象,但底层连接池保持线程安全单例;任务完成后显式释放临时文件,而不是等待容器关闭。验证包括连续解析身份不同、单例持有行为、资源关闭计数、异常中断清理和并发任务隔离,才能证明作用域选择正确。
设计评审必须同时写清谁创建、谁关闭、异常中断时谁回收;只写作用域而没有所有权,原型对象越多,临时文件、连接和线程越容易泄漏。
共享对象若包含缓存,还要验证过期、更新和异常回退时不会把一个租户的数据返回给另一个租户。
- 追问 1:原型对象可以依赖单例吗?直接回答:可以,多个原型可共享线程安全或无状态单例依赖,这是常见方向。
- 追问 2:单例怎样每次获得新原型?直接回答:注入提供器或作用域代理,在业务边界按需解析,并承担相应释放责任。
- 追问 3:原型能解决内存泄漏吗?直接回答:不能;若调用方长期持有或不关闭资源,原型反而更容易累积。
- 关联专题:prototype(原型作用域)边界
- 问题(综合题):懒加载应怎样决策,为什么它不能作为启动失败和循环依赖的通用修复?
口述答案:结论:懒加载只是把对象创建从容器刷新期推迟到第一次解析,换取较短启动时间,同时承担首请求延迟和错误后移;它不是错误恢复或架构解耦。适合懒加载的是低频、非核心、初始化成本可控且有明确降级的能力,例如偶尔使用的报表适配器。库存仓储、支付幂等和核心安全组件应在启动期校验,因为缺失时根本不能守住业务不变量。机制上,懒对象仍注册 BeanDefinition(Bean 定义),第一次注入解析或方法调用才进入创建、依赖和初始化流程;若存在候选歧义、配置错误或构造器循环,问题只会在用户流量中出现。使用延迟代理暂时打断某些初始化闭环,也会让依赖方向和代理语义更难理解,不能替代抽取第三服务或上层编排。项目中报表客户端初始化 4.8s,全局懒加载让首个 3s超时请求必然失败并触发重试;更合理的是就绪后受控预热,预热前返回明确状态,核心交易路径不受影响。验证要比较启动耗时、首请求分位、初始化失败率、预热完成水位和降级命中,执行冷实例压测而不是只测热实例。若临时用懒加载缓解循环,必须记录依赖链、代理身份、退出期限和回归测试,并在后续重构中移除。
灰度时应同时观测冷启动首请求、预热水位和错误暴露时点,若只是把启动失败变成用户超时,启动数字虽好看也应回退该策略。
异常测试必须覆盖创建成功但业务失败的情况,确保调用方仍会执行资源关闭,而非只处理构造异常。
- 追问 1:全局懒加载能否减少内存?直接回答:可能减少未使用对象,但运行后仍会创建,且错误后移和首请求抖动通常更重要。
- 追问 2:懒对象何时创建?直接回答:首次被实际解析或代理委托到目标时,具体取决于注入和作用域方式。
- 追问 3:如何避免首请求冷启动?直接回答:在实例就绪后受控预热、限制并发并暴露准备状态,不能让用户请求无提示承担初始化。
- 关联专题:懒加载与失败策略
- 问题(综合题):如何设计 Bean(对象实例)的初始化失败策略与优雅销毁,使滚动发布不破坏业务?
口述答案:结论:初始化和销毁都要按业务核心性、资源所有权和可恢复状态设计,不能简单理解为“启动时连一下、关闭时关一下”。初始化先做本地配置、密钥格式、候选唯一性和必要资源校验;核心库存、支付账务或数据库能力失败时应阻止上下文就绪,因为继续服务会破坏不变量。非核心渠道可显式降级,但必须从路由中摘除、暴露健康和告警,并有受控恢复。远程探测要有严格超时,不能在多个构造器中无界并发。关闭顺序是先把就绪探针置为不可接流量,停止领取 Runner(执行器)任务和新消息,再给在途请求有界排空窗口;外部调用结果未知时保存业务键和状态,交查询或对账恢复,随后按依赖顺序关闭线程池、连接池和文件资源。销毁回调必须幂等,单个资源失败不能阻止其他资源释放,强杀场景还要依赖持久状态、租约和幂等恢复,不能迷信回调一定执行。项目中滚动关闭前有 12 个履约请求,20s 内完成 11 个,剩余一个进入未知状态并由原订单号查单,避免客户端换键重试重复下单。验证闭环包括启动失败注入、流量摘除时序、在途数量、资源线程残留、租约接管和业务对账,最终指标是订单、库存和资金守恒,而不是进程退出码漂亮。
发布演练还要覆盖平台在宽限期结束后强制终止的路径,确认租约、幂等键和持久状态能让新实例接管,而非依赖销毁回调侥幸执行。
预热失败也要按核心性决定是否阻止路由,并保留下一次可控重试时间,不能无限后台循环。
- 追问 1:初始化异常可以吞掉吗?直接回答:只有明确可选能力有降级实现和健康隔离时才可转换,核心异常必须保留并失败。
- 追问 2:销毁等待多久合适?直接回答:由请求截止时间、平台终止宽限期和恢复机制共同决定,必须有上限。
- 追问 3:原型资源如何关闭?直接回答:调用方用显式关闭协议或资源作用域管理,不能等容器统一销毁。
- 关联专题:初始化失败与优雅销毁
- 问题(综合题):Aware(感知接口)有什么价值,为什么在领域代码中过度使用会成为设计问题?
口述答案:结论:Aware(感知接口)是容器在属性填充后向对象回传名称、工厂、上下文或资源加载器等基础设施能力的显式协议,适合框架组件和确需容器协作的适配器;普通领域服务大量使用会把业务逻辑绑定到运行容器。机制上,容器在初始化前识别相关接口并注入基础设施,然后才继续初始化前处理和回调,因此对象可在初始化时使用已提供能力。但一旦业务类持有 ApplicationContext(应用上下文)并随处按名称查对象,必需依赖从构造器消失,候选错误推到运行期,单元测试必须启动容器,调用方向也容易变成服务定位器。通过上下文获取自身代理更会掩盖内部调用和职责问题。合理边界是基础设施组件可感知资源、事件或名称,领域对象只依赖业务端口;若需要发布领域事实,注入明确事件端口,而不是直接操纵整个上下文。项目中支付渠道路由器可以在初始化阶段获得配置资源,但支付规则对象不应动态查渠道对象;渠道集合应由构造器注入并校验。验证时扫描上下文持有者、记录动态查找名称、执行无容器单元测试,并审查每个感知接口是否有更窄端口可替代。确需动态插件时也要封装在单一基础设施注册器中,提供类型安全、版本和失败策略,不让容器 API(应用程序接口)扩散到业务层。
如果确需感知容器能力,应把使用点集中在基础设施适配层,并为无容器测试提供窄接口替身;一旦领域规则直接依赖上下文,就安排收敛重构。
资源释放结果还应进入发布观测,若旧实例线程数长期不归零,就暂停继续滚动并排查。
- 追问 1:Aware(感知接口)回调在属性注入之前吗?直接回答:通常在属性填充之后、初始化处理阶段执行,因此普通依赖应已注入。
- 追问 2:通过上下文取对象一定错误吗?直接回答:框架基础设施或动态插件边界可合理使用,但普通业务服务应优先显式依赖。
- 追问 3:事件发布器是否也会造成容器耦合?直接回答:会有基础设施耦合,可用领域事件端口隔离;进程内事件也不等于可靠消息。
- 关联专题:Aware(感知接口)与生命周期
- 问题(综合题):请用 A、B 两个对象逐时刻讲清三级缓存如何解决单例属性循环依赖。
口述答案:结论:三级缓存解决的是满足特定前提的单例属性注入闭环,本质是先实例化、后注入,并用早期引用临时闭合对象图,最终仍要收敛到完整单例。T0 三层缓存为空,容器请求 A;T1 通过构造器得到原始 A,但尚未填充属性,容器把能够返回 A 早期引用的工厂放入 singletonFactories(三级缓存)。T2 A 需要 B,于是实例化原始 B,也登记 B 的早期工厂;T3 B 填充属性时再次请求 A,singletonObjects(一级缓存)没有完整 A,earlySingletonObjects(二级缓存)也没有,于是调用三级工厂。若 A 需要代理,工厂通过早期扩展得到 A’;结果移入二级并删除 A 的三级条目,确保后续请求都拿同一引用。T4 B 注入 A’后完成初始化,进入一级并清除自己的早期条目;T5 B 被注入 A,A 完成初始化,若已经有早期代理就复用 A’作为最终引用,放入一级并清空 A 的二三级条目。失败边界包括 A 构造器尚未完成、任一方是 prototype(原型作用域)、配置禁止循环、初始化异常和早期原对象与最终代理不一致。项目上即使容器能启动,也应审查双向职责;支付与账务互调应改上层编排或事实事件。验证要记录每一时刻缓存状态、最终对象身份、代理通知链和失败清理,证明没有半对象残留。
我会把这五个时刻做成诊断表,并在初始化异常处断开流程,确认 A、B 对应的早期条目、正在创建标记和外部注册全部清除。
- 追问 1:二级缓存为什么必要?直接回答:它保存三级工厂已经生成的唯一早期引用,避免多次调用工厂得到不同包装对象。
- 追问 2:一级缓存何时写入?直接回答:对象完整初始化并通过一致性检查后才写入,不能把半对象当成完整单例。
- 追问 3:失败后早期缓存怎么办?直接回答:必须按对象名称从三层缓存和正在创建集合清除,并销毁相关已创建单例。
- 关联专题:三级缓存全过程
- 问题(综合题):为什么三级缓存不是“为了多存一份对象”,第三级的真正价值是什么?
口述答案:结论:第三级保存的不是第三份对象,而是延迟生成早期引用的工厂;它把“是否需要提前暴露以及暴露原对象还是代理”的决定推迟到循环真正发生时。若没有循环,普通对象按完整生命周期在初始化后判断是否代理,不需要早期引用;若 B 在 A 尚未完成时请求 A,容器才调用工厂,让自动代理创建器执行 getEarlyBeanReference并可能返回 A’。结果立即移到二级,后续循环请求复用同一引用,最终初始化阶段也必须复用 A’。若一开始直接把原始 A 放二级,B 会持有原对象,而容器最终可能把事务、安全或监控代理 A’放入一级;同一单例出现两种调用语义,B 对 A 的调用绕过拦截。若每次都调用工厂又不设二级,可能重复包装得到 A’、A”,对象身份和通知顺序同样不稳定。因此三级负责延迟决策,二级负责已决策引用去重,一级负责完整对象共享。失败边界是多个自动代理创建器都试图包装、处理器顺序变化、原始对象在初始化中自行逃逸,以及某些复杂代理无法接受早期暴露。工程结论不是“缓存越多越高级”,而是把对象完成度和扩展协议显式建模。验证应比较依赖方引用和容器最终引用是否同一,检查通知链只出现一次,并在关闭或创建失败后确认三层条目均清除。
判断实现是否正确不能只看缓存命中,还要验证工厂仅调用一次、二级引用稳定、最终一级身份相同,以及失败后不存在任何可再次取得的半对象。
- 追问 1:没有 AOP(面向切面编程)时两级能否实现闭环?直接回答:理论上可更简化,但现有统一扩展协议需要延迟工厂,面试应解释框架设计而非只讨论最小数据结构。
- 追问 2:三级工厂什么时候被调用?直接回答:只有完整和早期对象都未命中,且允许早期引用时才调用。
- 追问 3:工厂调用后为何删除三级条目?直接回答:引用已经确定并进入二级,继续保留会增加重复生成和状态歧义。
- 关联专题:三级缓存职责
- 问题(综合题):构造器循环依赖为什么无法由三级缓存解决,遇到后应如何重构?
口述答案:结论:三级缓存的必要前提是至少一方先完成实例化,才能登记早期引用工厂;构造器循环在任何原始对象出现前就互相要求对方,因此没有可缓存内容。创建 A 时,容器先解析 A 的构造参数 B;创建 B 又先解析 B 的构造参数 A,此时 A 的构造器尚未调用,既不存在原始 A,也没有早期工厂,所以查三层缓存都不能得到对象,只能报告正在创建闭环。把其中一方改字段注入可能让框架兼容启动,却把必需依赖变成半初始化状态,并没有解释为什么两个服务互相负责。重构应先画出调用目的:若 A、B共同完成一个用例,由更上层应用服务依次调用;若两者共享规则,抽取第三个领域服务;若一方只是通知另一方,改为明确事件端口;若只是读取数据,注入更窄查询接口而不是完整服务。延迟提供器可用于真实的昂贵可选依赖或框架扩展,但不应成为默认逃生口。项目中支付确认和账务记账若互相构造依赖,可由支付应用服务写支付状态及 Outbox(发件箱),账务模块幂等消费并对账,不形成对象双向调用。验证闭环包括依赖图无环、构造器启动测试、业务状态机和失败补偿测试,以及删除临时循环开关后仍能启动。这样既解决对象创建,也解决事务和责任方向。
重构验收会删除临时延迟和循环配置,再用真实用例验证调用顺序、事务和补偿;若删除后仍无法启动,说明业务方向尚未真正拆开。
新事件链路要演练发布失败和重复消费,确保对象解耦没有牺牲原来的业务不变量。
- 追问 1:把一个依赖标为懒加载是否就解决?直接回答:只可能推迟真实解析,职责闭环仍在,并可能把失败移到请求阶段。
- 追问 2:上层编排会不会变成上帝类?直接回答:应用服务只编排单个用例和事务边界,领域规则仍由各能力对象承担,并需控制依赖数量。
- 追问 3:事件解耦一定需要 MQ(消息队列)吗?直接回答:同进程可先用显式事件端口;跨边界可靠传播再使用持久事件机制。
- 关联专题:构造器循环边界
- 问题(综合题):为什么 prototype(原型作用域)循环依赖不能复用单例三级缓存机制?
口述答案:结论:单例三级缓存依赖“一个名称在一个对象工厂中最终收敛为一个共享身份”,prototype(原型作用域)却要求每次解析产生新的实例,没有稳定的全局最终引用,因此不能把某次创建中的半对象缓存为后续所有请求共用。创建原型 A 时解析原型 B,B 又解析 A,容器只能在当前创建链中发现名称重复;若强行放入类似二级缓存,下一次独立请求可能错误复用上一次早期 A,破坏原型语义,也无法确定什么时候移除和销毁。原型对象的终止时点通常由调用方掌握,容器不会像单例那样在关闭时统一跟踪每个实例,这进一步使全局早期缓存不可行。把其中一个对象改成单例也不是机械修复,因为它会改变状态共享、线程安全和资源生命周期;必须先判断哪一方真正拥有另一方。项目中每个异步导出任务可使用独立任务上下文对象,但任务上下文不应反向依赖创建它的任务工厂;工厂创建并交给执行器,任务只依赖窄服务端口,完成后显式清理临时文件。失败边界还包括原型实例被单例直接注入后实际上长期复用,以及原型内部启动线程导致调用方无法正确关闭。验证要连续获取并确认身份隔离、异常路径资源清理、并发任务数据不串扰,并用依赖图证明没有当前创建链回环。
同时要检查单例持有者是否反向保存原型引用;一旦回调或缓存把短生命周期对象长期留住,作用域隔离和销毁责任都会再次失真。
若重构引入事件,还要补可靠发布、幂等消费和对账,不能用异步把原有强不变量悄悄削弱。
- 追问 1:原型依赖单例是否安全?直接回答:方向本身常见,但单例依赖必须线程安全或无状态,外部共享资源仍需正确约束。
- 追问 2:原型为什么不自动执行完整销毁?直接回答:容器不知道每个实例何时不再使用,调用方拥有实际终止时点。
- 追问 3:作用域代理能解决原型循环吗?直接回答:可能延迟目标获取,但不应拿来掩盖所有权闭环,仍要验证运行期不会重新递归。
- 关联专题:prototype(原型作用域)循环
- 问题(综合题):早期代理和最终代理如何保持身份一致,为什么这会影响事务与安全?
口述答案:结论:发生循环依赖且目标对象需要 AOP(面向切面编程)时,依赖方拿到的早期引用必须与对象完成后进入一级缓存的最终引用具有同一代理身份和拦截语义。机制上,三级工厂调用自动代理创建器的早期引用扩展,若 A 匹配通知器就生成 A’并记录;B 注入 A’。A 完成属性和初始化后,初始化后处理阶段发现该对象已有早期代理,应直接把 A’作为最终暴露引用,而不是返回原始 A 或再创建 A”。若 B 持有原始 A、外部持有事务代理 A’,B 对 A 的调用不经过事务拦截;若一级保存 A”,早期和最终代理的通知顺序、对象相等、缓存键和类型判断都可能不同。安全方法授权、审计和监控同样会出现入口差异,这比普通空指针更隐蔽。失败边界包括多个自动代理创建器职责重叠、某处理器在早期和最终阶段判断条件不同、初始化中把 this发布到静态集合,以及代理类型受接口或最终方法限制。项目中若库存服务通过循环注入拿到原始支付服务,而外部请求拿到幂等和事务代理,内部补偿调用可能重复入账。验证应比较依赖方与容器最终引用的匿名身份、代理类别和通知链,执行事务回滚、安全拒绝和审计计数回归,并通过重构移除业务循环,而不只接受启动成功。
上线前会用只触发一次的事务同步、审计计数和安全拒绝用例校验通知链,任何入口结果不同都视为身份分叉,而不是可接受差异。
容量测试还应比较原型创建速率和垃圾回收压力,确认隔离收益覆盖频繁创建成本。
- 追问 1:早期代理是另一个代理层吗?直接回答:不应是额外层,而应是最终代理的提前暴露引用。
- 追问 2:没有事务注解还要关心身份吗?直接回答:要,安全、缓存、审计、监控和对象相等语义都可能依赖最终包装。
- 追问 3:如何发现双重代理?直接回答:检查代理目标和通知链层级,比较实际引用,并执行只应触发一次的拦截计数测试。
- 关联专题:早期代理一致性
- 问题(综合题):多个自动代理创建器或 BeanPostProcessor(Bean 后置处理器)顺序冲突会怎样排查?
口述答案:结论:处理器顺序冲突不是简单的“谁先执行”问题,还可能改变候选识别、早期引用、最终包装和通知链,从而产生重复代理或部分对象漏处理。排查先保存具体框架版本、已注册 BeanPostProcessor(Bean 后置处理器)类型和顺序,找出哪些处理器具备自动代理能力;再对故障对象记录原始类型、早期引用类型、最终代理类型、目标对象和通知器列表。若处理器在注册完成前通过 getBean提前创建对象,该对象会错过后续处理器;若两个创建器都包装同一对象,可能形成 A”包 A’,通知执行两次或顺序与预期相反;循环依赖时一个创建器产生早期 A’,另一个在初始化后产生 A”,还会触发身份不一致。修复优先统一基础设施,只保留一套负责自动代理的机制,把业务通知器挂到同一代理链;顺序值只用于确有先后语义的通知,不用于掩盖重复职责。项目中幂等、事务和审计同时作用支付回调,应明确幂等判定、事务边界和审计记录的次序,并用同一代理基础设施组合,而不是三个自定义处理器各自包一层。验证闭环包括每个通知只执行一次、异常路径顺序、循环对象身份、无代理候选和重启后顺序稳定,还要比较升级前后注册列表,防止依赖升级偷偷增加创建器。
修复后还应在无循环对象、循环候选和初始化失败三条路径上比较代理层数,防止只修正常路径,早期引用路径仍生成另一套包装。
对于安全拦截,必须验证内部依赖调用也被拒绝,不能只测从控制器进入的外部路径。
- 追问 1:处理器有顺序接口就一定稳定吗?直接回答:只能稳定同一容器中的排序,不能保证职责不重叠或提前实例化不发生。
- 追问 2:双重代理一定错误吗?直接回答:框架可能合法嵌套,但必须证明通知顺序、类型和早期引用一致;无意重复通常应消除。
- 追问 3:生产如何安全取证?直接回答:在受控诊断下记录处理器类型、代理类别和通知器摘要,不打印密钥或完整对象。
- 关联专题:对象后置处理器与代理
- 问题(综合题):Spring Boot(快速开发框架)2.6+ 默认循环引用边界应怎样回答,临时开启是否合理?
口述答案:结论:Spring Boot(快速开发框架)从 2.6起默认禁止循环引用,是应用层默认策略收紧,目的是把双向依赖在启动期暴露;它不表示底层 Spring(Java 应用框架) Framework(核心框架)的早期单例引用机制被完全删除,也不让构造器或原型循环突然可解。回答时必须给出具体版本和配置来源,区分框架能力、应用默认值与项目自定义。升级后启动失败,先提取依赖链,确认注入方式、作用域和代理,再按业务语义重构:共同用例上移编排,共享规则抽第三服务,通知关系改事件,读取关系缩窄为查询端口。临时开启兼容只适用于发布窗口受限、已证明是单例属性循环、代理身份一致且有明确回退计划的场景;它必须伴随风险登记、负责人、移除期限、启动回归和运行监控。不能把开关当永久修复,因为后续增加事务、安全或异步处理器后,原本“能跑”的早期引用也可能失去一致性。项目中若旧 WMS(仓储管理系统)库存服务与波次服务循环,可先在紧急升级中受控兼容,但同时建立上层履约编排接口,在下个迭代移除双向注入。验证包括关闭开关后的目标版本启动测试、依赖图无环、关键代理生效、滚动发布和业务守恒。回答“默认禁止”时还应避免把未来版本永久写死,实际实施必须核对项目依赖树和对应官方版本文档。
决策记录需要写清采用该开关的业务损失、回退窗口和替代设计,并在看板上设到期条件;否则临时措施会自然沉淀成永久依赖。
还要检查对象类型判断和序列化行为,因为多层代理可能让依赖按具体类匹配或缓存键生成失败。
- 追问 1:开关开启后构造器循环能解决吗?直接回答:不能,因为双方都尚未实例化,没有早期引用可用。
- 追问 2:为什么升级前能启动、升级后失败?直接回答:通常是应用默认策略改变,把既有设计债务显式化,不代表依赖链刚刚产生。
- 追问 3:临时兼容最少要验证什么?直接回答:作用域、注入方式、最终代理身份、启动回归、业务调用行为和明确移除期限。
- 关联专题:版本边界与重构决策
- 问题(综合题):如何沿源码解释
getBean、getSingleton与doCreateBean的协作,而不是只背方法名?
口述答案:结论:源码主线应围绕请求入口、对象状态、扩展点和失败清理建立,而不是背调用栈。getBean入口先处理名称、父工厂和类型需求,doGetBean优先从单例注册表取缓存;命中完整单例是快速路径,创建中的对象在允许时才可能取早期引用。未命中后取得合并 BeanDefinition(Bean 定义),处理显式依赖和作用域;单例创建通过 getSingleton的创建回调协调“正在创建”状态、并发与异常清理,真正对象过程进入 createBean和 doCreateBean。后者完成实例化,必要时登记早期引用工厂,再执行 populateBean依赖填充和 initializeBean感知、回调与代理,最后检查早期引用是否与最终对象一致。成功后注册完整单例及销毁适配器;失败则清除缓存和创建标记,并沿依赖链包装根因。设计思想是模板方法编排稳定骨架,把实例化、属性、初始化和代理开放为扩展点,同时由单例注册表集中管理身份。阅读时应在具体版本源码中观察状态变化和条件,不把内部字段当公共 API(应用程序接口)。项目排障可在这些边界记录阶段耗时,快速判断是构造器、依赖解析、初始化远程调用还是代理冲突。验证用一条普通单例、一条懒对象、一条属性循环和一条初始化失败路径对照跟踪,才能把源码与行为对应。
调试时还会分别观察普通命中、创建中早期命中和完全未命中三条分支,记录对象名称与状态变化,从而把源码条件和实际异常逐项对应。
- 追问 1:为什么先查缓存?直接回答:保证单例复用并提供创建中早期引用入口,避免每次都进入昂贵创建流程。
- 追问 2:谁负责正在创建标记?直接回答:单例注册表围绕创建回调维护,供循环检测和异常清理使用。
- 追问 3:
doCreateBean完成后一定注册单例吗?直接回答:还取决于作用域与外层单例流程;原型创建不会进入同样的完整单例缓存。 - 关联专题:源码关键方法
- 问题(综合题):
populateBean和initializeBean分别解决什么问题,为什么排障时必须分开?
口述答案:结论:populateBean负责把原始对象变成“依赖已填充”的对象,initializeBean负责执行容器感知、生命周期初始化和最终包装;两者失败原因、对象完成度和修复方向不同。属性阶段先给实例化后处理器否决或调整机会,再处理注入注解和定义属性,为每个注入点解析候选并递归获取依赖,因此候选缺失、多候选歧义、类型转换和循环依赖主要在此暴露。初始化阶段先执行 Aware(感知接口),再由 BeanPostProcessor(Bean 后置处理器)初始化前处理触发生命周期注解,之后调用 InitializingBean(初始化接口)和自定义初始化方法,最后初始化后处理可返回代理,因此配置校验、远程探测、代理创建和重复包装问题主要在此出现。若只看到外层 BeanCreationException(对象创建异常)就猜构造器,会错过最深阶段证据。项目中支付渠道缺少签名器候选属于属性解析错误;签名器存在但密钥格式校验失败属于初始化错误;最终事务代理缺失则要检查初始化后处理链。验证应为每个阶段记录对象名称、耗时和根异常,禁止打印密钥;故障注入分别移除候选、制造初始化超时和重复代理,确认告警能区分。设计上不要在 populateBean之前发布对象,也不要在 initializeBean完成前让业务线程使用;否则半初始化状态会逃逸,后续缓存清理也无法回收外部引用。
若线上只有部分实例异常,我会按创建时间核对它们是否在处理器注册完成前产生,并比较阶段日志;这往往比反复查看业务堆栈更快定位。
- 追问 1:属性填充只处理字段吗?直接回答:不只,构造之外的属性值、注入点和相关实例后处理扩展都可能参与。
- 追问 2:初始化阶段能新增依赖吗?直接回答:技术上可动态查找,但会隐藏依赖和引入过早创建,普通业务对象不推荐。
- 追问 3:代理创建属于哪个阶段?直接回答:常见最终代理在初始化后处理,循环时可能通过早期引用扩展提前产生。
- 关联专题:依赖填充与初始化
- 问题(综合题):支付服务和账务服务形成循环依赖时,怎样从对象问题上升到业务一致性设计?
口述答案:结论:支付与账务互相注入通常不是单纯容器问题,而是权威状态、事务边界和流程编排没有划清;三级缓存让应用启动也无法保证资金一致。先定义支付域负责渠道请求、支付单和外部结果,账务域负责分录与余额事实;一次支付确认可由上层应用服务在本地边界内编排,跨模块传播则把支付状态和 Outbox(发件箱)记录放同一本地事务,账务按稳定事件标识和支付单号幂等入账。账务结果不能回调支付对象要求立即修改,而应通过状态、事件或查询端口反馈,未知结果由主动查单和对账收敛。这样对象依赖从双向变为支付事实向账务消费的单向关系。失败边界包括消息重复、发布失败、账务入账后确认丢失、渠道结果未知和人工冲正;这些必须靠唯一流水、状态机、重试预算和金额对账解决,容器锁或对象早期引用都无能为力。若同一进程且强不变量可在一个数据库事务内完成,可由应用服务直接调用两个窄端口,不必为了消除循环强行引入 MQ(消息队列)。验证闭环是依赖图无环、支付单与分录唯一、重复事件不重复入账、故障后可查证、日终金额守恒,以及关闭循环兼容开关仍可启动。面试表达应明确“先重构业务方向,再选同步或异步机制”,而不是用技术技巧掩盖资金责任。
架构评审还要明确每个事实的权威方、可接受不一致窗口和人工出口;若这些问题没有答案,改成事件也只是把对象循环升级为分布式循环。
日终还需按币种和渠道核对金额、笔数与差异年龄,避免技术成功掩盖资金差错。
- 追问 1:账务失败要回滚渠道支付吗?直接回答:外部支付通常不可由本地回滚,应保持未知或待入账并重试、查证、对账,必要时走独立退款或冲正。
- 追问 2:同库一定要异步吗?直接回答:不一定,能在清晰本地事务内守住强不变量时,同步编排更简单。
- 追问 3:事件如何避免重复入账?直接回答:稳定事件标识、账务唯一流水和条件状态共同保证,不能只依赖消费者内存。
- 关联专题:循环依赖重构决策
- 问题(综合题):WMS(仓储管理系统)中订单、库存和履约对象怎样避免双向依赖并保持可测试性?
口述答案:结论:对象依赖应跟随业务决策方向,而不是让每个服务都持有其他完整服务。订单应用服务接收创建或取消用例,调用库存端口完成预占或释放,并调用履约端口创建任务;库存只维护可售、冻结和扣减不变量,不反向调用订单服务获取整个订单,所需业务键和数量由命令显式携带;履约只消费已确认订单快照和渠道信息,不直接修改库存表。共同规则放到窄领域服务或值对象,上层编排负责一次用例的顺序和失败决策。容器层使用构造器注入这些窄端口,启动时即可发现缺失和循环;测试可直接传入库存、履约替身,验证调用和状态,而不启动 ApplicationContext(应用上下文)。跨边界异步时,订单状态和待发布事实同事务保存,消费者按订单行和事件版本幂等;同步本地事务能守住的不变量不必为形式上的微服务拆散。失败边界包括库存预占成功但订单响应丢失、履约创建重复、取消与出库并发、适配器远程超时;这些靠请求标识、条件状态、查单和补偿处理,而不是服务互相回调。验证闭环是依赖图单向、模块测试无需容器、重复命令结果一致、库存守恒、履约唯一和异常状态可对账。若发现库存为了发通知直接调用订单、订单又调用库存,就应把通知改为事实事件或让应用层统一编排。
代码侧再用模块依赖规则禁止库存和履约反向引用订单实现包,运行侧审计旁路写;编译依赖与数据写权同时单向,边界才不会回腐。
对账指标要同时看支付金额、账务分录与差异年龄,不能只统计消息消费成功率。
- 追问 1:查询订单信息是否也不能反向调用?直接回答:可依赖窄只读端口或业务快照,但不能借查询接口获得写入权和重新形成双向事务耦合。
- 追问 2:上层编排放哪里?直接回答:放应用服务,用例级协调,不把领域规则和基础设施细节都堆进去。
- 追问 3:如何测试无循环?直接回答:增加架构依赖规则和关闭循环兼容后的上下文启动测试,同时审查动态上下文查找。
- 关联专题:IoC(控制反转)项目落地
- 问题(综合题):异步线程、请求上下文和 Bean(对象实例)生命周期为什么容易产生错误,如何治理?
口述答案:结论:容器管理的是对象实例和作用域,线程切换不会自动复制事务、安全、日志、租户或请求上下文;在初始化阶段启动异步任务还可能让任务访问尚未完成的对象。机制上,单例可以被异步线程共享,但请求级状态通常绑定原线程或专用作用域;把任务提交到线程池后,原请求可能已结束,目标作用域无法解析,事务连接也不会自然转移。若构造或初始化回调提交任务并同步等待,而任务又依赖当前正在创建的 Bean(对象实例),就形成阶段闭环或线程池死锁,三级缓存不能提供跨线程业务就绪保证。治理方式是把启动期只用于注册能力,等 ApplicationContext(应用上下文)就绪后由独立协调器启动任务;任务消息显式携带租户、请求标识和业务版本,接收端重新鉴权和建立事务,不直接传递线程本地对象。Runner(执行器)任务使用持久状态、租约、检查点和幂等键,关闭时停止领取并允许其他实例接管。失败边界包括上下文泄漏到复用线程、旧租户污染下一任务、异步异常无人观察、线程池队列满和进程强杀。验证闭环包含线程切换后的上下文字段、事务边界、任务重复执行、关闭接管和线程池饱和演练;还要确认原型或请求域资源由正确所有者释放。这样回答能把容器生命周期与并发模型连接起来,而不是误以为注入成功就能安全异步。
任务验收还要在上下文缺失、线程池拒绝和进程强杀下执行,检查租户不串、状态可接管、重复无副作用,并记录每类失败的恢复时限。
- 追问 1:异步方法会继承调用方事务吗?直接回答:通常不会,线程绑定资源不自动跨线程;异步端应建立自己的事务和幂等边界。
- 追问 2:复制全部线程上下文可行吗?直接回答:不应盲目复制,安全和租户信息需最小化、可验证,数据库连接等资源不能跨线程搬运。
- 追问 3:初始化中提交任务为什么危险?直接回答:对象未最终就绪、代理未完成,任务还可能反向等待刷新线程形成死锁。
- 关联专题:异步与循环边界
- 问题(综合题):线上出现 BeanCreationException(对象创建异常)或部分方法代理失效,你会怎样系统排查?
口述答案:结论:排查要从完整证据和最深根因出发,先定位创建阶段,再判断定义、候选、循环、初始化或代理问题,不能反复加懒加载和开关试错。第一步保存框架与应用版本、配置来源、完整异常链和条件评估,找到最深 BeanCreationException(对象创建异常)及首次出现的正在创建对象;第二步列出对象定义来源、作用域、构造器和依赖候选,画 A 到 B 再回 A 的实际路径;第三步按阶段分流:注册阶段查扫描与覆盖,实例化查构造器和工厂方法,属性填充查候选与循环,初始化查回调远程调用和配置校验,代理阶段查处理器顺序、早期引用和重复包装。若现象是部分调用有事务、部分没有,要比较依赖方引用与容器最终引用的代理类别、匿名身份和通知链,排查原对象逃逸、内部调用和早期代理不一致。修复优先重构单向依赖或统一代理基础设施,临时兼容必须记录期限和回归。项目验证不能只看启动:支付回调要验证重复不入账、库存要验证条件更新、关闭要验证在途任务恢复。最后执行冷启动、并发调用、初始化失败和滚动关闭演练,检查三层缓存、线程、连接和注册信息均清理。报告中保留根因、触发条件、为何旧监控未发现以及防回归措施,避免下一次只看到不同外层异常。
事故复盘必须补一条可自动执行的防回归规则,例如关闭循环开关启动、候选唯一性断言或代理通知计数,避免结论停留在人工经验。
最后在关闭全部临时诊断开关后重复故障用例,确认常规指标仍能及时报警。
- 追问 1:外层异常信息够不够?直接回答:通常不够,它多为包装;最深原因、定义来源和首次循环提示更有价值。
- 追问 2:如何确认事务代理失效?直接回答:检查实际引用和调用是否经过代理,并用可回滚测试或事务同步证据验证,不能只看注解存在。
- 追问 3:临时开循环引用后要做什么?直接回答:验证代理身份和业务行为,登记负责人、退出期限,并立即安排依赖重构。
- 关联专题:启动与代理排查
- 问题(综合题):如果面试官让你用五分钟串讲 IoC(控制反转)、生命周期和循环依赖,你会怎样组织答案?
口述答案:结论先行:Spring(Java 应用框架)容器把对象创建、装配和生命周期外置,BeanDefinition(Bean 定义)描述配方,BeanFactory(Bean 工厂)负责创建与缓存,ApplicationContext(应用上下文)在其上组织环境、事件和刷新;循环依赖只是对象创建中的受限兼容机制。第一段讲刷新:先准备环境和定义,先执行 BeanFactoryPostProcessor(Bean 工厂后置处理器),再注册 BeanPostProcessor(Bean 后置处理器),最后预实例化必需单例;失败就销毁本轮对象,不发布就绪。第二段讲单对象:合并定义后通过构造器或工厂实例化,填充依赖,执行 Aware(感知接口)、构造后回调、初始化接口和自定义方法,初始化后处理可能返回代理,关闭时按依赖关系销毁。第三段讲缓存:单例属性循环中,原始 A 实例化后把早期引用工厂放三级,B 请求 A 时生成早期对象或代理放二级,A 完成后同一引用进入一级;构造器、prototype(原型作用域)、禁用循环和代理不一致都不能解决。第四段讲设计:第三级价值是延迟决定早期代理,能启动不等于设计正确,业务双向依赖应抽上层编排、第三服务或事件。第五段落项目:支付与账务用状态和 Outbox(发件箱)单向传播,WMS(仓储管理系统)订单编排库存与履约;启动校验候选唯一,关闭先摘流量再排空。最后给验证:依赖图、冷启动、代理身份、失败清理和业务守恒。这样的结构从思想、机制、边界到证据完整闭环。
真正口述时我会在每段最后给一个失败边界和一个项目证据,控制在五分钟内;面试官追源码时再沿关键方法展开,避免一开始陷入字段细节。
修复完成后再删除所有临时诊断开关,确认监控仍能用常规指标及时发现同类故障。
- 追问 1:这套答案最容易漏什么?直接回答:容易把刷新和单对象生命周期混在一起,也容易漏掉早期代理身份与失败清理。
- 追问 2:为什么强调项目证据?直接回答:面试官要判断是否真正处理过配置、代理和发布问题,而不是只背源码名词。
- 追问 3:一句话说明循环依赖立场?直接回答:框架可在受限场景兼容,但应用应优先保持单向依赖并把开关视为迁移工具。
- 追问 4:一句话说明第三级价值?直接回答:延迟生成唯一早期引用,并在需要时提前得到与最终一致的代理。
- 关联专题:本篇完整主线
7. 精通级复习清单
- 能在不看文档时区分容器刷新、单个 Bean(对象实例)创建与对象销毁。
- 能逐步说出定义注册、合并、构造器选择、依赖解析、初始化和代理时点。
- 能解释 BeanFactoryPostProcessor(Bean 工厂后置处理器)与 BeanPostProcessor(Bean 后置处理器)的输入、顺序和误用风险。
- 能画出 singletonObjects(一级缓存)、earlySingletonObjects(二级缓存)和 singletonFactories(三级缓存)的迁移。
- 能说明第三级缓存为何与早期代理一致性有关,而不是只说“解决循环依赖”。
- 能说明构造器、prototype(原型作用域)、异步创建和多代理创建器的失败边界。
- 能解释 singleton(单例作用域)不等于线程安全、单例注入原型为何仍只注入一次。
- 能从
refresh、doGetBean、getSingleton、doCreateBean、populateBean、initializeBean定位源码。 - 能讲清 Spring Boot(快速开发框架)2.6+ 默认策略与底层 Spring(Java 应用框架) Framework(核心框架)能力的区别。
- 能把支付、库存、履约和 Runner(执行器)任务的依赖方向、启动与关闭策略说成项目证据。
8. 事实与版本说明
- 本篇以 Java(编程语言)8 + Spring(Java 应用框架) Framework(核心框架)5.3 + Spring Boot(快速开发框架)2.7 作为存量项目理解基线,以 Java(编程语言)17+ + Spring(Java 应用框架) Framework(核心框架)6.x + Spring Boot(快速开发框架)3.x 作为现代迁移理解基线。
- Spring Boot(快速开发框架)2.6+ 默认循环引用策略只用于解释已核对的主版本边界;实施时必须以项目锁定的小版本、依赖树和对应官方文档为准。
singletonObjects、earlySingletonObjects、singletonFactories及源码方法属于实现理解入口,不是应用可依赖的公共扩展协议。
