Spring Boot · 第 07 章

07 · 自测题库

前六章建立了从容器到现代栈的机制链——这一章用强制回忆和跨章判别检验它是否真的进了你的 schema。总题 18 道,三层梯度。

本章你将建立的 schema

  • 概念层(对应 01–02):容器静态结构与 refresh 时序的精确记忆
  • 原理层(对应 03–05):自动配置、代理、属性绑定的机制推导
  • 应用判别层(跨章):真实故障场景下的多章联动诊断

§难度梯度金字塔

三个层次分别测试不同深度的理解:最底层只需准确记忆静态概念,中间层要求推导因果机制,顶层要求在真实故障场景中同时调动多章知识。

概念层 ch 01 · 02 — 容器结构 · refresh 时序 原理层 ch 03 · 04 · 05 — 自动配置 · 代理 · 配置绑定 应用判别层 跨章场景 · 需调动多章 6 题 回忆精度 7 题 因果推导 5 题 多章联动
图 7.1三层梯度金字塔:越往上,单道题激活的章节越多。注意:应用判别层的每道题都需要同时调动两章以上的机制——回答时写出"哪一章的哪个机制"是关键。

7.1概念层题目

对应 第 01 章(容器静态结构)和 第 02 章(refresh 时序)。合上教程,凭记忆作答——答案统一在文末展开。

概念层 · ch 01–02
  1. BeanFactory 和 ApplicationContext 都能 getBean(),二者有什么本质区别?列出 ApplicationContext 在 BeanFactory 基础上额外提供的至少三项能力。 提示:参考 01 章 §1.1
  2. BeanDefinition 和 Bean 实例的区别用一句话概括。为什么蓝图必须先于实例存在,这个设计解锁了什么能力? 提示:参考 01 章 §1.2
  3. BeanFactoryPostProcessor(BFPP)和 BeanPostProcessor(BPP)分别作用于什么对象、在容器生命周期的哪个阶段触发?用一句话说明"第一刀"的分界。 提示:参考 01 章 §1.3
  4. 为什么构造器注入的循环依赖必然抛出 BeanCurrentlyInCreationException,而 field 注入的循环依赖在 Spring 默认配置下却能解开?三级缓存的哪一级是关键? 提示:参考 01 章 §1.4
  5. refresh() 中,invokeBeanFactoryPostProcessors、registerBeanPostProcessors、finishBeanFactoryInitialization 三个阶段的执行顺序是什么?为什么必须是这个顺序? 提示:参考 02 章 §2.2
  6. 内嵌 Tomcat 在 refresh() 的哪个阶段启动?该阶段是在单例预实例化(finishBeanFactoryInitialization)之前还是之后? 提示:参考 02 章 §2.3

7.2原理层题目

对应 第 03 章(自动配置)、第 04 章(AOP 代理)、第 05 章(Web 与配置绑定)。这一层要求推导机制,而非只复述结论。

原理层 · ch 03–05
  1. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件取代了旧的 spring.factories 中的哪一部分?旧文件里仍然留有哪些内容没有搬走? 提示:参考 03 章 §3.3
  2. @ConditionalOnMissingBean 为什么能让用户的 @Bean 总是"赢过"自动配置的默认 Bean?请从解析顺序的角度解释,而不只是说"它检查 Bean 存不存在"。 提示:参考 03 章 §3.5
  3. 两个自动配置类之间的相对执行顺序,靠什么机制来保证?如果不声明顺序关系,@ConditionalOnMissingBean 的结果会出现什么问题? 提示:参考 03 章 §3.5
  4. Spring Boot 4.0 默认使用 CGLIB 代理而非 JDK 动态代理,这个行为由哪个配置项控制?CGLIB 无法代理 final 方法的根本原因是什么? 提示:参考 04 章 §4.2
  5. 同一个类内部的方法 a() 调用同类 b()(加了 @Transactional),事务不生效。请追溯到"代理不在调用栈"这一根因,并给出两种不改变调用方的修复方案。 提示:参考 04 章 §4.4
  6. Environment 中 PropertySource 的优先级链是什么顺序?命令行参数、OS 环境变量、application-prod.yml(prod profile 激活)、application.yml,按生效优先级从高到低排列。 提示:参考 05 章 §5.3
  7. 环境变量 MY_APP_PORT=9090 为什么能绑定到 @ConfigurationProperties(prefix="my.app") 的 port 字段,却绑定不到 @Value("${my.app.port}")?请说出 relaxed binding 的工作位置。 提示:参考 05 章 §5.4

