用 Spring Debugger 拆穿 Spring Boot「魔法」:属性、Bean 与事务的真实链路#
Spring Boot 把对象生命周期、配置合并、条件装配和 AOP 代理叠在一起,日志里往往只剩一行「started successfully」。有经验的工程师知道问题多半出在启动链的某一环,但缺的是:在不往业务类里塞 ApplicationContext 的前提下,把「文件里写的值」和「容器里真的用的值」对齐,并把 Bean 定义、条件评估和代理边界看清楚。
IntelliJ 的 Spring Debugger 把这类检查收进断点会话:Evaluate Expression 旁路 Spring Properties;Beans 页签区分已加载与未加载定义;配置 inlay 提示运行时覆盖。下文按机制组织——属性在 Environment 阶段定型,Bean 在定义与实例化阶段分叉,事务在代理边界上生效——并用 Terminator 主题样例里的类名作锚点(演讲者观点:谜题用于教学,不宜直接当作团队规范)。若你维护的是 Spring Boot 3.x,EPP 注册键包名可能是 org.springframework.boot.env.EnvironmentPostProcessor;Boot 4 则迁移到 org.springframework.boot.EnvironmentPostProcessor,以所用版本 Javadoc 为准。
调试入口:Spring Properties 与「谁改写了这个键」#
为什么#
application.properties、application-prod.properties、命令行参数和环境变量可以并存。若团队只比对 YAML 字面量,很容易误判「prod 一定覆盖 base」或「命令行永远最高」——而 Externalized Configuration 还允许在上下文刷新之前用 EnvironmentPostProcessor 插入更高优先级的 PropertySource。
机制与约束#
Boot 文档写明:基于文件的源有固定顺序,且 command line properties always take precedence over file-based property sources。但 MutablePropertySources.addFirst(...) 会把新源放在最高优先级(Framework Javadoc)。因此「最终值」必须看运行时 Environment,不能只看投票时选中的那一层文件。
IntelliJ 在断点处通过 Evaluate Expression → Spring Properties 显示属性的实际解析值;Review the current configuration 说明 inlay 可展示覆盖关系,并 Navigate to the property redefinition——跳转到改写该值的代码,不限于某个 application-*.properties。
怎么做#
在 SpringApplication.run 返回后或任意已暂停的断点:
// Evaluate Expression → 右侧菜单选 Spring Properties → 输入键名
// 例如 terminator.main.mission
多 profile 文件布局示例(与演示 OCR 一致的多环境命名):

application-dev.properties、application-prod.properties 与 terminator-validators.json 等同屏出现,说明配置源不止一份 base 文件。

JavaonedemoApplication 的 main,spring.factories 注册 EnvironmentPostProcessor,Debug 窗口可见 Beans / Environment 等 Spring 专用页签。
属性谜题(terminator.main.mission 同时出现在 application.properties、application-prod.properties、环境变量与命令行)在投票环节容易选错层:不能单凭「prod 文件」或「命令行」猜答案;演示运行日志出现 Sarah Connor 时,应立刻怀疑 EPP 而非 profile 覆盖失败(见下一节)。
常见误区#
- 用「常识优先级」代替 Spring Properties 里的 result。
- 以为 Navigate 只会打开 properties 文件;自定义 EPP 的
postProcessEnvironment才是跳转目标。 - 只打开 Actuator
/env端点却未在最早断点观察 EPP 执行前的中间态(若未启用 Actuator,Spring Properties 是更轻量的替代)。
EnvironmentPostProcessor:在 Bean 工厂之前就定型的 Environment#
为什么#
「配置写在 A,日志却是 B」类问题,时间线要前移到 ApplicationContext refresh 之前。Boot 4.x 的 EnvironmentPostProcessor 在 prior to the application context being refreshed 被调用,用于合并 property source 或编程式改写。
机制与约束#
内置与自定义 EPP 按 Ordered / @Order 排序。演示中的 TerminatorAcronymEpp 读取 terminator.main.mission,做首字母缩写并应用特殊规则(如 acronym 为 Sarah 时变为 Sarah Connor),再通过 environment.getPropertySources().addFirst(propertySource) 写回——这与官方 addFirst 语义一致。
注册方式(Boot 3.x 常见键名;Boot 4 包名可能迁移,以所用版本 Javadoc 为准):
# META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor=\
more.riddles.infra.TerminatorAcronymEpp
public class TerminatorAcronymEpp implements EnvironmentPostProcessor {
private static final String PROPERTY_NAME = "terminator.main.mission";
@Override
public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication app) {
String mission = env.getProperty(PROPERTY_NAME);
// 演示逻辑:缩写 + 规则 → 新 PropertySource → addFirst
}
}

