Spring 底层机制与扩展点
Spring 扩展点失效时先沿运行对象取证
Spring 底层机制不能只背“Bean 生命周期有几步”。真正写项目时,问题通常更扎手:@Transactional 写了但数据没回滚,AOP 切面没命中,自定义 Starter 抢了业务 Bean,升级后循环依赖直接启动失败,一个 BeanPostProcessor 让半个系统的行为变得不可见。
接下来按工程排障顺序,把 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 自动配置