7.3应用判别层题目(跨章场景)

每道题对应一个真实故障或选型场景。题目标注了所涉及的章节——作答时,须点名是"哪一章的哪个机制"造成了这个现象,以及如何系统性地排除。

应用判别层 · 跨章
  1. 一个 @Service 上标了 @Transactional,但数据库回滚并没有发生。请给出三种可能的根因,并说明对应的排查步骤: (a)自调用(self-invocation)失效; (b)方法是 private; (c)缺少 @EnableTransactionManagement(或 Boot 4.0 下相关自动配置未生效)。 三种场景的诊断方式有何不同? ch 04 + ch 01 提示:从"代理是否在调用栈"和"BPP 是否被正确注册"两个维度切入。
  2. 应用启动时抛出 BeanCurrentlyInCreationException。如何定位是哪两个 Bean 构成了构造器循环依赖环?有哪三种修法,各自的适用场景和代价是什么?Spring Boot 4.0 中为什么不能靠调整 spring.main.allow-circular-references=true 来永久解决? ch 01 + ch 02 提示:参考 01 章 §1.4 三级缓存说明和构造器注入的例外。
  3. 引入了一个第三方 starter,但预期中的默认 Bean 没有注入成功。请设计一个系统性的排查思路: (a)如何用 Actuator 的 /actuator/conditions(原 autoconfig)端点判断是 @ConditionalOnClass 没命中(classpath 缺少依赖)还是被用户 @Bean 顶掉? (b)如果 conditions 报告显示"matched"但 Bean 仍不存在,还有哪些其他可能? ch 03 提示:参考 03 章 §3.4 条件筛选流程。
  4. 运维在容器中通过环境变量 APP_DB_URL=jdbc:postgresql://host/db 传入数据库地址,但应用读到的是 null。排查发现代码里用的是 @Value("${app.db.url}")。请解释失败的机制原因,并给出两种修复方案,说明各自的原理。如果改用 @ConfigurationProperties,对应的字段命名应该是什么形式? ch 05 提示:参考 05 章 §5.4 relaxed binding 与 @Value 的对比。
  5. 一个 IO 密集型高并发服务(大量数据库查询 + 外部 HTTP 调用)正在评估三种调优方案:(A)开启虚拟线程(spring.threads.virtual.enabled=true)并扩大 HikariCP 连接池;(B)编译为 GraalVM 原生镜像;(C)保持平台线程但调大 server.tomcat.threads.max。请从吞吐量瓶颈、启动时间、运维复杂度、框架生态约束四个维度对比三个方案,并给出你的选型建议及前提条件。 ch 06 提示:参考 06 章 §6.3 虚拟线程与瓶颈转移,以及 §6.4 三条快启动路线。

7.4默写练习

读者绘图提示

合上教程,在纸上默写 refresh() 流水线:三条轨道(改蓝图轨 / 加工实例轨 / onRefresh 启 Tomcat 轨)加上三个关键阶段的先后顺序。

画完之后翻回 第 02 章图 2.2 对照——重点检查:你有没有把"Tomcat 在 onRefresh 阶段启动,早于 finishBeanFactoryInitialization 单例预实例化"画对?这是 refresh 时序里最容易画错的反直觉点。

如果画对了,再追问自己:为什么 Tomcat 要在单例还没全部造完时就开始监听端口——这个设计决定意味着什么?

想一想

如果把内嵌 Tomcat 的启动挪到 finishBeanFactoryInitialization 之后(即所有单例已就绪),会对应用有什么影响?

展开答案(先停 10 秒)