TerminatorAcronymEpp implements EnvironmentPostProcessor,PROPERTY_NAME = "terminator.main.mission"。

Sarah(大小写不敏感)时的分支逻辑。
排错时建议对照三条证据链:spring.factories(或 imports)里的 FQCN → EPP 源码里的 addFirst / 改写规则 → Spring Properties 中的最终字符串。演示注释写「Environment post-processor that transforms the terminator.main.mission property」,与字幕中「built-in EPP 先跑、自定义在后」的口述一致(演讲者观点:内置处理器数量约十余种,精确列表随 Boot 版本变化,不必背表,只需知道还有一层)。
常见误区#
- 在 Bean 已创建后再去改
Environment,为时已晚。 - 忽略 EPP 后仍坚持「命令行一定赢」——在
addFirst之后,命令行也可能被压过(机制并存,非文档矛盾)。 - 把
TerminatorAcronymEpp的演示输出Sarah Connor当成框架默认行为。
组件扫描、条件装配与 IDE 里的 Bean 状态#
为什么#
@ComponentScan 决定候选组件;Classpath Scanning 写明:候选类须匹配过滤器且有对应 bean definition 注册——不等于全部会实例化。@Conditional / @ConditionalOnProperty 在定义注册前就必须 match。
机制与约束#
切换 @ComponentScan(basePackages = "puzzler2") 会改变扫描列表,但 profile 或条件不满足时,IDE 可能列出类型却没有运行时 Bean(演讲者观点:静态导航与运行时装配可能脱节)。
IntelliJ — Review loaded beans:green = 已加载,transparent = 未加载,yellow = mock。这与「猜 Bean」相比,更可靠的是断点下看容器。
@SpringBootApplication
@ComponentScan(basePackages = "puzzler2") // 演示中在 puzzler1/2/3 间切换
public class JavaonedemoApplication { }

SpringApplication.run 与 === Application started successfully === 同屏,说明在改扫描包后验证的是运行结果而非仅项目树。
常见误区#
- 把 Project 视图里看到的
@Component类等同于「一定有一个单例 Bean」。 - 不结合 Spring Properties 查看条件属性是否真为
true。
@ConditionalOnProperty 与 legacy XML primary:两条定义路径#
为什么#
排查「为什么注入的不是带 @ConditionalOnProperty 的那个实现」时,必须同时查条件注解和 XML / @ImportResource 是否重复声明了同名 bean。
机制与约束#
@ConditionalOnProperty:默认 missing attributes do not match;havingValue = "true" 时属性须存在且匹配。条件不满足则不注册该组件定义。
演示中 IneedYourClothesTerminatorValidator 带:
@ConditionalOnProperty(
name = "terminator.model.ineedyourcloses.enabled",
havingValue = "true")
@Component
public class IneedYourClothesTerminatorValidator implements TerminatorValidator { }
Spring Properties 里没有该启用键,故扫描路径不应产生该 BeanDefinition;但若 legacy XML 仍声明同名 bean 且 primary="true",注入点会选 primary 候选——与类上的 @Conditional 无关(机制与官方条件语义一致;XML 细节以演示仓库为准)。

@ConditionalOnProperty 与 @Component 出现在 puzzler2.service 包下的 validator 类上。

application-prod.properties 与上述 validator 源码。
常见误区#
- 只看注解,不打开 Beans 视图或
getBeanDefinition查第二来源。 - 以为
@Component上的条件会「关掉」XML 里已注册的 bean。
Evaluate Expression:在调试会话里读 BeanDefinition#
为什么#
临时 @Autowired ApplicationContext 或打日志会污染代码。Spring Debugger 允许在表达式里访问 all properties and beans,even when not in the current execution context。
机制与约束#
从可见的 ConfigurableApplicationContext context 可向下取 BeanFactory,查看 depends-on、bean 名、注入字段实际类型。IntelliJ 文档未逐字列出 getBeanDefinition("skynet"),但该 API 属于 ConfigurableListableBeanFactory 常规用法(合理推断,非 IntelliJ 逐字保证)。
// 断点处(需 context 在作用域内):
context.getBeanFactory().getBeanDefinition("skynet");
// 或对字段:skynet.getTerminatorValidator().getClass().getName()

main 线程 RUNNING,项目树列出多个 TerminatorValidator 实现供对照注入结果。

