Spring Boot · 第 07 章
07 · 自测题库
前六章建立了从容器到现代栈的机制链——这一章用强制回忆和跨章判别检验它是否真的进了你的 schema。总题 18 道,三层梯度。
本章你将建立的 schema
- 概念层(对应 01–02):容器静态结构与 refresh 时序的精确记忆
- 原理层(对应 03–05):自动配置、代理、属性绑定的机制推导
- 应用判别层(跨章):真实故障场景下的多章联动诊断
§难度梯度金字塔
三个层次分别测试不同深度的理解:最底层只需准确记忆静态概念,中间层要求推导因果机制,顶层要求在真实故障场景中同时调动多章知识。
7.1概念层题目
对应 第 01 章(容器静态结构)和 第 02 章(refresh 时序)。合上教程,凭记忆作答——答案统一在文末展开。
-
BeanFactory和ApplicationContext都能getBean(),二者有什么本质区别?列出ApplicationContext在BeanFactory基础上额外提供的至少三项能力。 提示:参考 01 章 §1.1 -
BeanDefinition和 Bean 实例的区别用一句话概括。为什么蓝图必须先于实例存在,这个设计解锁了什么能力? 提示:参考 01 章 §1.2 -
BeanFactoryPostProcessor(BFPP)和BeanPostProcessor(BPP)分别作用于什么对象、在容器生命周期的哪个阶段触发?用一句话说明"第一刀"的分界。 提示:参考 01 章 §1.3 -
为什么构造器注入的循环依赖必然抛出
BeanCurrentlyInCreationException,而 field 注入的循环依赖在 Spring 默认配置下却能解开?三级缓存的哪一级是关键? 提示:参考 01 章 §1.4 -
refresh()中,invokeBeanFactoryPostProcessors、registerBeanPostProcessors、finishBeanFactoryInitialization三个阶段的执行顺序是什么?为什么必须是这个顺序? 提示:参考 02 章 §2.2 -
内嵌 Tomcat 在
refresh()的哪个阶段启动?该阶段是在单例预实例化(finishBeanFactoryInitialization)之前还是之后? 提示:参考 02 章 §2.3
7.2原理层题目
对应 第 03 章(自动配置)、第 04 章(AOP 代理)、第 05 章(Web 与配置绑定)。这一层要求推导机制,而非只复述结论。
-
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件取代了旧的spring.factories中的哪一部分?旧文件里仍然留有哪些内容没有搬走? 提示:参考 03 章 §3.3 -
@ConditionalOnMissingBean为什么能让用户的@Bean总是"赢过"自动配置的默认 Bean?请从解析顺序的角度解释,而不只是说"它检查 Bean 存不存在"。 提示:参考 03 章 §3.5 -
两个自动配置类之间的相对执行顺序,靠什么机制来保证?如果不声明顺序关系,
@ConditionalOnMissingBean的结果会出现什么问题? 提示:参考 03 章 §3.5 -
Spring Boot 4.0 默认使用 CGLIB 代理而非 JDK 动态代理,这个行为由哪个配置项控制?CGLIB 无法代理
final方法的根本原因是什么? 提示:参考 04 章 §4.2 -
同一个类内部的方法
a()调用同类b()(加了@Transactional),事务不生效。请追溯到"代理不在调用栈"这一根因,并给出两种不改变调用方的修复方案。 提示:参考 04 章 §4.4 -
Environment中 PropertySource 的优先级链是什么顺序?命令行参数、OS 环境变量、application-prod.yml(prod profile 激活)、application.yml,按生效优先级从高到低排列。 提示:参考 05 章 §5.3 -
环境变量
MY_APP_PORT=9090为什么能绑定到@ConfigurationProperties(prefix="my.app")的port字段,却绑定不到@Value("${my.app.port}")?请说出 relaxed binding 的工作位置。 提示:参考 05 章 §5.4
7.3应用判别层题目(跨章场景)
每道题对应一个真实故障或选型场景。题目标注了所涉及的章节——作答时,须点名是"哪一章的哪个机制"造成了这个现象,以及如何系统性地排除。
-
一个
@Service上标了@Transactional,但数据库回滚并没有发生。请给出三种可能的根因,并说明对应的排查步骤: (a)自调用(self-invocation)失效; (b)方法是private; (c)缺少@EnableTransactionManagement(或 Boot 4.0 下相关自动配置未生效)。 三种场景的诊断方式有何不同? ch 04 + ch 01 提示:从"代理是否在调用栈"和"BPP 是否被正确注册"两个维度切入。 -
应用启动时抛出
BeanCurrentlyInCreationException。如何定位是哪两个 Bean 构成了构造器循环依赖环?有哪三种修法,各自的适用场景和代价是什么?Spring Boot 4.0 中为什么不能靠调整spring.main.allow-circular-references=true来永久解决? ch 01 + ch 02 提示:参考 01 章 §1.4 三级缓存说明和构造器注入的例外。 -
引入了一个第三方 starter,但预期中的默认 Bean 没有注入成功。请设计一个系统性的排查思路:
(a)如何用 Actuator 的
/actuator/conditions(原autoconfig)端点判断是@ConditionalOnClass没命中(classpath 缺少依赖)还是被用户@Bean顶掉? (b)如果 conditions 报告显示"matched"但 Bean 仍不存在,还有哪些其他可能? ch 03 提示:参考 03 章 §3.4 条件筛选流程。 -
运维在容器中通过环境变量
APP_DB_URL=jdbc:postgresql://host/db传入数据库地址,但应用读到的是null。排查发现代码里用的是@Value("${app.db.url}")。请解释失败的机制原因,并给出两种修复方案,说明各自的原理。如果改用@ConfigurationProperties,对应的字段命名应该是什么形式? ch 05 提示:参考 05 章 §5.4 relaxed binding 与 @Value 的对比。 -
一个 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)
-
BeanFactory vs ApplicationContext:
BeanFactory是最基础的 DI 接口,只提供getBean()、containsBean()等原始 Bean 获取能力。ApplicationContext继承BeanFactory,额外提供:①AOP 集成(自动注册BeanPostProcessor);②事件发布/订阅(ApplicationEventPublisher);③国际化(MessageSource);④Environment与 profile 支持;⑤自动扫描(@ComponentScan)。日常使用的AnnotationConfigServletWebServerApplicationContext是后者的子类。 -
BeanDefinition vs Bean:
BeanDefinition是"怎么造这个 Bean 的元数据"——包含类名、scope、构造参数、属性、是否懒加载、init/destroy 回调,不是 Bean 本身。蓝图先于实例存在,使得BeanFactoryPostProcessor能在实例化前读写这些元数据(例如替换占位符、修改 lazy-init),是整个"可插拔机制"的基础。 -
两类后置处理器的分界:BFPP(
BeanFactoryPostProcessor)作用于蓝图(BeanDefinition),在所有 Bean 实例化前触发;BPP(BeanPostProcessor)作用于实例,在每个 Bean 初始化前后触发。一句话分界:改蓝图用 BFPP,改实例用 BPP。 -
构造器循环依赖为何解不了:创建 Bean A 需要先完整创建 Bean B(构造器参数),而创建 Bean B 又需要先完整创建 Bean A——这是死锁。三级缓存中关键的是第三级
singletonFactories:field/setter 注入时,容器在 Bean 实例化后、属性注入前就把"早期引用工厂"放入第三级缓存,对方可以先拿到这个半成品;构造器注入无法在进入构造器之前暴露任何引用,死锁无法打破,直接抛BeanCurrentlyInCreationException。Boot 2.6+ 默认allow-circular-references=false,field 注入的环也不再被允许。 -
三阶段顺序及原因:
invokeBeanFactoryPostProcessors(阶段 5)→registerBeanPostProcessors(阶段 6)→finishBeanFactoryInitialization(阶段 11)。必须如此是因为:BFPP 要先把所有 BeanDefinition 收集并改写完(包括自动配置的 BeanDefinition 都在此加载);BPP 必须在单例实例化前就注册好,否则实例化时没有 BPP 监听,代理就来不及包(AbstractAutoProxyCreator是 BPP,必须在阶段 6 就位)。 -
内嵌 Tomcat 启动阶段:在
onRefresh(阶段 9),ServletWebServerApplicationContext重写该方法,调用createWebServer()初始化 Tomcat 并绑定端口。这早于finishBeanFactoryInitialization(阶段 11)的单例预实例化——Tomcat 端口先占,业务单例后造。真正开始接受外部流量是在finishRefresh(阶段 12)调用webServer.start()之后。
原理层答案(题 7–13)
-
.imports 文件取代的部分:只取代了
spring.factories中org.springframework.boot.autoconfigure.EnableAutoConfiguration键下的自动配置类列表(从 Boot 2.7 起)。spring.factories中仍保留:EnvironmentPostProcessor、ApplicationContextInitializer、AutoConfigurationImportFilter、FailureAnalyzer、SpringApplicationRunListener等条目——这些没有搬走。 -
用户 Bean 为何总赢(排序角度):
AutoConfigurationImportSelector是DeferredImportSelector,延迟到用户所有@Configuration解析之后才执行。AutoConfigurationSorter把自动配置类排在最后。当@ConditionalOnMissingBean(它是ConfigurationCondition,在REGISTER_BEAN阶段求值)执行时,用户的 BeanDefinition 已经在注册表里——条件判断到"Bean 已存在",自动配置的@Bean直接 back off。这不是魔法,是排序让用户先注册的必然结果。 -
自动配置类间相对顺序:靠
@AutoConfigureBefore/@AutoConfigureAfter/@AutoConfigureOrder声明。如果两个自动配置类之间没有声明顺序,AutoConfigurationSorter不能保证二者的相对位置,@ConditionalOnMissingBean的结果取决于哪个先解析,行为不稳定——这是多个自动配置模块互相依赖时的常见陷阱。 -
Boot 默认 CGLIB 的控制项:
AopAutoConfiguration把spring.aop.proxy-target-class默认设为true,DefaultAopProxyFactory据此选 CGLIB(ObjenesisCglibAopProxy)。CGLIB 无法代理final方法的根本原因:CGLIB 通过在运行时生成目标类的子类来拦截方法,final方法不能被子类重写,因此代理子类无法覆盖它——拦截链无法插入,advice 不生效。 -
self-invocation 根因与修复:根因:
this.b()直接调用原始对象,代理对象不在调用栈中,TransactionInterceptor无法触发。修复方案:①把b()拆到另一个 Bean,通过外部注入调用(最干净);②在同类中自注入(@Autowired private ThisBean self;,再调self.b()),注入的是代理;③改用 AspectJ 编译期/类加载期织入(不依赖代理,直接改字节码)。 -
PropertySource 优先级(从高到低):命令行参数(
--key=value)→SPRING_APPLICATION_JSON(环境变量/系统属性中的 JSON)→ OS 环境变量 →application-prod.yml(profile-specific)→application.yml(base)→@PropertySource→ 默认属性。先命中即生效,故命令行参数最高、@PropertySource最低(仅高于默认属性)。 -
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)
-
@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 了该自动配置类或没有PlatformTransactionManagerBean,条件不满足,同样不生效。检查方式:/actuator/conditions查看TransactionAutoConfiguration的 condition 评估结果。 -
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 注入替换构造器注入——只需把其中一条依赖边从构造器改为@Autowiredfield,三级缓存可解,但破坏不可变性设计;③@Lazy延迟注入——在其中一个构造器参数上标@Lazy,注入的是代理占位符,代理在首次调用时才真正解析目标 Bean,代价是增加了运行时查找开销。 为什么不能靠allow-circular-references=true:构造器循环依赖即使打开这个选项也无法被三级缓存解开(构造器注入在进入构造器时就需要完整实例),该选项只能豁免 field/setter 注入的环。 -
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中手动排除。 -
环境变量不绑上的根因与修复:
@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对应的字段命名应为 camelCasedbUrl或 kebab-casedb-url(在 POJO 字段上),Binder均能正确匹配。 -
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 适配成本。