好处是:Tomcat 开始接流量时所有单例都已就绪,不存在"Bean 还没好但请求已进来"的窗口期。代价是:现有的 ApplicationRunner/CommandLineRunner 扩展点以及 SmartInitializingSingleton 机制会被打乱,而且 refresh() 完成前 SpringApplication 不会返回——这意味着一旦某个单例初始化极慢,端口要等很久才监听,Kubernetes 等就绪探针超时的风险增大。Spring 的设计是让 Tomcat 先占端口、最后再调 webServer.start()(在 finishRefresh 阶段)才真正接受外部流量——onRefresh 时只是 createWebServer,并非立即对外开放。

§全部答案

先完成所有题目再展开——顺序与题目保持一致,按三层分组。

展开全部答案(18 题,按层分组)

概念层答案(题 1–6)

  1. BeanFactory vs ApplicationContext:BeanFactory 是最基础的 DI 接口,只提供 getBean()、containsBean() 等原始 Bean 获取能力。ApplicationContext 继承 BeanFactory,额外提供:①AOP 集成(自动注册 BeanPostProcessor);②事件发布/订阅(ApplicationEventPublisher);③国际化(MessageSource);④Environment 与 profile 支持;⑤自动扫描(@ComponentScan)。日常使用的 AnnotationConfigServletWebServerApplicationContext 是后者的子类。
  2. BeanDefinition vs Bean:BeanDefinition 是"怎么造这个 Bean 的元数据"——包含类名、scope、构造参数、属性、是否懒加载、init/destroy 回调,不是 Bean 本身。蓝图先于实例存在,使得 BeanFactoryPostProcessor 能在实例化前读写这些元数据(例如替换占位符、修改 lazy-init),是整个"可插拔机制"的基础。
  3. 两类后置处理器的分界:BFPP(BeanFactoryPostProcessor)作用于蓝图(BeanDefinition),在所有 Bean 实例化前触发;BPP(BeanPostProcessor)作用于实例,在每个 Bean 初始化前后触发。一句话分界:改蓝图用 BFPP,改实例用 BPP。
  4. 构造器循环依赖为何解不了:创建 Bean A 需要先完整创建 Bean B(构造器参数),而创建 Bean B 又需要先完整创建 Bean A——这是死锁。三级缓存中关键的是第三级 singletonFactories:field/setter 注入时,容器在 Bean 实例化后、属性注入前就把"早期引用工厂"放入第三级缓存,对方可以先拿到这个半成品;构造器注入无法在进入构造器之前暴露任何引用,死锁无法打破,直接抛 BeanCurrentlyInCreationException。Boot 2.6+ 默认 allow-circular-references=false,field 注入的环也不再被允许。
  5. 三阶段顺序及原因:invokeBeanFactoryPostProcessors(阶段 5)→ registerBeanPostProcessors(阶段 6)→ finishBeanFactoryInitialization(阶段 11)。必须如此是因为:BFPP 要先把所有 BeanDefinition 收集并改写完(包括自动配置的 BeanDefinition 都在此加载);BPP 必须在单例实例化前就注册好,否则实例化时没有 BPP 监听,代理就来不及包(AbstractAutoProxyCreator 是 BPP,必须在阶段 6 就位)。
  6. 内嵌 Tomcat 启动阶段:在 onRefresh(阶段 9),ServletWebServerApplicationContext 重写该方法,调用 createWebServer() 初始化 Tomcat 并绑定端口。这早于 finishBeanFactoryInitialization(阶段 11)的单例预实例化——Tomcat 端口先占,业务单例后造。真正开始接受外部流量是在 finishRefresh(阶段 12)调用 webServer.start() 之后。