Evaluate Expression...,与查看 SpringApplication 导入同屏。
常见误区#
- 只在 Variables 窗格看字段快照,不查 定义阶段 的
primary/depends-on。 - 在未暂停或未进入含
context的栈帧时求值失败却归咎于 Spring。
List<T> 注入:显式 @Bean List 与按类型收集#
为什么#
同时存在 @Bean List<TerminatorValidator> 和多个 TerminatorValidator 实现时,Skynet 构造函数里的 List 内容并不直观。Framework 对集合注入的默认语义是:向 List<T> 注入时聚合容器中该类型的 bean(Using @Autowired)。
机制与约束#
演讲中的谜题选项包括:仅 Rev9(显式 List bean)、T800+AI(个体 bean)、三者皆有、或异常。演讲者称 Spring Boot 3.3 之前行为与部分观众直觉不同,3.3 之后演示走向「按类型收集 validators」(演讲者观点;本次未能从 Boot 3.3 Release Notes 独立核实 changelog,升级务必用集成测试验证)。


context = {AnnotationConfigApplicationContext@...},与 SpringApplication.run(JavaonedemoApplication.class, ..args 同屏。

List<TerminatorValidator> 的内容,选项含 Rev9、T800+AI、全部三者或 Exception。
怎么做#
断点停在 Skynet 构造后:
terminatorValidator.size();
terminatorValidator.stream().map(Object::getClass).toList();
常见误区#
- 假设
List.of(new Rev9...)的@Bean一定会原样注入。 - 不区分 Boot 小版本 就照搬旧谜题答案。
Spring Framework 7 的 BeanRegistrar:定义阶段动态注册#
为什么#
validator 清单来自 JSON、类名由运维配置携带时,用 Class.forName + 程序化注册比手写 dozens of @Bean 更贴近现实;包名重构会导致启动期 ClassNotFoundException。
机制与约束#
Programmatic Bean Registration(Framework 7):通过 @Import 导入实现 BeanRegistrar 的类,在 尚无 bean 实例 时调用 register(BeanRegistry, Environment)。BeanRegistry.Spec 支持 order、primary、prototype、lazyInit、description 等;当前 Javadoc 无 qualifier(...)——演讲者称 qualifier 仍依赖类上的 @Qualifier(与 API 面一致,属演讲者归纳)。
public class TerminatorValidatorRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
for (var v : loadConfiguration().getValidators()) {
Class<?> clazz = Class.forName(v.getClassName()); // 拼写错误 → 启动失败
registry.registerBean(v.getBeanName(), clazz,
spec -> {
if ("prototype".equalsIgnoreCase(v.getScope())) spec.prototype();
spec.order(v.getOrder());
});
}
}
}

TerminatorValidatorRegistrar implements BeanRegistrar,loadConfiguration() 从外部配置加载。

registerValidator(BeanRegistry registry, ValidatorConfigDTO.ValidatorDefinition ...) 方法可见。
启动失败时可加 --debug 查看 condition evaluation report(Boot 常规手段;演示控制台 OCR 含 Beans / Health / Mappings / Environment 页签)。
常见误区#
- 在 Bean 已实例化后试图用 Registrar「补注册」同名单例。
- 认为 JSON 里的 FQCN 会随 IDE 重构自动更新。
启动时间线:把问题钉在正确的阶段#
为什么#
用 BeanPostProcessor 解释属性来源、或用 EPP 解释事务代理,都会错位。教学上可把启动粗分为四段(综合模型;Spring 无与下述完全同序的单页官方示意图,各段均有独立文档支撑——见 P09 核实结论)。
机制与约束#
| 阶段 | 典型动作 | 权威锚点 |
|---|---|---|
| EnvironmentPostProcessors | 合并/改写 PropertySources | Boot EnvironmentPostProcessor Javadoc |
| BeanRegistrar / 定义注册 | 注册 BeanDefinition,无实例 | Framework 7 programmatic registration |
| BeanFactory 实例化 | singleton / prototype 创建 | Bean lifecycle 文档 |
| BeanPostProcessor | 初始化前后增强、AOP 代理 | factory-extension |


