五、事务、依赖注入、AOP 与自动配置深化(S31–S40)
S31 · 知识点:事务传播 REQUIRES_NEW
【题目】ServiceA.methodA 标注 @Transactional,在其中调用 ServiceB.methodB,而 methodB 标注 @Transactional(propagation = Propagation.REQUIRES_NEW)。若 methodB 抛出 RuntimeException 且未捕获,通常会发生什么?
A. methodA 与 methodB 使用同一事务,两者都不会回滚 B. 仅 methodA 回滚,methodB 已提交的独立事务不受影响 C. methodB 挂起外层事务并在新事务中执行;methodB 新事务回滚;若异常继续抛到 methodA,外层事务通常也会回滚 D. methodB 必须以非事务方式执行,REQUIRES_NEW 无效
答案:C
【考点】REQUIRES_NEW 的“挂起当前 + 新建独立事务”语义,以及异常向上传播后的外层影响。
【结论】选 C。REQUIRES_NEW 会挂起 methodA 的事务,为 methodB 开新事务;methodB 失败则新事务回滚;异常若未在 methodA 捕获,methodA 的事务边界仍会因异常触发回滚。
【逐项辨析】
- A 错误:二者不是同一事务;这更接近默认 REQUIRED 的共享事务情形。
- B 错误:独立新事务会先因异常回滚;外层是否回滚取决于异常是否被处理。
- C 正确:体现挂起 + 独立事务 + 异常传播两层语义。
- D 错误:REQUIRES_NEW 表示新建事务;非事务对应 NOT_SUPPORTED。
【知识点】
- REQUIRES_NEW:总是开启新物理事务;外层事务被挂起。
- 典型用途:操作日志、审计流水——希望“主事务失败也要留下痕迹”。
- 注意坑:双连接与锁;内层成功外层失败时内层不回滚,可能不一致;自调用不走代理时失效。
- 与 REQUIRED:REQUIRED 共享事务;REQUIRES_NEW 隔离成败。
【记忆锚点】「REQUIRES_NEW 另开新档:外层暂停,内层自己提交回滚;异常传出去,外层仍可能回滚。」
【易混对比】
| 传播行为 | 新事务? | 外层是否挂起 | 典型场景 |
|---|---|---|---|
| REQUIRED | 无事务才新建 | 否 | 默认业务方法 |
| REQUIRES_NEW | 总是新建 | 是 | 审计日志 |
| NESTED | Savepoint 嵌套 | 否 | 部分回滚 |
| NOT_SUPPORTED | 否 | 是 | 非事务查询 |
【自测】 若 methodB 使用 REQUIRED 且抛出未捕获 RuntimeException,methodA 会怎样?
答:同一事务整体回滚;与 REQUIRES_NEW 的独立回滚不同。与 S13/S32 连考。
【知识关联】
- 同库关联:与 S13(@Transactional 基础)、S14(失效场景)、S32(NOT_SUPPORTED)、S36(自调用失效)构成事务题群;面试常连着 MySQL 事务隔离级别、锁等待一起追问。
- 实现层:TransactionInterceptor 在进入 methodB 时读取 Propagation.REQUIRES_NEW,调用 AbstractPlatformTransactionManager.getTransaction:若当前已有事务,则先 suspend 挂起(保存 Connection/TransactionStatus 到同步上下文),再开启新物理事务。methodB 抛 RuntimeException 时,新事务按 rollback 规则回滚;异常继续抛出到 methodA 的拦截器时,methodA 的事务边界也会因未捕获异常回滚(除非 methodA catch 掉且不再抛)。两套事务可能占两个数据库连接,高并发下易打满连接池。内层提交、外层回滚时会出现「审计日志已落库但业务未提交」的不一致,需补偿或最终一致设计。
- 面试追问:① 审计日志为何常用 REQUIRES_NEW?② 与 NESTED 的区别?③ 如何避免连接池打满? 【拓展延伸】
- 变式问法:① 给调用链与传播属性,判断回滚范围;② 问「methodB 成功提交后 methodA 再失败」时 methodB 数据是否回滚(否,已独立提交);③ 问 MANDATORY/NEVER 的行为。
- 版本差异:Spring 事务传播语义自 2.x/3.x 起稳定;Boot 2/3 均支持注解式事务;Jakarta EE(Boot 3)包名从 javax 变为 jakarta,传播枚举语义不变;响应式事务(WebFlux + R2DBC)模型不同,不能直接套用同一套传播语义。
- 工程注意点:① REQUIRES_NEW 仅用于真正需要独立成败的场景(审计、通知流水);② 评估连接池大小与超时;③ 文档化传播策略,避免新人乱加;④ 分布式场景本地事务不够,要消息表/TCC/Saga;⑤ 监控长事务与锁等待,必要时拆分。
S32 · 知识点:事务传播 NOT_SUPPORTED
【题目】@Transactional(propagation = Propagation.NOT_SUPPORTED) 的含义是?
A. 方法必须在已有事务中运行,否则抛异常 B. 方法以非事务方式运行;若存在外层事务则将其挂起 C. 方法总会新建事务,并嵌套在外层事务内 D. 与默认 REQUIRED 行为完全相同
答案:B
【考点】NOT_SUPPORTED / MANDATORY 等传播行为语义。
【结论】选 B。NOT_SUPPORTED:挂起当前事务(若有),以非事务方式执行方法。
【逐项辨析】
- A 错误:这是 MANDATORY。
- B 正确:NOT_SUPPORTED 标准定义。
- C 错误:接近 NESTED/REQUIRES_NEW 的混淆版。
- D 错误:REQUIRED 是“有则加入无则新建”。
【知识点】 Spring 定义了七种 Propagation,核心差异是「有无当前事务时怎么办」:
| 传播行为 | 有事务时 | 无事务时 |
|---|---|---|
| REQUIRED(默认) | 加入 | 新建 |
| REQUIRES_NEW | 挂起并新建 | 新建 |
| NESTED | Savepoint 嵌套 | 新建 |
| SUPPORTS | 加入 | 非事务执行 |
| NOT_SUPPORTED | 挂起,非事务执行 | 非事务执行 |
| MANDATORY | 加入 | 抛异常 |
| NEVER | 抛异常 | 非事务执行 |
| 本题 NOT_SUPPORTED:方法以非事务方式运行;若存在外层事务则先 suspend 挂起,执行完再 resume。因此内层 SQL 不受外层回滚约束,内层异常也不会自动回滚外层(外层是否回滚取决于外层自己的异常处理)。典型场景:耗时导出、报表、外部 HTTP 调用——避免长事务占连接。与 REQUIRED(有则加入无则新建)、MANDATORY(没事务我不干)对比记忆。注意 @Transactional(readOnly=true) 仍在事务内,只是提示驱动做只读优化,与 NOT_SUPPORTED 不同。实现层由 AbstractPlatformTransactionManager 负责挂起/恢复 Connection 与 TransactionStatus。 |
【记忆锚点】「NOT_SUPPORTED:有事务先挂起;MANDATORY:没事务我不干。」
【易混对比】
- NOT_SUPPORTED vs SUPPORTS:前者坚持非事务,后者随大流。
- 只读长查询可考虑 NOT_SUPPORTED,但异常不再触发外层事务回滚。
【自测】 耗时 30s 的导出接口适合 REQUIRED 吗?
答:长事务占连接,通常不合适;可考虑 NOT_SUPPORTED 或不开启事务的只读服务。与 S31 连考。
【知识关联】
- 同库关联:与 S31(REQUIRES_NEW)、S13(默认 REQUIRED)、S14(@Transactional 失效)连考;导出/报表/只读查询是 NOT_SUPPORTED 的典型业务背景。
- 实现层:Propagation.NOT_SUPPORTED 在 AbstractPlatformTransactionManager 中:若存在当前事务则 suspend,随后以非事务方式执行,方法结束后 resume 原事务。因此 methodB 中的 SQL 不受外层事务回滚约束,其异常也不会自动回滚外层事务(外层是否回滚取决于外层自己的异常处理)。MANDATORY:无事务直接抛异常。NEVER:有事务则抛异常。SUPPORTS:有则加入,无则非事务运行。只读优化:@Transactional(readOnly=true) 会设置连接只读提示;但与 NOT_SUPPORTED 不同,readOnly 仍在事务内。
- 面试追问:① NEVER 何时用?② 只读事务优化点?③ 挂起期间 ThreadLocal 事务上下文如何? 【拓展延伸】
- 变式问法:① 传播行为匹配选择题;② 问「30s 导出接口用 REQUIRED 有何问题」;③ 问异常在 NOT_SUPPORTED 方法中抛出,外层事务状态。
- 版本差异:七种传播行为自 Spring 早期稳定;Boot 2/3、JDBC/JPA 语义一致;R2DBC/响应式场景用 TransactionalOperator,传播支持有限,需单独设计。
- 工程注意点:① 长查询、外部 HTTP 调用避免包在大事务里,可 NOT_SUPPORTED 或拆到非事务服务;② 组合传播要在团队规范中白名单化;③ 注意事务挂起后手动 JDBC 时的连接来源;④ 日志打印当前是否在事务中便于排障;⑤ 与读写分离中间件交互时,非事务读可能路由到从库,注意一致性预期。
S33 · 知识点:三级缓存与循环依赖
【题目】Spring 解决单例 Bean 的 setter/字段注入循环依赖时使用三级缓存。关于“为什么需要第三级缓存(singletonFactories)”,下列说法最准确的是?
A. 仅仅为了加快对象创建速度,与 AOP 无关 B. 第三级缓存直接存放最终初始化完成的 Bean C. 只有 prototype 作用域才使用三级缓存 D. 为了在需要时通过 ObjectFactory 生成早期引用,以便在 Bean 被 AOP 代理时提前暴露的是代理对象(或正确形态),避免拿到未代理的原始对象
答案:D
【考点】三级缓存职责划分,以及第三级缓存与 AOP 代理提前暴露的关系。
【结论】选 D。singletonFactories 保存 ObjectFactory,在真正需要早期引用时再 getObject(),从而有机会返回代理后的对象。
【逐项辨析】
- A 错误:不是单纯加速;关键价值是延迟决定早期引用形态。
- B 错误:成品 Bean 在一级缓存
singletonObjects。 - C 错误:三级缓存针对单例;prototype 默认不靠它解决循环依赖。
- D 正确:与循环依赖 + AOP 同时出现时的正确性直接相关。
【知识点】
| 缓存 | 名称 | 内容 |
|---|---|---|
| 一级 | singletonObjects | 成品 Bean |
| 二级 | earlySingletonObjects | 早期引用(确定形态) |
| 三级 | singletonFactories | ObjectFactory(可能产出代理) |
若只有两级并立刻放入 raw 对象,AOP 场景下循环方可能注入未代理对象,事务/切面失效。
【记忆锚点】「一级成品、二级早产、三级是工厂留后手;要代理时再变身。」
【易混对比】
- 三级缓存 ≠ 性能缓存;核心是循环引用形态正确性。
@Lazy可打破构造器循环。
【自测】 A、 B setter 互相注入且 A 需要事务代理,B 注入到的 A 可能是什么?
答:可能是 A 的代理对象,从而事务注解仍有效。与 S18/S35 连考。
【知识关联】
- 同库关联:与 S18(构造器循环依赖无法解)、S34(prototype 环)、S35(AOP 代理)、S36(自调用)构成容器与代理题群;Boot 2.6+ 默认禁止循环依赖,使本题从「静默成功」变为「启动失败」的架构约束。
- 实现层:DefaultSingletonBeanRegistry 三级缓存:① singletonObjects(一级):完全初始化的单例;② earlySingletonObjects(二级):已确定形态的早期引用;③ singletonFactories(三级):ObjectFactory,getEarlyBeanReference 时可能调用 SmartInstantiationAwareBeanPostProcessor 生成代理。流程:实例化 A(构造,属性空)→ 把 A 的 ObjectFactory 放入三级缓存 → 填充 A 属性时依赖 B → 创建 B → 填充 B 时依赖 A → 从三级缓存取出 factory.getObject() 得到 A 的早期引用(可能是代理)→ 放入二级缓存并移除三级 → B 初始化完成 → A 完成初始化进入一级。若无三级、过早暴露 raw A,则事务切面可能对原始对象失效。
- 面试追问:① 为何不用二级?② 哪些 Bean 不能循环依赖?③ @Lazy 如何破环? 【拓展延伸】
- 变式问法:① 问三级缓存各自存什么;② A/B setter 互注且 A 需事务,B 拿到的 A 是什么;③ 问「只有一级缓存行不行」。
- 版本差异:三级缓存机制自 Spring 3.x/4.x 稳定;Spring Boot 2.6(Spring Framework 5.3.x 起)默认 spring.main.allow-circular-references=false,循环依赖启动即失败;Boot 3 保持默认禁止,官方建议构造器注入与无环设计。
- 工程注意点:① 优先构造器注入暴露设计问题;② 临时用 @Lazy/ObjectProvider 缓解,而不是全局打开 allow-circular-references;③ 领域服务依赖方向单向:领域 → 防腐层 → 基础设施,避免双向;④ 启动失败日志中有 cycle 提示时按依赖图重构;⑤ 单测验证关键 Bean 的代理类型。
S34 · 知识点:循环依赖无法解决的场景
【题目】下列哪种情况最可能导致 Spring 无法自动解决循环依赖?
A. 两个 prototype Bean 通过 setter 互相注入,期望容器自动解决 B. 两个 singleton Bean 通过 setter/字段互相注入(未禁用循环依赖) C. 两个 singleton Bean 通过字段互相注入且使用默认容器能力(允许循环依赖时) D. 单个 Bean 仅注入配置类中无相互指向的 @Bean 产品
答案:A
【考点】三级缓存主要服务 singleton;prototype 循环依赖不在自动解决范围。
【结论】选 A。prototype 每次 getBean 都新建,容器不维护其完整生命周期缓存,无法用三级缓存解决互相注入的环。
【逐项辨析】
- A 正确:Spring 对 prototype 循环依赖基本不提供自动解决。
- B 错误:经典可解场景(未禁用循环依赖时)。
- C 错误:同 B,setter/字段注入的 singleton 环是成功案例。
- D 错误:不构成典型双向循环依赖。
【知识点】 Spring 不能(或默认不)解决的情况:
- 构造器注入形成的环(S18);
- prototype 作用域互相依赖;
- Boot 2.6+ 默认禁止循环依赖,setter 环也会启动失败;
- 无法暴露 early reference 的特殊场景。
解决思路:重构消除双向依赖;@Lazy;ObjectProvider;仅在允许时改 setter/字段注入。
【记忆锚点】「构造器死结、prototype 不管、Boot2.6 默认不让环。」
【易混对比】
- singleton setter 环 vs prototype 环。
- 循环依赖是架构坏味道,失败快优于不一致。
【自测】 Boot 3 项目启动报循环依赖,可能原因与处理?
答:默认不允许循环依赖;应重构,或评估后打开
spring.main.allow-circular-references(不推荐),或用 @Lazy。与 S18/S33 连考。
【知识关联】
- 同库关联:与 S18(构造器环)、S33(三级缓存只服务单例)、S35/S36(代理与自调用)连考;架构评审中「是否存在双向依赖」是常见检查项。
- 实现层:prototype 的 getBean 每次 createBean,容器不把 prototype 放入 singleton 三级缓存,也就没有「提前暴露的早期引用」可供另一半注入;A 创建时要 B、B 创建时要 A,环无法解开而失败——实测抛
UnsatisfiedDependencyException,root cause 是BeanCurrentlyInCreationException(不是 StackOverflowError,容器有 in-creation 标记会先检出)。构造器注入的 singleton 环同理:实例化 A 时就要 B,B 要 A,A 尚未暴露引用。Boot 2.6+ 即便 setter 环可被三级缓存解,也会因默认禁止而启动失败。@Lazy 把依赖解析推迟到首次调用,注入的是代理,从而打断启动期环。ObjectProvider 在使用点再 getBean,同样能延迟。 - 面试追问:① 如何检测循环依赖?(启动日志、ArchUnit、IDE 依赖分析)② 为何构造器注入更安全?③ 分布式服务间的「循环调用」与容器内环有何不同? 【拓展延伸】
- 变式问法:① 判断题「所有循环依赖 Spring 都能解」;② 给场景问 Boot 3 启动是否失败;③ 问 prototype 互注的现象。
- 版本差异:Boot 2.6 默认策略变更影响面大;Boot 3/Jakarta 持续默认禁止;Spring Native/AOT 下更强调无反射友好与启动期确定性,环依赖问题更早暴露。
- 工程注意点:① 模块边界与依赖方向写入架构规范;② 公共能力下沉,避免 Service 互相注入的泥球;③ 用事件(ApplicationEvent)或中介者解耦双向协作;④ 评审时检查 allow-circular-references 是否被偷偷打开;⑤ 新服务脚手架直接关闭该配置,迫使设计无环。
S35 · 知识点:AOP 代理方式(JDK 与 CGLIB)
【题目】关于 Spring AOP 的 JDK 动态代理与 CGLIB,下列说法正确的是?
A. JDK 动态代理必须基于接口,通过让代理类实现与目标相同的接口来织入逻辑 B. CGLIB 通过生成目标类子类实现代理,因此可以代理任意 final 类 C. Spring Boot 2.x 及以后取消了代理机制,切面不再依赖代理对象 D. 二者都会在编译期直接改写目标类源码后再编译
答案:A
【考点】JDK 动态代理与 CGLIB 的实现差异,以及 Spring Boot 2.x 默认代理策略。
【结论】选 A。JDK 动态代理基于接口;CGLIB 基于继承/字节码生成子类。
【逐项辨析】
- A 正确:
Proxy.newProxyInstance要求接口列表。 - B 错误:CGLIB 依赖生成子类,final 类无法被继承代理。
- C 错误:Spring AOP 仍基于代理;Boot 2.x 更常默认 CGLIB(
proxy-target-class=true)。 - D 错误:Spring AOP 是运行时代理;编译期改写是 AspectJ 等技术。
【知识点】
- JDK 动态代理:接口 + InvocationHandler。
- CGLIB:生成子类;final 类/方法难代理。
- Boot 默认:经典 Spring“有接口默认 JDK”;Boot 2.x 起优先 CGLIB。
- 自调用:同类 this 调用不走代理(S36)。
- AspectJ:编译期/加载期织入,非 Spring AOP 默认。
【记忆锚点】「JDK 代理要接口,CGLIB 靠子类;final 不可继承就代不了;Boot2 常默认 CGLIB。」
【易混对比】
| 代理 | 依据 | 限制 |
|---|---|---|
| JDK 动态代理 | 接口 | 必须有接口 |
| CGLIB | 继承/字节码生成 | final 类/方法难以代理 |
| AspectJ | 织入 | 需额外编译/agent |
【自测】 为何 final class 上的 @Transactional 可能不生效?
答:CGLIB 无法继承 final 类,切面无法织入。与 S14/S36 连考。
【知识关联】
- 同库关联:与 S14/S15(AOP 与事务)、S33(循环依赖与代理提前暴露)、S36(自调用失效)构成代理题群;与 J16/J17(接口)呼应——JDK 代理必须基于接口。
- 实现层:JDK 动态代理:Proxy.newProxyInstance(loader, interfaces, handler) 生成实现相同接口的代理类,调用转发到 InvocationHandler.invoke;Spring JdkDynamicAopProxy 在其中织入拦截器链。CGLIB:运行时生成目标类子类(字节码),重写非 final 方法,通过 MethodInterceptor 拦截;因此 final 类/方法、private 方法无法被有效代理。Spring AOP 默认在运行期生成代理对象放入容器,注入方拿到的是代理而非原始 target。经典 Spring:有接口默认 JDK,无接口 CGLIB;Boot 2.x 起 spring.aop.proxy-target-class=true 成为默认,即优先 CGLIB。AspectJ 则是编译期/加载期织入,不依赖运行时代理,但需要额外构建/agent。
- 面试追问:① Boot 2 为何默认 CGLIB?② 如何强制某种代理?③ 代理对象 equals/hashCode 注意什么? 【拓展延伸】
- 变式问法:① 问 final class 上 @Transactional 是否生效;② 选择题问 Boot 2 默认代理类型;③ 给接口+实现类,问注入类型是接口还是实现类。
- 版本差异:Boot 2.0 起默认 CGLIB(proxy-target-class);Spring Framework 6 / Boot 3 继续;JDK 动态代理在 Java 模块系统下仍可用;CGLIB 与 Java 新版本字节码兼容性由 Spring 依赖的版本决定,升级 Boot 时需同步。
- 工程注意点:① 业务 Bean 避免滥用 final 关键方法,若需要代理语义;② 测试中可用 AopTestUtils 取原始对象;③ 自我注入、@Lazy、AopContext 等方案各有侵入性,优先拆 Bean;④ 监控代理创建失败;⑤ 对外 API 优先面向接口编程,兼容两种代理。
S36 · 知识点:AOP 与事务自调用失效
【题目】同一类中,方法 A 直接 this.methodB() 调用本类中标注了 @Transactional 的方法 B,事务注解通常?
A. 正常生效,因为注解在源码里已经写上 B. 失效,因为 this 调用未经过 Spring 代理对象,切面逻辑不会被触发 C. 仅当方法 B 是 private 时才生效 D. 会自动开启一个嵌套事务 NESTED
答案:B
【考点】Spring AOP 基于代理,自调用不走代理导致注解不生效。
【结论】选 B。同类内部 this.methodB() 绕过代理,TransactionInterceptor 不会执行。
【逐项辨析】
- A 错误:注解必须被代理织入拦截器后才有事务语义。
- B 正确:经典失效场景。
- C 错误:private 方法更难被代理,不是“private 才生效”。
- D 错误:自调用时根本没有进入事务拦截。
【知识点】 Spring AOP(含事务)基于代理对象,不是改写目标类源码。容器中注入的 Bean 引用通常是代理;代理持有 target,调用链为:外部调用 → 代理 → 拦截器链(TransactionInterceptor 等)→ target.method。 因此:
- 外部 beanA.methodB():走代理,@Transactional 生效;
- 同类内部 this.methodB():this 是目标对象,不经过代理,注解完全不被处理 → 事务/AOP 失效。 这也是「注解写在源码里却不生效」最经典的原因。常见失效清单还包括:
- 非 public 方法(代理/织入受限);
- 异常被 catch 且未再抛(拦截器看不到异常,默认不回滚);
- rollbackFor 不匹配(默认只回滚 RuntimeException/Error);
- Bean 未被 Spring 管理(new 出来的对象无代理);
- 新线程/@Async 中调用(事务上下文不自动传播);
- final 类/方法导致 CGLIB 无法有效代理。 解决思路:抽到另一个 Bean 再注入调用;@Lazy 注入自身代理;AopContext.currentProxy()(需 exposeProxy);改用 TransactionTemplate 编程式事务;或 AspectJ 编译期织入。记忆:「注解躺在目标上,代理不包 this 调;要事务就绕出类外走代理门。」
【记忆锚点】「注解躺在目标上,代理不包 this 调;要事务就绕出类外走代理门。」
【易混对比】
- 外部
beanA.methodB()vs 内部this.methodB()。 TransactionTemplate可编程控制事务边界。
【自测】 如何最小改动让同类自调用的事务生效?
答:抽到另一个 Spring Bean 并注入调用;或注入自身代理。与 S14/S35 连考。
【知识关联】
- 同库关联:与 S14(事务失效总表)、S35(JDK/CGLIB 代理)、S31/S32(传播行为,只有走代理才谈传播)连考;CR 与代码审计高频项。
- 实现层:Spring 容器中 Bean 的引用通常是代理对象,目标对象 target 被代理持有。外部 beanA.methodB() 经代理 → 拦截器链 → TransactionInterceptor → 开启事务 → 调用 target.methodB。同类内部 this.methodB() 的 this 是目标对象,不经过拦截器,注解不被处理。private/static/final 方法同样常无法织入。异常被 catch 后不再抛出时,拦截器看不到异常,默认不会回滚;rollbackFor 不匹配(默认仅 RuntimeException/Error)时受检异常不回滚;新线程/@Async 里调用,事务上下文不会自动传播。解决:抽到另一 Bean;@Lazy 注入自身代理;AopContext.currentProxy()(需 exposeProxy);TransactionTemplate 编程式事务;AspectJ 编译期织入可覆盖部分自调用场景。
- 面试追问:① 异常吞掉为何失效?② 多线程为何失效?③ TransactionTemplate 适用何时? 【拓展延伸】
- 变式问法:① 代码挑错:同类 this 调 @Transactional;② 问「private 方法加 @Transactional 会怎样」;③ 问如何最小改动让自调用事务生效。
- 版本差异:代理模型多年一致;Boot 3 仍基于代理;虚拟线程下更要显式传递事务上下文;AspectJ LTW 在 Boot 中可通过 spring-instrument 配置,但不常见。
- 工程注意点:① 团队规范:事务方法 public 且经代理调用;② CR 搜索同类中 this.xxx( 与 @Transactional 组合;③ 日志中打印是否在事务中帮助确认;④ 编排层与领域层拆分,减少「上帝 Service」自调用;⑤ 单测覆盖回滚路径,而不是只测成功路径。
S37 · 知识点:Bean 作用域扩展
【题目】在 Spring Web 应用中,希望“每个用户会话(HttpSession)对应一个 Bean 实例”,应使用?
A. @Scope("singleton") B. @Scope("prototype") C. @Scope("request") D. @Scope("session") 或 @SessionScope
答案:D
【考点】Web 作用域 request/session/application 的区分。
【结论】选 D。session 作用域将 Bean 实例绑定到用户 HttpSession。
【逐项辨析】
- A 错误:全局单例,所有用户共享。
- B 错误:每次注入/getBean 新建,不绑定会话。
- C 错误:每个 HTTP 请求一个实例。
- D 正确:会话级作用域,购物车/用户偏好典型。
【知识点】
- 作用域:singleton、prototype、request、session、application、websocket。
- 需要 Web 上下文;测试无 Web 环境时可能失败。
- 注入 singleton 时应使用 scoped proxy 或 ObjectProvider。
@Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)。
【记忆锚点】「Request 一请求,Session 一用户,Application 一应用,Proto 每次新。」
【易混对比】
| 作用域 | 生命周期 |
|---|---|
| singleton | 容器级 |
| prototype | 每次获取新建 |
| request | 每个 HTTP 请求 |
| session | 每个 HttpSession |
【自测】 把 @SessionScope Bean 直接注入 Singleton Controller 可能遇到什么?
答:缺会话上下文或绑定错误;应使用 scoped proxy / ObjectProvider。与 S16 连考。
【知识关联】
- 同库关联:与 S16(Bean 作用域基础)、S38/S39(注入与消歧,作用域代理影响注入形态)连考;Web 会话购物车、用户偏好、多租户上下文是 session 作用域经典场景。
- 实现层:Spring 内置作用域:singleton(容器级)、prototype(每次 getBean 新建)、request/session/application/websocket(Web 层,绑定到 RequestAttributes)。@Scope("session") 或 @SessionScope(Spring Web 提供)把 Bean 实例放到 HttpSession 属性中。注入点若本身是 singleton,直接注入 session Bean 会在启动/首次注入时固定一个错误实例或失败,故需要 proxyMode = ScopedProxyMode.TARGET_CLASS(CGLIB 代理)或 INTERFACES,每次方法调用经代理解析到当前会话实例;也可用 ObjectProvider/@Lazy 在使用点获取。无 Web 请求上下文时(如启动 CommandLineRunner、单元测试)访问 session 作用域会抛异常。prototype 不由容器负责完整销毁回调链,需自行管理资源。
- 面试追问:① 分布式会话注意什么?② 为何要 scoped proxy?③ session Bean 过大有何问题? 【拓展延伸】
- 变式问法:① 选择题:每个用户会话一个实例用什么 scope;② 问 singleton Controller 注入 @SessionScope 的正确写法;③ 问 request 与 session 作用域差别。
- 版本差异:经典 Servlet 作用域长期稳定;WebFlux 无传统 HttpSession,对应 WebSession 模型;Boot 3 + Spring Session 可把会话放入 Redis,实现分布式 session 作用域 Bean 的共享。
- 工程注意点:① 会话 Bean 保持轻量,大对象放缓存/Redis;② 集群必须 session 共享(Spring Session)否则作用域语义破坏;③ 单测提供 mock 请求上下文或改为 prototype/请求级设计;④ 避免在 session Bean 中注入大量有状态依赖导致序列化问题;⑤ 与安全框架的 SecurityContext 协同时理清谁承载用户态。
S38 · 知识点:@Autowired 与 @Resource 注入细节
【题目】容器中存在两个类型均为 UserRepository 的 Bean,名称分别为 mysqlRepo 与 mongoRepo。现在要注入 mysqlRepo,下列写法最规范、意图最明确的是?
A. @Autowired private UserRepository mysqlRepo;(仅依赖字段名隐式匹配) B. @Resource private UserRepository mongoRepo; C. @Autowired @Qualifier("mysqlRepo") private UserRepository repo; D. 仅 @Autowired 不加任何限定必然注入成功且一定选 mysqlRepo
答案:C
【考点】多候选 Bean 时 @Qualifier/@Resource 指定名称的规则。
【结论】选 C。@Autowired 默认按类型;多候选时 Spring 依次看 @Primary → @Priority → 最后用注入点名称与 beanName 的隐式匹配兜底。C 用 @Qualifier 把意图写死,不依赖字段名,是最规范的写法。
【逐项辨析】
- A 不选(能注入成功,但不够规范):字段名
mysqlRepo恰好等于 beanName,Spring 的按名退化匹配会命中它,并不会抛NoUniqueBeanDefinitionException;风险在于这是一次"命名巧合"——字段改名、加@Primary、或换成构造器注入且参数名不同,就会立刻失败或被注成别的 Bean。 - B 错误:
@Resource默认按字段名mongoRepo查找,会注入到 mongo。 - C 正确:显式
@Qualifier("mysqlRepo"),意图清晰、结果确定。 - D 错误:不加限定不能保证必然成功且选中 mysqlRepo。
【知识点】
@Autowired
@Qualifier("mysqlRepo")
private UserRepository repo;
@Resource(name = "mysqlRepo")
private UserRepository repo;| 注解 | 来源 | 默认策略 | 多候选处理 |
|---|---|---|---|
| @Autowired | Spring | byType | @Primary / @Qualifier |
| @Resource | JSR-250 | byName | name 属性 / 字段名 |
【记忆锚点】「Autowired 先看类型,多胞胎用 Qualifier;Resource 先喊名字。」
【易混对比】
- 字段名恰等于 bean 名时“看起来都能成功”,但机制不同。
【自测】 若希望无特殊标注时默认选 mongoRepo,怎么加?
答:在 mongoRepo 对应 Bean 上加
@Primary。与 S17/S39 连考。
【知识关联】
- 同库关联:与 S17(@Autowired 基础)、S39(@Primary/@Qualifier 分工)构成 DI 消歧题群;多数据源、多 MQ 客户端、读写仓库分离是生产高频场景。
- 实现层:@Autowired 默认 byType:容器收集所有匹配类型的候选 Bean;恰好 1 个则注入;0 个且 required=true 报找不到 Bean;多个则进入消歧:先看 @Primary,再看 @Qualifier 指定名称,再尝试与注入点字段/参数名同名的 Bean,仍不确定则 NoUniqueBeanDefinitionException。规范写法是显式 @Autowired @Qualifier("mysqlRepo"),或构造器参数加 @Qualifier。@Resource(JSR-250)默认 byName:先按指定 name 找,再退化按字段名,最后按类型;@Resource(name="mysqlRepo") 意图清晰。字段名恰好等于 bean 名时「碰巧成功」,但机制不同,重构改名即碎。集合注入:List<UserRepository>/Map<String, UserRepository> 可注入全部候选。
- 面试追问:① @Qualifier 与 @Resource name 的差异?② 集合注入怎么做?③ 构造器注入为何更受欢迎? 【拓展延伸】
- 变式问法:① 多数据源注入选择题;② 问「仅 @Autowired 字段名 mysqlRepo」是否一定成功;③ 问 @Primary 与 @Qualifier 同时存在时谁优先。
- 版本差异:注解语义稳定;Boot 2.2+ 推荐构造器注入;Boot 3/Jakarta 使用 jakarta.annotation.Resource;@Autowired 的 required 语义不变;AOT 推理下更鼓励显式限定,减少按名魔法。
- 工程注意点:① 公共组件用 @Qualifier/@Named 显式点名;② 多模块 starter 慎用 @Primary,优先调用方限定;③ 构造器注入 + final 字段利于不可变与测试;④ 集成测试断言注入的是哪一个 Bean;⑤ 代码评审关注「隐式同名匹配」的脆弱点。
S39 · 知识点:@Primary 与 @Qualifier
【题目】关于 @Primary 与 @Qualifier,下列说法正确的是?
A. @Primary 用于在同类型多 Bean 中标记首选;@Qualifier 用于在注入点按名称精确指定 B. @Qualifier 只能加在类上,不能加在注入字段上 C. @Primary 与 @Qualifier 完全互斥,同一项目不能共存 D. @Primary 指定的 Bean 名称必须叫 primary
答案:A
【考点】依赖注入消歧两种手段的分工。
【结论】选 A。@Primary 解决“默认选谁”,@Qualifier 解决“这个点要谁”。
【逐项辨析】
- A 正确:一个是容器级首选,一个是注入点级点名。
- B 错误:@Qualifier 常用在字段、方法参数、类上。
- C 错误:可共存;注入点 @Qualifier 优先于 @Primary。
- D 错误:@Primary 是布尔注解,与 bean 命名无关。
【知识点】 依赖注入出现同类型多候选时,Spring 提供两级消歧手段:
- @Primary:标在 Bean 定义处(@Component 类或 @Bean 方法),表示「同类型多候选时,未额外限定的注入点默认选我」。它是标记注解,不接受名称参数,与 bean 是否叫 primary 无关。
- @Qualifier("name"):标在注入点(字段、构造器/方法参数,也可标在类上),表示「本注入点只要这个名字的 Bean」。 消歧优先级经验顺序:注入点显式 @Qualifier(或 @Resource 的 name)> @Primary > 与注入点同名的 Bean 名 > 抛 NoUniqueBeanDefinitionException。二者可以共存:一旦注入点写了 @Qualifier,就忽略 @Primary。本题 A 选项准确描述了分工——@Primary 管「默认选谁」,@Qualifier 管「这个点要谁」,故选 A。 典型用法:多数据源时在主数据源 Bean 上标 @Primary,个别 DAO 用 @Qualifier 指定从库;测试时在 @TestConfiguration 里用 @Primary @Bean 提供 mock 替身。注意:公共 starter 谨慎使用 @Primary,以免抢走业务方的注入点;@Primary 只解决注入期选择,不解决运行期策略路由。
【记忆锚点】「Primary 是班级默认班长,Qualifier 是点名册喊学号。」
【易混对比】
| 注解 | 标注位置 | 作用 |
|---|---|---|
| @Primary | Bean 定义处 | 同类型默认选我 |
| @Qualifier | 注入点 | 本注入点要指定名字 |
【自测】 同时存在 @Primary 的 BeanA 与注入点 @Qualifier("beanB"),最终注入谁?
答:BeanB。与 S38 连考。
【知识关联】
- 同库关联:与 S38(@Autowired/@Resource)、S17(注入基础)连考;测试中用 @Primary 替换真实 Bean 为 mock 是常见手法,也与 Boot 测试切片相关。
- 实现层:@Primary 标在 Bean 定义上(@Bean 方法或 @Component 类),表示「同类型多候选时,未额外限定的注入点优先选我」。@Qualifier("name") 标在注入点(字段、方法参数、类),表示「本注入点只要这个名字」。容器消歧优先级经验顺序:注入点显式 @Qualifier(或 @Resource name)> @Primary > 名称匹配 > 失败。二者可共存:若注入点写了 @Qualifier,则忽略 @Primary。@Primary 是标记注解,不接受名称参数;Bean 名称仍由组件扫描默认规则或 @Bean 方法名决定。测试替身:@TestConfiguration 中提供 @Primary @Bean mock,可覆盖生产实现而少改业务代码。
- 面试追问:① 测试如何替换 Bean?② 多个 @Primary 同类型会怎样?(歧义,仍可能失败)③ starter 里 @Primary 的风险? 【拓展延伸】
- 变式问法:① 同时存在 @Primary BeanA 与 @Qualifier("beanB") 注入点,问注入谁;② 问 @Qualifier 能标在哪些位置;③ 判断题「@Primary 指定 bean 名必须叫 primary」(错)。
- 版本差异:注解自 Spring 3.0 时代稳定;Boot 3 无语义变化;测试注解生态演进(如 @MockitoBean),但仍常与 @Primary 方案并存。
- 工程注意点:① 公共库/starter 不要随意 @Primary,避免抢走业务注入点;② 业务内多实现时,优先调用方 @Qualifier,把「默认策略」集中在配置层;③ 文档化默认实现;④ 反序列化/策略路由场景,@Primary 只解决注入,不解决运行期路由;⑤ 架构上可用配置项 + 条件装配代替硬编码首选。
S40 · 知识点:Spring Boot 自动配置细节
【题目】Spring Boot 2.7+ 中,自动配置类列表主要通过 classpath 下哪个路径的文件进行注册?
A. 启动类上的 @ComponentScan 自动扫描所有 jar 中的 @Configuration B. application.yml 中的 spring.autoconfigure 列表 C. META-INF/spring.factories(所有版本都只用它,且不存在其他注册方式) D. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
答案:D
【考点】自动配置元数据文件位置演进与条件装配。
【结论】选 D。Spring Boot 2.7+ 主用该 imports 文件;旧版主要用 META-INF/spring.factories 的 EnableAutoConfiguration 键。
【逐项辨析】
- A 错误:@ComponentScan 扫描用户代码包;自动配置走 AutoConfigurationImportSelector。
- B 错误:application 配置可用于 include/exclude,不是主注册文件。
- C 错误:spring.factories 是旧版主机制;“所有版本都只用它”不成立。
- D 正确:2.7+ 官方 imports 文件路径。
【知识点】
- 触发链:@SpringBootApplication → @EnableAutoConfiguration → AutoConfigurationImportSelector → 读取 imports/factories → @Conditional* 过滤。
- 常见条件:@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty。
- 用户优先:自定义同类型 Bean 时,自动配置常因 OnMissingBean 退让。
- 排查:
--debug/ ConditionEvaluationReport。
【记忆锚点】「2.7 后看 AutoConfiguration.imports;老项目 spring.factories;条件注解裁决谁上岗。」
【易混对比】
| 机制 | 位置 | 用途 |
|---|---|---|
| 自动配置(新) | ...AutoConfiguration.imports | 2.7+ 列表 |
| 自动配置(旧) | META-INF/spring.factories | 历史版本主用 |
| 用户组件扫描 | @ComponentScan | 自己的 Bean |
【自测】 自定义了 DataSource Bean 后,为何 DataSourceAutoConfiguration 往往不再接管?
答:常见
@ConditionalOnMissingBean(DataSource.class),用户优先。与 S02/S03 连考。
【知识关联】
- 同库关联:与 S01(@SpringBootApplication 组合)、S02(自动配置原理)、S03(条件装配)、J79(工厂思想)构成 Boot 启动与自动配置题群;写自定义 starter 是进阶面试点。
- 实现层:触发链:@SpringBootApplication → @EnableAutoConfiguration → @Import(AutoConfigurationImportSelector.class) → selectImports 读取候选列表 → 过滤(@ConditionalOnClass/OnMissingBean/OnProperty/OnBean 等)→ 注册生效配置类。元数据文件:Spring Boot 2.7+ 官方路径 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(每行一个自动配置类全限定名);历史版本主用 META-INF/spring.factories 的 EnableAutoConfiguration 键。2.7 为过渡期双读,3.x 以 imports 为主。用户自定义同类型 Bean 时,自动配置中 @ConditionalOnMissingBean 常让位,实现「用户优先」。排查:启动加 --debug 输出 ConditionEvaluationReport,或 Actuator 的 conditions 端点。
- 面试追问:① 如何写自定义 starter?② 自动配置为何「智能」?③ spring.factories 与 imports 能否同时写? 【拓展延伸】
- 变式问法:① 文件路径选择题;② 问条件注解匹配结果;③ 自定义 DataSource 后 DataSourceAutoConfiguration 是否仍接管。
- 版本差异:2.7 引入 AutoConfiguration.imports 作为新注册方式并过渡;3.0+ 主用新文件,部分场景不再读 spring.factories 的自动配置键(其他 SPI 用途仍在);Boot 3 基于 Spring Framework 6 / Jakarta;AOT 与 GraalVM native 下自动配置在构建期就被推理,条件反射受限,starter 需提供 hints。
- 工程注意点:① 写 starter 时同时兼容 2.7 imports 与旧 factories(若要覆盖旧版本);② 自动配置类用 @AutoConfiguration(2.7+)并显式 before/after;③ 条件装配优先 @ConditionalOnMissingBean 尊重用户定义;④ 用 --debug/conditions 报告排查「为什么我的 Bean 没生效」;⑤ 纳管 starter 版本与 Spring Boot 版本矩阵,避免类路径冲突。