原理层答案(题 7–13)

  1. .imports 文件取代的部分:只取代了 spring.factories 中 org.springframework.boot.autoconfigure.EnableAutoConfiguration 键下的自动配置类列表(从 Boot 2.7 起)。spring.factories 中仍保留:EnvironmentPostProcessor、ApplicationContextInitializer、AutoConfigurationImportFilter、FailureAnalyzer、SpringApplicationRunListener 等条目——这些没有搬走。
  2. 用户 Bean 为何总赢(排序角度):AutoConfigurationImportSelector 是 DeferredImportSelector,延迟到用户所有 @Configuration 解析之后才执行。AutoConfigurationSorter 把自动配置类排在最后。当 @ConditionalOnMissingBean(它是 ConfigurationCondition,在 REGISTER_BEAN 阶段求值)执行时,用户的 BeanDefinition 已经在注册表里——条件判断到"Bean 已存在",自动配置的 @Bean 直接 back off。这不是魔法,是排序让用户先注册的必然结果。
  3. 自动配置类间相对顺序:靠 @AutoConfigureBefore / @AutoConfigureAfter / @AutoConfigureOrder 声明。如果两个自动配置类之间没有声明顺序,AutoConfigurationSorter 不能保证二者的相对位置,@ConditionalOnMissingBean 的结果取决于哪个先解析,行为不稳定——这是多个自动配置模块互相依赖时的常见陷阱。
  4. Boot 默认 CGLIB 的控制项:AopAutoConfiguration 把 spring.aop.proxy-target-class 默认设为 true,DefaultAopProxyFactory 据此选 CGLIB(ObjenesisCglibAopProxy)。CGLIB 无法代理 final 方法的根本原因:CGLIB 通过在运行时生成目标类的子类来拦截方法,final 方法不能被子类重写,因此代理子类无法覆盖它——拦截链无法插入,advice 不生效。
  5. self-invocation 根因与修复:根因:this.b() 直接调用原始对象,代理对象不在调用栈中,TransactionInterceptor 无法触发。修复方案:①把 b() 拆到另一个 Bean,通过外部注入调用(最干净);②在同类中自注入(@Autowired private ThisBean self;,再调 self.b()),注入的是代理;③改用 AspectJ 编译期/类加载期织入(不依赖代理,直接改字节码)。
  6. PropertySource 优先级(从高到低):命令行参数(--key=value)→ SPRING_APPLICATION_JSON(环境变量/系统属性中的 JSON)→ OS 环境变量 → application-prod.yml(profile-specific)→ application.yml(base)→ @PropertySource → 默认属性。先命中即生效,故命令行参数最高、@PropertySource 最低(仅高于默认属性)。
  7. relaxed binding 的位置:@ConfigurationProperties 由 ConfigurationPropertiesBindingPostProcessor(BPP)驱动,内部用 Binder 处理属性绑定,Binder 在解析属性名时应用 relaxed binding 规则(kebab-case / snake_case / SCREAMING_SNAKE_CASE / camelCase 归一),因此 MY_APP_PORT 能匹配到 port 字段。@Value 是由 AutowiredAnnotationBeanPostProcessor 处理的 SpEL/属性占位符,直接做精确字符串匹配,不经过 Binder,不支持 relaxed binding,所以 ${my.app.port} 找不到 MY_APP_PORT。

