Spring 底层机制与扩展点
这篇文章怎么用
Spring 底层机制不能只背“Bean 生命周期有几步”。真正写项目时,问题通常更扎手:@Transactional 写了但数据没回滚,AOP 切面没命中,自定义 Starter 抢了业务 Bean,升级后循环依赖直接启动失败,一个 BeanPostProcessor 让半个系统的行为变得不可见。
本文按工程排障顺序讲 Spring 底层机制:先说明范围和诊断入口,再把 BeanDefinition -> BeanFactory -> ApplicationContext -> BeanFactoryPostProcessor -> BeanPostProcessor -> 实例化 -> 属性填充 -> Aware -> 初始化 -> AOP 代理 -> 事务拦截器 -> 循环依赖 -> 自动配置扩展点 串成一条完整链路。你读完后要能回答三件事:运行时到底经过了哪些对象,线上现象能从哪里反推,新增扩展点上线前要怎么验证和约束。
本文范围
本文不展开 Spring MVC 请求链路、Spring Boot 启动全流程、数据库事务隔离和分布式事务,它们分别属于独立主题。这里聚焦容器、代理、事务拦截和自动配置扩展点的边界。
版本基线
新项目可以以 Spring Boot 4.1.x / Spring Framework 7.0.x 为基线;Boot 4.1.0 要求 Java 17 及以上,并要求 Spring Framework 7.0.8 或以上。仍在 Boot 3.x、Framework 6.x 的项目,不要因为看到新版本就直接跨主版本升级,先锁定 BOM,补回归测试,再处理 Jakarta、Servlet、JDK、三方 Starter 的兼容问题。
前置条件
这篇文章默认你已经会写普通的 @Service、@Configuration、@Transactional 和 Spring Boot 应用。为了让每个机制可验证,建议准备一个最小 Demo 工程,至少包含:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>如果老项目仍使用 spring-boot-starter-web 或 spring-boot-starter-aop,升级 Boot 4 时要按官方迁移说明确认 starter 命名、传递依赖和三方 starter 兼容性;不要只改父版本就上线。
企业内网和离线环境里,重点不是“能不能访问官网”,而是依赖闭环:BOM、插件、父 POM、私服缓存、构建镜像、测试依赖都要能在内网稳定解析。本文只讲开发侧机制和诊断命令,不展开 Maven 私服、镜像仓库和离线部署。
先打开诊断入口
场景说明:Spring 的很多行为发生在容器刷新、后置处理器注册、Bean 创建和代理创建阶段。没有诊断入口,只看业务代码,很容易把“代理没生效”误判成“注解没扫描到”。
直接做法,开发环境打开少量 Actuator 端点和关键日志。生产环境不要照抄,尤其不要把 beans、conditions、env 这类端点暴露到公网。
management:
endpoints:
web:
exposure:
include: health,beans,conditions,loggers,startup,metrics
endpoint:
health:
show-details: when-authorized
logging:
level:
org.springframework.boot.autoconfigure: info
org.springframework.beans.factory.support: info
org.springframework.transaction.interceptor: trace
org.springframework.aop: debug如果要看启动阶段耗时,给 SpringApplication 配置 BufferingApplicationStartup。它会把启动步骤缓存在内存中,后续可以通过 startup 端点导出来看。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(DemoApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
}
}验证结果:
./mvnw spring-boot:run
curl "http://localhost:8080/actuator/health"
curl "http://localhost:8080/actuator/beans"
curl "http://localhost:8080/actuator/conditions"
curl "http://localhost:8080/actuator/startup"启动失败或自动配置冲突时,加 --debug 看条件评估报告。
java -jar app.jar --debug常见坑:只看 health,不看 conditions;启动慢只盯业务初始化,不看 Bean 数量和启动步骤;线上临时打开诊断端点后忘记关闭。
生产建议:诊断端点要走内网、鉴权、最小暴露和变更记录。排障时临时提高日志级别可以,但要同时约定关闭时间。
完整链路:从定义到代理对象
先把主链路画出来。理解这张图,后面排查 AOP、事务、自动配置、循环依赖才不会乱。
源码入口不用逐行背,但排障时要知道该往哪里看:
| 环节 | 关键入口 | 排障意义 |
|---|---|---|
| 容器刷新 | AbstractApplicationContext#refresh | 看 BeanFactory 后处理器、BeanPostProcessor 注册和单例实例化顺序 |
| 定义修改 | PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors | 看配置类解析、BeanDefinition 注册和条件影响 |
| 后置处理器注册 | PostProcessorRegistrationDelegate#registerBeanPostProcessors | 看 AOP、注入、初始化注解等处理器是否进容器 |
| Bean 创建 | AbstractAutowireCapableBeanFactory#doCreateBean | 看实例化、属性填充、初始化、早期暴露 |
| 属性注入 | populateBean、AutowiredAnnotationBeanPostProcessor | 看 @Autowired、@Value、@Resource 为什么没注入 |
| AOP 代理 | AbstractAutoProxyCreator | 看代理是否创建、是否提前暴露、是否被跳过 |
| 单例缓存 | DefaultSingletonBeanRegistry | 看三级缓存和循环依赖现象 |
| 事务拦截 | TransactionInterceptor、TransactionAspectSupport | 看事务属性、事务管理器、回滚规则和传播行为 |
这里要注意一个关键事实:初始化回调发生在原始目标 Bean 上,AOP 拦截器通常是在目标 Bean 完成创建后再套上去。所以不要指望 @PostConstruct 里面的自调用能触发事务或业务切面。
BeanDefinition、BeanFactory、ApplicationContext
场景说明:很多 Bean 冲突不是对象创建错了,而是定义阶段就已经错了。比如自动配置提前注册了一个默认 Bean,业务方又定义了同名 Bean;或者条件注解没命中,BeanDefinition 根本没有进入容器。
三者职责要分清:
BeanDefinition是配方,记录 Bean 的 class、scope、构造参数、属性、init、destroy、依赖关系等元数据。BeanFactory是工厂,负责按配方创建 Bean、管理单例缓存、处理依赖注入和生命周期。ApplicationContext是应用级容器,在 BeanFactory 之上增加 Environment、事件、资源加载、国际化、启动关闭等能力。
直接做法:遇到 Bean 找不到、Bean 冲突、自动配置不生效,先查“定义有没有进来”和“最终对象来自哪里”。
java -jar app.jar --debug
curl "http://localhost:8080/actuator/conditions" > conditions.json
curl "http://localhost:8080/actuator/beans" > beans.json如果是测试自动配置,优先用 ApplicationContextRunner,不要直接起完整应用。
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
class AuditAutoConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AuditAutoConfiguration.class));
@Test
void createsDefaultAuditClient() {
contextRunner
.withPropertyValues("company.audit.endpoint=http://audit.example")
.run(context -> assertThat(context).hasSingleBean(AuditClient.class));
}
}常见坑:
- 在
BeanFactoryPostProcessor里调用getBean(),导致普通 Bean 提前实例化,破坏标准生命周期。 - 把自动配置类放到业务扫描包里,被
@ComponentScan扫进来,条件和顺序都不可控。 - 只看类上有没有
@Component,不看条件注解、Profile 和配置项。
生产建议:Bean 冲突先看定义,不要急着改注入点。团队内部 Starter 要给每个默认 Bean 写清楚名称、条件、覆盖方式和排除方式。
Bean 生命周期:完整链路和最小验证
场景说明:Bean 生命周期会影响初始化、销毁、代理增强和扩展点顺序。很多“为什么注解没生效”的根因,都是把实例化、属性填充、初始化和代理阶段混在一起了。
一条更细的创建链路大致是:
合并 BeanDefinition
-> 推断构造方法
-> 实例化原始对象
-> 必要时暴露早期引用
-> 属性填充
-> Aware 回调
-> 初始化前 BeanPostProcessor
-> @PostConstruct
-> InitializingBean.afterPropertiesSet
-> 自定义 init 方法
-> 初始化后 BeanPostProcessor
-> 可能返回代理对象
-> 放入单例池
-> SmartInitializingSingleton / Runner / 事件
-> 停机销毁回调直接做法,写一个探针 Bean 验证顺序。
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class LifecycleProbe implements BeanNameAware, ApplicationContextAware,
InitializingBean, SmartInitializingSingleton, DisposableBean {
@Override
public void setBeanName(String name) {
System.out.println("1 setBeanName " + name);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("2 setApplicationContext");
}
@PostConstruct
public void postConstruct() {
System.out.println("3 @PostConstruct");
}
@Override
public void afterPropertiesSet() {
System.out.println("4 afterPropertiesSet");
}
@Override
public void afterSingletonsInstantiated() {
System.out.println("5 afterSingletonsInstantiated");
}
@PreDestroy
public void preDestroy() {
System.out.println("6 @PreDestroy");
}
@Override
public void destroy() {
System.out.println("7 destroy");
}
}再写一个 BeanPostProcessor,看初始化前后是否能拿到同一个 Bean。
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class ProbeBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if ("lifecycleProbe".equals(beanName)) {
System.out.println("before init: " + bean.getClass().getName());
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if ("lifecycleProbe".equals(beanName)) {
System.out.println("after init: " + bean.getClass().getName());
}
return bean;
}
}验证结果:
./mvnw spring-boot:run | grep -E "setBeanName|PostConstruct|afterPropertiesSet|before init|after init"常见坑:
- 构造方法里访问还没注入完成的依赖。
- 初始化方法里调用本类的事务方法,以为能触发代理。
BeanPostProcessor不做类型收敛,导致每个 Bean 都走一遍复杂逻辑。- 资源只初始化不释放,停机时没有
@PreDestroy、DisposableBean或destroyMethod。
生产建议:生命周期里只放轻量、确定、可失败的初始化。缓存预热、远程注册、批量同步、配置拉取这些动作要有超时、日志、开关和失败策略,不要把服务启动绑死在非关键外部系统上。
扩展点顺序:先改定义,再改实例
场景说明:Spring 扩展点很多,但越靠近容器底层,影响面越大。一个扩展点该不该上生产,不能只看“能不能实现”,还要看失败时能不能观测、能不能回滚、会不会误伤。
先把 ApplicationContext#refresh 看成一条有插槽的启动流水线。设计评审时,不要只问“我要实现哪个接口”,而要先定位它插入哪一段:改定义的扩展点必须在普通 Bean 实例化前完成,改实例的扩展点必须控制处理范围,启动后的 Runner 和监听器不能反过来承担容器阶段的职责。
这张图用于排启动慢和扩展点误伤:startup 里如果慢在 spring.beans.instantiate,优先查 BPP、非懒加载单例和初始化方法;如果慢在刷新之后,才去查 Runner、监听器和远程 SDK。做公共 Starter 评审时,也可以沿图问三件事:它是不是在 BFPP 里拉了真实 Bean,它的 BPP 是否只命中目标类型,它有没有让 AOP 自动代理器之前的 Bean 提前创建。
按触发时机选择扩展点:
| 扩展点 | 触发时机 | 适合做什么 | 生产约束 |
|---|---|---|---|
BeanDefinitionRegistryPostProcessor | BeanDefinition 注册阶段 | 注册或补充 BeanDefinition | 必须控制顺序,避免重复注册 |
BeanFactoryPostProcessor | 普通 Bean 实例化前 | 修改配置元数据、占位符、默认属性 | 不要访问真实 Bean 实例 |
InstantiationAwareBeanPostProcessor | 实例化前后、属性填充阶段 | 构造器推断、注入点处理、特殊代理 | 类型判断要窄,避免拖慢全局 |
BeanPostProcessor | 初始化前后 | 包装、校验、代理、注解处理 | 必须有命中日志或测试 |
FactoryBean | 获取产品对象时 | 复杂对象或代理对象创建 | 要区分 bean 和 &bean |
ApplicationListener | 事件发布时 | 解耦通知、启动完成后的轻任务 | 默认同步监听器可能拖慢发布方 |
ApplicationRunner | 应用启动后 | 一次性检查、轻量初始化 | 长任务要异步或加开关 |
| AOP | 代理方法调用时 | 权限、审计、幂等、数据源标记 | 只放横切逻辑,明确顺序 |
| 自动配置 | Boot 条件装配阶段 | Starter 默认组件 | 必须允许业务覆盖和禁用 |
BeanFactoryPostProcessor 和 BeanPostProcessor 有一个容易被忽略的生产风险:它们自己会在容器启动的特殊阶段提前创建。Spring AOP 的自动代理本身也是一种 BeanPostProcessor,所以过早创建的后处理器、以及被它直接依赖的 Bean,可能拿不到完整后置处理和代理增强。日志里如果出现类似 not eligible for getting processed by all BeanPostProcessors,不要急着忽略,它通常在提醒你有 Bean 被提前实例化了。
更稳的写法是:返回 BFPP/BPP 的 @Bean 方法尽量声明为 static,方法参数不要依赖业务 Bean,后处理器内部也不要调用 applicationContext.getBean() 拉真实业务对象。需要业务配置时,优先读 Environment、BeanDefinition 元数据或轻量属性对象,不要把整个 Service 链路提前拽进容器启动阶段。
新增扩展点前,先回答这几个问题:
它在哪个阶段触发?
它影响哪些 Bean、方法或配置?
它失败时业务会看到什么现象?
它有没有日志、指标、测试和关闭方式?
它的执行顺序是否依赖别的扩展点?常见坑:
- 把业务规则塞进
BeanPostProcessor,新同事在业务代码里完全看不到。 ApplicationListener执行慢,发布事件的一方被拖住。ApplicationRunner做远程调用没有超时,服务迟迟不 ready。- AOP、事务、动态数据源都用
@Order,但没人说明谁应该先执行。
生产建议:建立扩展点登记表。AOP、动态数据源、权限、审计、事务增强、自动配置这类隐式能力,必须登记触发条件、顺序、影响范围、负责人、观测方式和回滚开关。
AOP 代理:创建时机和失效现场
场景说明:AOP 不生效最常见的原因不是切点表达式写错,而是调用没有经过代理对象。Spring AOP 是代理模型;在 Spring Boot 中,AOP 自动配置默认使用 CGLIB 代理,如果要改成 JDK 代理,可设置 spring.aop.proxy-target-class=false。
排查 AOP 和事务时,先沿下面这张图确认“调用有没有穿过代理边界”。事务只是 Advisor 链上的一种拦截能力;如果入口绕过代理,@Transactional、审计切面、动态数据源切面都会一起失效。
这张图在设计评审里有两个用法:一是检查入口是否稳定来自容器注入对象,避免把关键状态流转藏在同类自调用里;二是检查失败证据是否齐全,例如代理类型、切面命中日志、事务 trace 日志和回滚测试。线上如果出现“审计日志没打、事务也没回滚”,不要分头查两个框架,先按代理边界排一遍。
直接做法,写一个最小注解切面,并用 AopUtils 验证注入进来的到底是不是代理。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
String value();
}import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(100)
public class AuditAspect {
@Around("@annotation(audited)")
public Object around(ProceedingJoinPoint pjp, Audited audited) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
System.out.println("audit action=" + audited.value() + " cost=" + cost + "ms");
}
}
}import org.springframework.aop.support.AopUtils;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ProxyCheckConfig {
@Bean
ApplicationRunner proxyCheck(AccountService accountService) {
return args -> {
System.out.println("isAopProxy=" + AopUtils.isAopProxy(accountService));
System.out.println("isCglibProxy=" + AopUtils.isCglibProxy(accountService));
System.out.println("targetClass=" + AopUtils.getTargetClass(accountService));
};
}
}如果团队希望固定代理方式,可以显式写配置,避免不同服务靠默认值猜行为。
spring:
aop:
proxy-target-class: true常见坑:
- 同类内部
this.freezeAccount()调用会绕过代理。 final类不能被 CGLIB 继承,final或private方法也无法被覆盖增强。- 切点扫太宽,把 Controller、Mapper、基础客户端都包进去,日志和性能一起出问题。
- 多个切面顺序不清楚,审计、事务、数据源切换互相影响。
生产建议:AOP 只承载横切能力。权限、审计、幂等、限流、数据源标记可以用;核心业务状态流转不要藏进切面。关键切面必须有命中日志或指标,至少能回答“这个请求有没有真的进切面”。
事务:代理、回滚规则和传播边界
场景说明:@Transactional 是最容易“看起来正确”的注解。它本质上还是代理拦截:代理对象调用方法,TransactionInterceptor 读取事务属性,交给 PlatformTransactionManager 开启、提交或回滚事务。
直接做法,把事务入口放在 Service 层公开方法上,并用测试验证回滚。
@Service
public class TransferService {
private final AccountMapper accountMapper;
private final TransferLogMapper transferLogMapper;
public TransferService(AccountMapper accountMapper, TransferLogMapper transferLogMapper) {
this.accountMapper = accountMapper;
this.transferLogMapper = transferLogMapper;
}
@Transactional(rollbackFor = Exception.class)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
int deducted = accountMapper.deduct(fromId, amount);
if (deducted != 1) {
throw new IllegalStateException("deduct failed");
}
accountMapper.add(toId, amount);
transferLogMapper.insert(fromId, toId, amount);
}
}验证结果:
@SpringBootTest
class TransferServiceTest {
@Autowired
TransferService transferService;
@Autowired
AccountMapper accountMapper;
@Test
void shouldRollbackWhenTransferLogFailed() {
BigDecimal before = accountMapper.findBalance(1L);
assertThrows(Exception.class, () ->
transferService.transfer(1L, 2L, new BigDecimal("100.00")));
assertThat(accountMapper.findBalance(1L)).isEqualByComparingTo(before);
}
}排查事务是否进入拦截器,可以临时打开日志:
logging:
level:
org.springframework.transaction.interceptor: trace事务失败常见原因:
- 自调用:
this.innerTxMethod()没经过代理。 - 方法不是代理可拦截的公开方法。
- 异常被捕获后没有重新抛出。
- 默认只对运行时异常和
Error回滚,受检异常需要配置rollbackFor。 @Async切换线程后,事务上下文不在同一个线程。- 多数据源没有指定正确的事务管理器。
- 事务中做远程调用,外部接口超时后数据库锁还占着。
传播行为要特别小心:
REQUIRES_NEW会挂起外层事务并申请独立事务资源,连接池容量不足时可能把线程一起卡住。NESTED依赖保存点,通常映射到 JDBC savepoint;不是所有事务管理器和资源类型都支持。- 内层事务把当前事务标记为 rollback-only,外层仍然尝试提交时,可能看到
UnexpectedRollbackException。
生产建议:事务内只做必须一致的数据库操作。外部 HTTP、MQ、缓存、搜索索引、文件上传尽量放在事务外,或者用事务后事件、可靠消息、补偿任务处理。新增关键事务必须有回滚测试,尤其覆盖受检异常、异常吞掉、自调用、多数据源和传播行为。
循环依赖:三级缓存不是设计方案
场景说明:循环依赖通常不是“Spring 太严格”,而是服务职责缠住了。Boot 当前公共配置里 spring.main.allow-circular-references 默认是 false,即使临时打开,也不代表所有环都能被安全解决。
错误示例:
@Service
public class OrderService {
public OrderService(PaymentService paymentService) {
}
}
@Service
public class PaymentService {
public PaymentService(OrderService orderService) {
}
}Spring 能处理的典型场景,是部分单例 Bean 在实例化后、属性填充前提前暴露引用。三级缓存大致是:
| 缓存 | 作用 |
|---|---|
singletonObjects | 完整创建完成的单例 Bean |
earlySingletonObjects | 已提前暴露但生命周期没走完的早期 Bean |
singletonFactories | 创建早期引用的 ObjectFactory,必要时可返回早期代理 |
第三级缓存的关键价值,是在确实发生循环依赖时,才通过 getEarlyBeanReference 给 AOP 等代理机制一个提前包装的机会。没有循环时,Bean 仍按正常生命周期创建。
直接做法,先按业务依赖方向拆职责,不要第一反应开临时开关。
@Service
public class CheckoutService {
private final OrderService orderService;
private final PaymentService paymentService;
public CheckoutService(OrderService orderService, PaymentService paymentService) {
this.orderService = orderService;
this.paymentService = paymentService;
}
@Transactional(rollbackFor = Exception.class)
public void checkout(String orderNo, PaymentCommand command) {
orderService.lockOrder(orderNo);
paymentService.pay(command);
orderService.markPaid(orderNo);
}
}验证结果:
./mvnw test
java -jar app.jar --debug如果启动失败,错误里常见 BeanCurrentlyInCreationException 或依赖环提示。确实需要短期过渡时,可以局部使用 ObjectProvider 或 @Lazy,但要留下整改任务。
常见坑:
- 两个 Service 互相调用对方的“方便方法”,边界越来越乱。
- 为了解决循环依赖改成字段注入,设计问题被延后。
- 用事件替代同步调用后,关键状态更新变成异步,破坏一致性。
- 代理对象和早期引用混在一起,导致注入对象和最终单例对象理解不一致。
生产建议:循环依赖要按设计问题处理。能用应用服务编排就新增编排层;能拆能力接口就拆接口;能下沉共同逻辑就下沉领域服务。全局打开循环引用只能作为迁移期止血方案,不要作为长期配置。
自动配置:能默认,必须能退出
场景说明:内部基础库经常写 Starter,比如统一审计、统一 Redis、统一对象存储、统一接口签名。自动配置写得好,业务只引依赖就能用;写得差,就会抢 Bean、抢配置、启动变慢、冲突难查。
直接做法,新 Starter 使用 @AutoConfiguration,并通过 AutoConfiguration.imports 注册。
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnClass(AuditClient.class)
@EnableConfigurationProperties(AuditProperties.class)
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AuditClient auditClient(AuditProperties properties) {
return new AuditClient(properties.endpoint(), properties.timeout());
}
@Bean
@ConditionalOnMissingBean
public AuditAspect auditAspect(AuditClient auditClient) {
return new AuditAspect(auditClient);
}
}资源文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports内容一行一个自动配置类:
com.example.audit.autoconfigure.AuditAutoConfiguration自动配置排障不要只看“类在不在 classpath”。Boot 会先从 imports 文件拿候选自动配置,再结合 classpath、Environment、已有 BeanDefinition、Web 类型、条件注解和排除规则决定是否注册默认 Bean。
这张图用于 Starter 评审和冲突排查:如果 /actuator/conditions 里是 OnClassCondition 未命中,先补依赖;如果是 OnBeanCondition 未命中,先看业务 Bean 是否已经注册、返回类型是否精确、条件放在类上还是方法上;如果候选类根本没有出现,先查 imports 文件、排除配置和是否误用组件扫描。自动配置的目标不是“强行创建 Bean”,而是在业务没有明确选择时给出默认值,并且出事故时能从条件报告里解释清楚。
配置属性:
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "company.audit")
public record AuditProperties(
String endpoint,
Duration timeout,
boolean enabled
) {
}验证结果:
java -jar app.jar --debug
curl "http://localhost:8080/actuator/conditions" | grep AuditAutoConfiguration
curl "http://localhost:8080/actuator/beans" | grep auditClient常见坑:
- 自动配置类被组件扫描扫到,而不是通过 imports 加载。
- 自动配置类里再写
@ComponentScan,把一堆不该进来的组件拉进业务应用。 - 没有
@ConditionalOnMissingBean,业务方无法覆盖默认实现。 @Bean方法返回类型不够精确,条件判断和类型匹配变得含糊。- 没有禁用开关,出事故只能删依赖或排除自动配置。
生产建议:自动配置要像公共 API 一样维护。每个默认 Bean 都要有覆盖方式,每个关键能力都要有启用条件、禁用开关、配置元数据、条件报告和回滚版本。
启动变慢:从 refresh 步骤和扩展点反推
场景说明:启动慢不一定是 Spring 慢。常见原因是扫描范围过大、后置处理器处理太多 Bean、自动配置引入了不需要的客户端、初始化里连远程服务、Runner 做大任务、数据库连接或配置中心阻塞。
直接做法,先拿启动步骤和 Bean 数量。
curl "http://localhost:8080/actuator/startup" > startup.json
curl "http://localhost:8080/actuator/beans" > beans.json
curl "http://localhost:8080/actuator/conditions" > conditions.json临时验证懒加载是否能降低启动耗时:
spring:
main:
lazy-initialization: true这里要注意,懒加载只是诊断或特定场景优化手段。它会把一部分错误推迟到首次请求,不适合用来掩盖启动期配置错误。
排查顺序:
- 看
startup里耗时最高的步骤。 - 看 Bean 总量和新增 Starter 带来的自动配置。
- 看
BeanPostProcessor、ApplicationRunner、ApplicationListener是否做了重动作。 - 看数据库、配置中心、远程 SDK 是否在启动阶段获取连接。
- 看扫描包是否过大,是否把测试、历史、非当前服务模块扫进来。
生产建议:保留启动基线。每次新增 Starter、扩展点、Runner、远程客户端,都要观察启动耗时和 Bean 数量变化。重初始化动作必须有开关,服务 ready 不应依赖非关键外部系统。
可运行实验矩阵:把隐式机制跑出来
Spring Core 的机制最怕只在脑子里理解。下面这组实验可以放到一个最小 Demo 工程里,每次新增切面、事务、自动配置或后处理器时跑一遍。它的目标不是证明“Spring 很复杂”,而是让团队能用证据判断隐式能力有没有真的生效。
| 实验 | 直接做法 | 预期现象 | 反推问题 |
|---|---|---|---|
| BeanDefinition 是否注册 | /actuator/beans、/actuator/conditions | 能看到目标 Bean 和自动配置命中原因 | Bean 找不到、条件没命中、扫描范围错误 |
| BFPP 是否误触发实例化 | 写一个 BFPP 并观察 not eligible 日志 | 非 static 或依赖业务 Bean 时可能触发提前实例化 | 后处理器让 Bean 失去代理或完整后置处理 |
| BPP 处理范围 | 给 BPP 增加 beanName/type 命中日志 | 只命中目标类型,不扫全容器重逻辑 | 启动变慢、误伤业务 Bean |
| 生命周期顺序 | 运行 LifecycleProbe | Aware -> BPP before -> init -> BPP after -> SmartInitializingSingleton | 初始化阶段放错任务 |
| AOP 是否创建代理 | AopUtils.isAopProxy(bean) | 被切点命中的 Service 是代理对象 | 切点没命中、final/private、自调用 |
| 事务是否真的回滚 | 故意让写库后抛异常并断言余额不变 | 数据回滚,事务日志出现拦截信息 | 回滚规则、自调用、异常被吞 |
REQUIRES_NEW 资源压力 | 并发外层事务中调用内层 REQUIRES_NEW | 连接池不足时等待或超时 | 传播行为和连接池容量不匹配 |
| 循环依赖 | 构造器互相注入两个 Service | 启动失败并打印依赖环 | 设计职责缠绕 |
| 自动配置覆盖 | 业务侧声明同类型 Bean | 默认 Bean 因 @ConditionalOnMissingBean 退出 | 内部 starter 抢业务 Bean |
| 启动耗时基线 | BufferingApplicationStartup + /actuator/startup | 能定位耗时最高的启动步骤 | Runner、监听器、远程客户端拖慢启动 |
实验跑完以后,结果要进入代码评审,而不是只停在个人电脑上。新增扩展点的 PR 至少应该带上命中测试、失败测试和关闭方式;新增事务边界要带回滚测试;新增自动配置要带 ApplicationContextRunner 条件测试。Spring 的隐式机制一旦进公共库,就必须像公开 API 一样被验证。
落地工程深水区
代理失效
代理失效的本质是调用没经过代理对象。判断标准不要靠感觉,直接用 AopUtils.isAopProxy、切面命中日志和集成测试验证。
上线检查:
调用入口是否来自容器注入的 Bean?
是否存在同类内部调用?
类或方法是否 final?
方法是否 private 或不可见?
多个切面和事务是否有明确顺序?
切面是否有命中日志、指标或测试?事务失效
事务失效往往不会报错,而是数据已经提交。最可靠的验证是回滚测试。
生产约束:
- 事务入口放在应用服务层公开方法。
- 异常不能被吞掉。
- 受检异常需要配置
rollbackFor。 - 多数据源要指定事务管理器。
- 不在事务中做远程调用和长耗时任务。
REQUIRES_NEW使用前先核对连接池容量。
循环依赖
循环依赖会让模块边界变脆。短期可用 @Lazy 或 ObjectProvider 过渡,长期必须拆职责。
判断标准:
A 调 B 是不是为了业务编排?
B 调 A 是不是为了拿状态或工具方法?
能不能把编排上移到应用服务?
能不能把共同能力下沉到领域服务?
这个环如果靠事件解开,会不会破坏一致性?自动配置冲突
自动配置冲突最容易出现在公司内部 Starter。表现通常是业务自定义 Bean 没生效、条件没命中、Bean 重名、默认配置覆盖业务配置。
处理顺序:
- 看
/actuator/conditions,确认自动配置为什么生效或没生效。 - 看
/actuator/beans,确认最终 Bean 来自哪里。 - 给默认 Bean 增加
@ConditionalOnMissingBean。 - 给能力增加
enabled=false禁用开关。 - 必要时业务侧临时
exclude,同时修复 Starter。
扩展点治理
真正拖垮生产的不是某个扩展点本身,而是扩展点没人登记、没人测试、没人知道什么时候触发。上线前至少要明确:
- 触发条件和影响范围。
- 与事务、数据源、权限、缓存、异步的顺序关系。
- 失败时是阻断主流程、降级还是只记日志。
- 是否能通过配置关闭。
- 是否有单测、集成测试和回归样例。
- 是否会让启动耗时、Bean 数量、连接数明显上升。
排障手册
| 现象 | 优先判断 | 直接排查 | 修复建议 |
|---|---|---|---|
| Bean 找不到 | 扫描范围、Profile、条件没命中 | /actuator/beans、/actuator/conditions | 调整扫描包、条件注解或配置项 |
| Bean 被覆盖 | 默认 Bean 缺少保护 | beans 看 Bean 名称和来源 | 加 @ConditionalOnMissingBean 或改命名 |
| AOP 不生效 | 没经过代理 | AopUtils.isAopProxy、切面日志 | 改成外部调用、去掉 final/private、调整代理方式 |
| 事务不回滚 | 回滚规则或代理失效 | 事务 trace 日志、回滚测试 | rollbackFor、抛出异常、放到 Service 入口 |
UnexpectedRollbackException | 内层标记 rollback-only | 事务日志、传播行为 | 拆事务边界,避免吞内层异常 |
REQUIRES_NEW 卡住 | 连接池不足 | 连接池指标、线程栈 | 扩容连接池或改传播策略 |
| 自动配置没生效 | 条件不满足 | --debug、conditions | 补依赖、补配置、调整条件 |
| 循环依赖启动失败 | 职责互相调用 | 启动异常依赖链 | 新增编排层、拆服务、短期延迟注入 |
| 启动变慢 | 初始化或扫描过重 | startup、Bean 数量、日志时间戳 | 缩小扫描、延迟重任务、加超时 |
| Runner 卡启动 | 启动任务过重 | Runner 日志和 startup 步骤 | 改异步任务或加开关 |
| BeanPostProcessor 误伤 | 处理范围过宽 | before / after 日志 | 缩小类型判断和包范围 |
| 事件丢失或拖慢 | 同步监听器无补偿 | 事件日志、异常栈 | 明确同步/异步、失败补偿 |
命令速查
# 查看依赖版本
./mvnw dependency:tree | grep -E "spring-boot|spring-context|spring-aop|spring-tx|aspectj"
# 启动并打印条件报告
java -jar app.jar --debug
# Actuator 诊断
curl "http://localhost:8080/actuator/health"
curl "http://localhost:8080/actuator/beans"
curl "http://localhost:8080/actuator/conditions"
curl "http://localhost:8080/actuator/startup"
curl "http://localhost:8080/actuator/loggers/org.springframework.transaction.interceptor"
# 临时调整事务日志级别
curl -X POST "http://localhost:8080/actuator/loggers/org.springframework.transaction.interceptor" \
-H "Content-Type: application/json" \
-d '{"configuredLevel":"TRACE"}'Windows PowerShell 可以把 grep 换成 Select-String:
.\mvnw dependency:tree | Select-String "spring-boot|spring-context|spring-aop|spring-tx|aspectj"扩展点登记模板
名称:审计切面
类型:AOP
触发条件:标注 @Audited 的 public Service 方法
影响范围:com.example.*.service 包
执行顺序:@Order(100),需要说明在事务之前还是之后
失败表现:审计失败是否影响主流程
观测方式:audit.action、audit.cost、audit.error 日志和指标
开关:company.audit.enabled
测试用例:命中、不命中、异常、事务回滚、自调用失效
负责人:后端基础组
回滚方式:关闭配置或排除自动配置自动配置模板
@AutoConfiguration
@ConditionalOnClass(FeatureClient.class)
@EnableConfigurationProperties(FeatureProperties.class)
public class FeatureAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public FeatureClient featureClient(FeatureProperties properties) {
return new FeatureClient(properties.endpoint(), properties.timeout());
}
}META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importscom.example.feature.autoconfigure.FeatureAutoConfiguration上线检查清单
- 新增扩展点是否登记触发条件、影响范围、顺序和回滚方式。
- AOP 是否有代理验证和切面命中测试。
- 事务是否有回滚测试,是否覆盖受检异常、异常吞掉、自调用、多数据源和传播行为。
- 自动配置是否通过 imports 注册,是否没有被组件扫描直接扫到。
- 自动配置是否提供
@ConditionalOnMissingBean、启用条件和禁用开关。 /actuator/conditions是否能解释自动配置命中原因。- BeanPostProcessor 是否缩小处理范围,避免误伤。
- Runner 和监听器是否有超时、日志和失败策略。
- 循环依赖是否通过职责拆分解决,而不是长期依赖临时开关。
- 启动耗时、Bean 数量和 startup 步骤是否有基线。
- 诊断端点和日志级别是否不会在生产公网裸露。
官方文档入口
- Spring Framework 7.0.8 项目页
- Spring Boot 4.1.0 项目页
- Spring Boot 4.1.0 System Requirements
- Spring Framework Bean 生命周期
- Spring Framework 容器扩展点
- Spring Framework AOP 代理机制
- Spring Framework 声明式事务回滚
- Spring Framework 事务传播
- Spring Boot AOP 自动配置
- Spring Boot 自动配置开发
- Spring Boot Actuator Endpoints
- Spring Boot Application Startup tracking