BeanFactoryPostProcessor,说明在 bean 实例化之前可修改 bean definition(与 BPP 阶段区分)。
常见误区#
- 混淆 BeanFactoryPostProcessor(改定义)与 BeanPostProcessor(改实例)。
- 在已 refresh 的上下文里用调试器反推 EPP 顺序却不看
spring.factories注册列表。
Prototype 与销毁回调:@PreDestroy 别用在错误 scope 上#
为什么#
四选一谜题常考「prototype 上的 @PreDestroy 会不会在 context.close() 时执行」。若误以为所有 scope 都会走统一销毁链,prototype 资源可能泄漏。
机制与约束#
Prototype scope:Spring does not manage the complete lifecycle;configured destruction lifecycle callbacks are not called;client code must clean up。@PostConstruct 仍可在创建时调用;@PreDestroy 主要可靠场景是 singleton(与演讲结论一致)。
@Component @Scope("prototype")
class T800 {
@PostConstruct void init() { }
@PreDestroy void shutdown() { /* 勿指望容器在 close 时调用 */ }
}
常见误区#
- 用
context.close()验证 prototype 的@PreDestroy。 - 把电影梗当成容器行为。
事务与自调用:REQUIRES_NEW 经不起 this.save()#
为什么#
@JavaOneTransactionalService 组合了 @Service 与类级 @Transactional(REQUIRED, timeout=10000);save 方法标 REQUIRES_NEW。若 saveAll 内部直接调用 save,不经代理,则方法级传播不生效。
机制与约束#
Spring 文档明确:self-invocation does not lead to an actual transaction——even if the invoked method is @Transactional。外部经代理进入 saveAll 时外层 REQUIRED 事务存在;内部 this.save() 无新事务;若外层因异常回滚,演示结论为 Nothing will be saved(演讲简化场景,完整分析还须看 rollbackFor 与异常传播)。
@Retention(RetentionPolicy.RUNTIME)
@Service
@Transactional(propagation = Propagation.REQUIRED, timeout = 10_000)
public @interface JavaOneTransactionalService {}
@JavaOneTransactionalService
public class TerminatorTalkingModule {
public void saveAll() {
save(record); // 自调用 → 无 REQUIRES_NEW 代理边界
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Record r) { }
}

package com.jb.terminator_transactions.annotations 与 @Retention 元注解组合事务服务接口。

spring.datasource.url=jdbc:postgresql://localhost:5432/terminator_db 与 spring.application.name=terminator_transactions 同屏。
深入理解代理边界见 Understanding AOP Proxies。
若必须在内层使用独立事务,应通过注入自身代理、ApplicationContext.getBean 获取当前 bean,或拆到另一个 Spring bean——而不是在同类里直接 this.save()。演示工程里 PostgreSQL 与 spring.jpa.hibernate.ddl-auto=update 只为让「是否落库」可见;生产环境仍应以事务日志与集成测试验证回滚路径,而非只记谜题口号「Nothing will be saved」。
常见误区#
- 认为方法上的
REQUIRES_NEW一定能「救」内层调用。 - 不区分 注入的代理 与 this 引用。
- 把组合元注解
@JavaOneTransactionalService当成会自动修复自调用的语法糖。
可复现的排错清单#
- 属性:断点 → Spring Properties → 查键 → Navigate 到覆盖逻辑 → 查
META-INF/spring.factories中的 EPP。 - Bean 是否存在:Beans 页签 /
getBeanDefinition/ 条件属性与 XMLprimary。 - 集合注入:对
List字段求size()与getClass(),勿信旧版本谜题记忆。 - 动态注册:核对 JSON FQCN、
--debug、Registrar 的scope/order。 - 事务:对外部调用单步进入代理;警惕同类内部调用。
- 版本:EPP 注册键、Boot 3.3 前后
List行为、Framework 7BeanRegistrar均以当前依赖版本文档为准,勿跨版本套用谜题答案。
掌握调试器并不能替代阅读 Reference,但能把「魔法」还原为可点击的源码与定义——这正是 Spring Debugger 的设计目标:在断点处回答「这个值从哪来、这个 Bean 为何存在、这次调用有没有经过代理」。
参考与延伸阅读#
- IntelliJ IDEA — Spring debugger(Evaluate / Spring Properties / Loaded beans)
- Spring Boot Reference — Externalized Configuration
- Spring Boot 4.0.6 — EnvironmentPostProcessor API
- Spring Framework — Classpath Scanning
- Spring Boot — @ConditionalOnProperty
- Spring Framework — Using @Autowired(集合注入)
- Spring Framework 7.0.7 — BeanRegistrar
- Spring Framework — Programmatic Bean Registration
- Spring Framework — Container Extension Points(BFPP vs BPP)
- Spring Framework — Bean Scopes(prototype 销毁)
- Spring Framework — Declarative Transaction Management(self-invocation)
- Spring Framework — Understanding AOP Proxies
- Spring Framework — MutablePropertySources.addFirst
- Spring Framework — @Conditional
- Spring Boot — SpringApplication run 与调试启动(IDE 运行配置文档入口)