应用判别层答案(题 14–18)

  1. @Transactional 不回滚的三种根因诊断: (a)self-invocation:在日志/调试器里确认调用链是否经过代理(例如打印 this.getClass().getName(),如果是 $$EnhancerBySpringCGLIB 子类说明是代理调用;如果是原始类名说明绕过了代理)。修复:将方法拆到另一个 Bean,或自注入调代理。 (b)private 方法:CGLIB 生成子类时无法重写 private 方法,AbstractAutoProxyCreator(BPP,ch 01)不会对其生成拦截器。IDE 静态分析即可发现,改为 public 或 protected。 (c)缺 @EnableTransactionManagement:这意味着 TransactionManagementConfigurationSelector 未导入,InfrastructureAdvisorAutoProxyCreator(BPP)未注册,代理根本没有事务 advice。Boot 4.0 下 TransactionAutoConfiguration 自动配置了它,但若用户 exclude 了该自动配置类或没有 PlatformTransactionManager Bean,条件不满足,同样不生效。检查方式:/actuator/conditions 查看 TransactionAutoConfiguration 的 condition 评估结果。
  2. BeanCurrentlyInCreationException 的定位与修复: 定位:异常栈中包含"Requested bean is currently in creation: Is there an unresolvable circular reference?",通常会列出 Bean 名称。Spring Boot 4.0(默认 allow-circular-references=false)会在启动阶段更早地检测并报告循环链。开启 --debug 或查看 BeanDefinition 的构造器参数,能追溯两个 Bean 互相依赖的参数注入路径。 三种修法:①重构(最优)——提取公共依赖到第三个 Bean,打破环;②setter/field 注入替换构造器注入——只需把其中一条依赖边从构造器改为 @Autowired field,三级缓存可解,但破坏不可变性设计;③@Lazy 延迟注入——在其中一个构造器参数上标 @Lazy,注入的是代理占位符,代理在首次调用时才真正解析目标 Bean,代价是增加了运行时查找开销。 为什么不能靠 allow-circular-references=true:构造器循环依赖即使打开这个选项也无法被三级缓存解开(构造器注入在进入构造器时就需要完整实例),该选项只能豁免 field/setter 注入的环。
  3. starter 默认 Bean 不出现的排查: (a)访问 /actuator/conditions(需要在 management.endpoints.web.exposure.include 中暴露),找到对应的自动配置类。若"notMatched"段显示 OnClassCondition 失败,说明 classpath 缺少依赖,检查 pom.xml 是否引入了正确的 starter jar;若"notMatched"段显示 OnBeanCondition 失败("@ConditionalOnMissingBean … found beans …"),说明用户已有同类型 Bean,是预期的 back-off 行为。 (b)其他可能:@ConditionalOnProperty 所要求的属性未设置或值不匹配;自动配置类因 @AutoConfigureAfter 依赖的前置配置失败而跳过;自动配置类被用户在 @SpringBootApplication(exclude=…) 或 spring.autoconfigure.exclude 中手动排除。
  4. 环境变量不绑上的根因与修复: @Value("${app.db.url}") 做精确字符串匹配,查找的是属性键 app.db.url(点分小写),而 OS 环境变量 APP_DB_URL(下划线大写)经 relaxed binding 转换后等价于 app.db.url,但 @Value 不经过 Binder,直接在 Environment 的 PropertySource 链中精确匹配——匹配不到。 修复方案①:改用 @ConfigurationProperties(prefix="app.db") + POJO,字段名 url,Binder 会应用 relaxed binding 匹配 APP_DB_URL。 修复方案②:保留 @Value,但将环境变量改名为 APP_DB_URL → app.db.url(在 Docker/K8s 中设置属性格式的环境变量),即把点分小写形式直接作为环境变量键。 @ConfigurationProperties 对应的字段命名应为 camelCase dbUrl 或 kebab-case db-url(在 POJO 字段上),Binder 均能正确匹配。
  5. IO 密集型高并发服务选型对比: 方案 A(虚拟线程 + 扩大连接池):吞吐量瓶颈从线程数转移到 HikariCP 连接数(默认 10),需要将 spring.datasource.hikari.maximum-pool-size 调大(通常调至 DB 服务器能承受的最大连接数);启动时间与 JVM 启动相同;运维复杂度低;需要 Java 21+;框架生态无需改变。最适合已有 Spring 体系、Java 21+ 的 IO 密集型服务。 方案 B(GraalVM 原生镜像):吞吐量与 JVM 相当(稳态无优势);启动极快(50–100ms)、RSS 减半;运维复杂度高(闭世界要求所有反射/动态代理提前声明 AOT hint,三方库兼容性须验证);CI 编译时间长。适合追求快速冷启动的 FaaS/Serverless 场景,而非高并发长运行服务的主要选型理由。 方案 C(平台线程调大 threads.max):每个线程消耗约 1MB 栈,调至 1000+ 会带来显著内存压力,且线程上下文切换在高并发 IO 等待场景下效率低。这是虚拟线程出现前的老方法,现在不推荐。 建议:IO 密集 + 高并发 + 长运行 → 选方案 A,同步调大连接池;若部署场景有冷启动 SLA(如云函数)→ 可叠加方案 B,但需评估 AOT 适配成本。