news 2026/9/14 15:33:05

Spring IOC源码学习:从声明式事务入口拆解代理机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring IOC源码学习:从声明式事务入口拆解代理机制

Spring IOC 源码学习,我从声明式事务的入口点开始拆

做了几年 Java 后端,Spring 天天在用,但真正下决心啃一次源码,还是因为被线上一个事务失效的问题折磨到头皮发麻。业务方法明明标了@Transactional,异常也抛了,数据就是没回滚。查来查去,问题出在同类内部调用上——方法绕过了代理对象,事务注解根本没被解析。那一刻我意识到,如果只是背面试题,不把 Spring IOC 和 AOP 的入口链路摸清楚,遇到这种问题永远只能靠猜。

于是我从声明式事务这个最常见的功能切入,把 Spring IOC 容器从启动到 Bean 实例化、再到 AOP 代理生成的全过程过了一遍源码。这篇文章就围绕一个核心问题展开:Spring 到底是在哪个入口点、通过什么机制,把@Transactional注解变成真实事务的

先说清楚一个容易混淆的点:这里的 IOC 是 Spring 的 Inversion of Control(控制反转),跟网络安全领域那个 IOC(Indicator of Compromise,威胁指标)完全是两码事。下面所有讨论,都限定在 Spring 容器和 Bean 管理的范畴内。

我将整个学习过程分成四块:先定位入口点,再拆 IOC 容器如何为事务代理铺路,然后沿着代理创建链路读到事务拦截器,最后讲怎么用调试把这条链路跑通。每一块都会给出源码位置、关键类、以及我在实际排查中踩过的坑。

1. 声明式事务入口点定位:从@EnableTransactionManagement开始

1.1 一个注解背后隐藏了三个核心角色

@EnableTransactionManagement是声明式事务的总开关。这个注解本身不干活,它只是向容器导入了一批配置类。顺着注解的@Import指令,会进入TransactionManagementConfigurationSelector,这个类根据mode属性的值决定导入哪些配置。

默认的modePROXY,在这种模式下会导入两个关键配置类:AutoProxyRegistrarProxyTransactionManagementConfiguration。前者负责向容器注册一个名为internalAutoProxyCreator的 BeanDefinition,指向InfrastructureAdvisorAutoProxyCreator;后者负责注册事务切面所需的通知器(Advisor)和通知(Advice)。

这里要特别注意:InfrastructureAdvisorAutoProxyCreatorAbstractAutoProxyCreator的子类,而AbstractAutoProxyCreator实现了BeanPostProcessor接口。BeanPostProcessor是 Spring IOC 容器对外暴露的扩展点,它允许在 Bean 实例化后、初始化前后插手干预。事务代理就是通过这个后置处理器,在所有普通 Bean 创建完成后统一植入的。

我把这条链路上的核心角色梳理成一张表,方便对照:

角色类名职责
总开关@EnableTransactionManagement导入配置类,开启事务能力
配置选择器TransactionManagementConfigurationSelector根据 mode 选择导入哪些配置
代理注册器AutoProxyRegistrar注册 InfrastructureAdvisorAutoProxyCreator
代理创建器InfrastructureAdvisorAutoProxyCreator实现 BeanPostProcessor,创建 AOP 代理
切面定义BeanFactoryTransactionAttributeSourceAdvisor定义事务切面的匹配规则和通知
事务通知TransactionInterceptor真正执行事务开启、提交、回滚逻辑

1.2 入口点的真正含义:不是拦截器,是 BeanPostProcessor

很多人学声明式事务,容易把注意力全放在TransactionInterceptor上。这个类确实承载了事务的核心逻辑,但严格来说,它不是入口点。真正的入口点是AbstractAutoProxyCreator.postProcessAfterInitialization方法。

为什么这么说?因为声明式事务本质上是 AOP 的一种应用场景,而 AOP 代理在 IOC 容器中的创建时机,是由BeanPostProcessor的生命周期回调决定的。容器中每个 Bean 实例化并完成属性填充、初始化之后,Spring 都会回调所有BeanPostProcessorpostProcessAfterInitialization方法。AbstractAutoProxyCreator在这个方法里判断:当前 Bean 是否需要被代理?如果需要,就创建代理对象返回;如果不需要,就返回原始对象。

换句话说,声明式事务的入口点,是 IOC 容器创建 Bean 的最后一个环节。理解了这个,才能理解为什么事务失效总是和“对象是否经过代理”强相关。这是后面所有排查经验的根基。

1.3 为什么 Spring 要把事务实现放到 IOC 容器里

这个设计初看有点绕:事务管理不是独立功能吗,为什么要跟 IOC 容器耦合得这么深?答案是,@Transactional声明的是“哪些方法需要事务”,这个信息本身是 Bean 元数据的一部分。Spring 需要把注解信息、Bean 实例、代理逻辑三者串联起来,而 IOC 容器是唯一能同时拿到这三样东西的地方。

  • 注解信息在 BeanDefinition 或目标类 / 方法的AnnotationMetadata上;
  • Bean 实例由容器创建并持有;
  • 代理逻辑需要借助 BeanFactory 查找 Advisor、解析事务属性。

BeanPostProcessor恰好处于容器管理 Bean 的流程中,天然有机组合这三者。这也是为什么事务、缓存、异步这些基于 AOP 的功能,全部依赖 IOC 容器扩展点来实现,而不是各自搞一套独立代理机制。理解这一点,对后续阅读@EnableCaching@Async的源码也有直接帮助,它们的入口点和事务几乎一模一样。

2. 先看懂 IOC 容器管理 Bean 的主流程

2.1 从 refresh() 到 getBean():Bean 从哪里来

Spring IOC 容器的核心是AbstractApplicationContext.refresh()方法,这是整个容器启动的总入口。refresh()内部经历十几个步骤,包括环境准备、BeanDefinition 加载、BeanFactory 后置处理、BeanPostProcessor 注册、事件发布等。

其中与事务代理关系最紧密的是finishBeanFactoryInitialization(beanFactory)这一步,它调用beanFactory.preInstantiateSingletons(),遍历所有非懒加载的单例 BeanDefinition,逐个调用getBean()

getBean()最终会走到AbstractAutowireCapableBeanFactory.createBean(),方法内部依次执行:

  • resolveBeforeInstantiation:如果 Bean 实现了InstantiationAwareBeanPostProcessor,在这里有机会返回代理对象;
  • doCreateBean:真正实例化 Bean,进行属性填充、初始化回调;
  • initializeBean:触发BeanPostProcessor的前置和后置处理。

事务代理走的不是resolveBeforeInstantiation那条短路逻辑,而是在initializeBean里的postProcessAfterInitialization阶段介入。这里有一个很重要的顺序问题:Bean 的属性注入已经完成,afterPropertiesSetinit-method也已经执行完,此时 Bean 已经是一个“完备”的对象,只是还没经过代理包装。

2.2 三级缓存与循环依赖:代理对象为什么不会破坏缓存

既然已经准备读取三级缓存的内容了,就顺手把循环依赖和 AOP 的关系讲透。Spring 解决循环依赖靠的是DefaultSingletonBeanRegistry里的三个 Map,俗称三级缓存:

  • 一级缓存singletonObjects:存最终成品 Bean;
  • 二级缓存earlySingletonObjects:存早期暴露的原始 Bean(尚未完成属性填充);
  • 三级缓存singletonFactories:存ObjectFactory工厂,用于生成早期引用。

三级缓存的设计意图很关键:Spring 在实例化 Bean 后、属性填充前,就把一个ObjectFactory放入三级缓存。这个工厂内部会回调getEarlyBeanReference,而AbstractAutoProxyCreator重写了getEarlyBeanReference方法。

这里有个经典面试题:如果 A 和 B 循环依赖,且 A 需要被事务代理,Spring 是怎么保证注入给 B 的是代理对象而不是原始对象的?

答案分两步走:

  1. A 实例化后放入三级缓存,此时工厂的getEarlyBeanReference被调用(如果 B 在属性填充时引用了 A,会触发提前暴露);
  2. getEarlyBeanReference内部调用wrapIfNecessary,检查 A 是否命中 Advisor,如果命中,直接创建一个早期代理对象。

这个早期代理会替代原始对象注入给 B,最终 A 完成初始化后,一级缓存存代理对象,B 拿到的也是代理对象。两级缓存存的是同一个代理引用,所以不会出现 B 持有普通对象、而容器最终持有代理对象这种不一致的问题。

我在看这段源码时最大的感受是:Spring 的设计者把“代理创建时机”提前到了三级缓存暴露阶段,而不是等到postProcessAfterInitialization才处理,就是为了保证循环依赖场景下代理的一致性。如果只看postProcessAfterInitialization,会漏掉这个重要分支。

2.3 BeanPostProcessor 的注册顺序与优先级

容器中远不止事务一个BeanPostProcessor。Spring Boot 环境下,内建的BeanPostProcessor有十几个,比如AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessorApplicationContextAwareProcessor等。

这些后置处理器的执行顺序由PriorityOrderedOrdered@Order控制。InfrastructureAdvisorAutoProxyCreator实现了Ordered接口,默认顺序是Ordered.LOWEST_PRECEDENCE,也就是容器中大多数后置处理器都跑完之后,它才执行。

这个设计是有讲究的。如果事务代理创建得太早,其他后置处理器(比如@Autowired属性注入处理器)还没来得及往 Bean 里注入依赖,代理对象拿到手的就只是一个“半成品”。反过来,放在最后执行,可以确保被代理的 Bean 已经是完整的、经过所有初始化回调的对象。

我自己调试的时候会专门在postProcessAfterInitialization里打断点,然后看调用栈里的BeanPostProcessorChecker,它能打印每个 Bean 被哪些后置处理器处理过。这个类在AbstractApplicationContext里,日志级别调到 DEBUG 就能看到,对理解执行顺序特别有帮助。

3. 源码级拆解:事务代理创建的核心链路

3.1postProcessAfterInitializationwrapIfNecessary的完整逻辑

事务代理的创建集中发生在AbstractAutoProxyCreator.postProcessAfterInitialization方法中,源码逻辑清晰,步骤如下:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; }

第一步先判断当前 Bean 是否已经在循环依赖早期暴露时创建过代理。earlyProxyReferences这个 Map 会在getEarlyBeanReference阶段记录 beanName 与原始对象,如果当前传入的bean和记录一致,说明已经处理过,直接返回,避免二次包装。

如果没处理过,进入wrapIfNecessary。核心逻辑如下:

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { // 如果已经被处理过,返回原对象 if (this.targetSourcedBeans.contains(cacheKey)) return bean; // 如果是不需要代理的类型(Advice、Advisor、BeanPostProcessor 等),返回原对象 if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass())) return bean; // 查找当前 Bean 匹配的 Advisor Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); // 创建代理对象 Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; }

这里面有几个关键过滤条件:

  • isInfrastructureClassAdvisorAdviceAopInfrastructureBean的子类不会被代理,避免代理类再次被代理形成死循环;
  • shouldSkipInfrastructureAdvisorAutoProxyCreator会跳过BeanPostProcessorAopInfrastructureBean类型的 Bean;
  • getAdvicesAndAdvisorsForBean:核心匹配入口,它有专门的适配逻辑,下一节详细说。

3.2getAdvicesAndAdvisorsForBean:事务切面如何匹配到目标 Bean

getAdvicesAndAdvisorsForBean的实现位于AbstractAdvisorAutoProxyCreator,这是AbstractAutoProxyCreator的另一个子类。Spring Boot 场景下最终生效的InfrastructureAdvisorAutoProxyCreator同时继承了这两个类的逻辑。

匹配过程可以简化为三步:

  1. 从容器中找出所有Advisor类型的 Bean;
  2. 过滤出实现了IntroductionAwareMethodMatcher或普通MethodMatcher的切面;
  3. 针对目标 Bean 的所有方法,逐一调用Advisor的匹配逻辑,判断是否命中。

事务相关的AdvisorBeanFactoryTransactionAttributeSourceAdvisor。它的静态内部类TransactionAttributeSourcePointcut实现了MethodMatcher,匹配逻辑就一句话:方法或类上是否能解析出事务属性。

public boolean matches(Method method, Class<?> targetClass) { TransactionAttributeSource tas = getTransactionAttributeSource(); return (tas == null || tas.getTransactionAttribute(method, targetClass) != null); }

这里的getTransactionAttributeSource()拿到的就是AnnotationTransactionAttributeSource。它内部会先查方法上的@Transactional注解,如果方法没有,就查类上的;类上也没有,最终会走到 Spring 默认的内部方法查找逻辑,查接口方法上的@Transactional

这部分代码值得细细读一遍。AnnotationTransactionAttributeSource.determineTransactionAttribute里有一个fallback链,它通过AnnotationParsingUtils解析注解属性,把propagationisolationtimeoutreadOnlyrollbackFor等配置一一封装成RuleBasedTransactionAttribute

我读源码时踩过一个坑:事务属性解析是支持从接口方法上继承的,但需要满足一个条件——被调用的目标方法必须是通过接口暴露的方法。如果一个类实现了接口 A,接口 A 的某个方法标了@Transactional,但实现类里的方法定义没标,那么默认情况下事务是可以生效的,因为AnnotationTransactionAttributeSource提供publicMethodsOnly模式。但如果你在实现类里新增了接口没有的方法,并给这个方法标了事务,它照样能生效。反过来,如果接口方法标了事务,实现类方法也标了但故意改成了不传播,结果以实现类为准。这个优先级顺序我后面做表格列出来。

3.3createProxy:JDK 动态代理还是 CGLIB

当匹配命中后,wrapIfNecessary调用createProxy创建代理对象。这里涉及的类有ProxyFactoryAdvisedSupportDefaultAopProxyFactory

DefaultAopProxyFactory.createAopProxy的逻辑是:

if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); }

翻译成业务规则就是:

  • 如果目标类不是接口,且proxyTargetClass为 true(Spring Boot 默认),走 CGLIB;
  • 如果目标类本身就是接口,走 JDK 动态代理;
  • 如果目标类有接口但没设置proxyTargetClass,默认走 JDK 动态代理。

Spring Boot 2.x 之后,spring.aop.proxy-target-class默认是 true,所以大多数业务场景用的是 CGLIB。CGLIB 通过继承目标类生成子类作为代理,因此目标类不能是 final,被代理的方法也不能是 final。

createProxy还有一个容易被忽略的点:ProxyFactory会先调用buildAdvisors,把所有匹配到的 Advisor 收集起来,再按@Order排序,最后塞进AdvisedSupport。多个事务切面共存时,排序决定了嵌套事务先走哪一个逻辑。

3.4 代理对象返回后:容器持有的是代理,不是原始 Bean

代理对象创建完成后,postProcessAfterInitialization返回这个代理对象,Spring 会把它作为最终的 Bean 放入一级缓存。后续所有注入这个 Bean 的地方,拿到的都是代理对象。

这一点是整个声明式事务生效的前提,也解释了自调用失效的原因。自调用失效的完整链路是这样的:

  • Spring 容器中UserService对应的对象是UserService$$EnhancerBySpringCGLIB的实例;
  • 对外暴露的方法都走了TransactionInterceptor
  • 但是UserService内部this.save()调用,this指向的是代理对象内部的原始目标对象,不会经过代理对象;
  • 因此@Transactional注解虽然还在,但TransactionInterceptor根本没机会执行。

我可以在Interceptor的 invoke 方法里打上断点来观察这个现象:外部调用会进入断点,自调用不会。这也是我建议所有排查事务失效问题的人首先做的一个验证动作。

4. 事务拦截器内部机制:代理创建之后发生了什么

4.1TransactionInterceptor如何获取事务属性

TransactionInterceptor实现MethodInterceptor,它的 invoke 方法直接调用了TransactionAspectSupport.invokeWithinTransaction

public Object invoke(MethodInvocation invocation) throws Throwable { Class<?> targetClass = AopUtils.getTargetClass(invocation.getThis()); return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); }

在进入事务逻辑之前,需要先拿到事务属性。TransactionAspectSupport内部持有TransactionAttributeSource,默认情况下就是AnnotationTransactionAttributeSource。它会根据当前调用的方法,解析出TransactionAttribute,包含了传播行为、隔离级别、超时时间、只读标志、回滚规则等信息。

这里有个容易被忽略的性能点:AnnotationTransactionAttributeSource内部做了缓存,同一个方法第一次解析后,结果会缓存在ConcurrentMapCache里,后续调用直接命中缓存,不会再做注解反射解析。所以不用担心每个方法调用都走一遍注解扫描。

4.2 事务属性解析优先级:方法、类、接口、默认值

在实际编码中,事务属性可能分散在多个位置,解析顺序必须清楚。我整理了一份我排查时经常参考的优先级表:

优先级查找位置说明
1当前方法上的 @Transactional决定了当前调用最精确的行为
2当前方法所在类的 @Transactional类级别配置,方法级没有就取类级
3实现接口方法上的 @Transactional仅 public 方法,且需通过接口调用链解析
4默认值默认传播 REQUIRED,隔离 DEFAULT,不回滚受检异常

特别提醒一下:接口方法上的@Transactional在 CGLIB 代理下不一定能解析到。Spring 官方文档推荐把@Transactional写在实现类方法上,而不是接口方法上。原因在于AnnotationTransactionAttributeSource默认有publicMethodsOnly限制,且 CGLIB 代理的getTargetClass拿到的是目标类而不是接口,接口注解的解析依赖BridgeMethodResolver,容易踩坑。

4.3invokeWithinTransaction:事务的开启、提交与回滚

invokeWithinTransaction是整个事务执行逻辑的中枢核心,大致流程如下:

  1. 根据事务属性决定是否存在一个事务;
  2. 如果存在,通过TransactionManager(在 Spring Boot 中通常是DataSourceTransactionManager)获取或创建事务;
  3. 执行目标方法;
  4. 正常返回时提交事务;
  5. 捕获异常时根据回滚规则决定回滚或提交。

我们以DataSourceTransactionManager为例看它怎么实现事务的开启。AbstractPlatformTransactionManager.getTransaction方法中有几种情况:

  • 如果当前没有事务,doBegin会通过DataSourceUtils.getConnection获取数据库连接,然后con.setAutoCommit(false),这就是数据库事务开始的本质;
  • 如果当前已经存在事务,会根据传播行为决定是否挂起当前事务、开启新事务,或者加入当前事务;
  • 新事务绑定到当前线程:通过TransactionSynchronizationManager.bindResource,把连接和DataSource的映射存到ThreadLocal里。

提交和回滚的逻辑也要看底层。doCommit调用con.commit()doRollback调用con.rollback()。这里有一个面试经常问的问题:RuntimeExceptionError默认回滚,受检异常默认提交。这个默认规则在DefaultTransactionAttribute.rollbackOn方法里定义,可以通过rollbackFornoRollbackFor来修改。

4.4 传播行为在源码中的处理位置

传播行为是声明式事务最常用的配置项,源码处理也集中在AbstractPlatformTransactionManager里。

PROPAGATION_REQUIRED是默认值。它的逻辑是:如果当前线程已经绑定了一个事务,直接加入;否则新建一个。对应源码handleExistingTransaction里的一个分支。

PROPAGATION_REQUIRES_NEW的逻辑是:先把当前事务挂起(suspend会保存原来的连接和同步状态),然后新开一个数据库连接,绑定到ThreadLocal。这里的关键是“挂起”而不是“结束”,等新事务提交后,再把原来的事务恢复绑定。

PROPAGATION_NESTED在 Spring 中默认依赖数据库的 savepoint(保存点)机制。DataSourceTransactionManager会通过 JDBC 3.0 的SavepointAPI 实现嵌套。这个和REQUIRES_NEW的区别在于:嵌套事务回滚只回滚到保存点,外层事务仍可继续提交;而REQUIRES_NEW是彻底的新事务,与外层事务互不干扰。

5. 实操心法:把声明式事务源码链路调试跑通

5.1 快速搭建一个最小可调试工程

读源码最怕只看不跑。我建议你直接用 Spring Boot 3.x 搭一个最小工程,数据库用 H2 内存库,不需要额外安装任何东西。

工程结构很简单:

  • 一个UserService,提供createUsercreateUserWithError两个方法;
  • 一个User实体类,JPA 或者 JdbcTemplate 都行;
  • 一个测试类,分别调用正常方法和抛异常方法,观察数据是否回滚。

关键点在于,不要把@Transactional和业务代码放在同一个类里测试自调用,要额外写一个内部类或者从外部注入另一个 service 来测试跨类调用。自调用失效是必须亲自复现的。

@Service public class UserService { @Transactional public void createUser(String name) { User user = new User(); user.setName(name); userRepository.save(user); // 这个方法用于模拟场景 } public void createUserThenError() { createUser("haha"); throw new RuntimeException("强制回滚"); } public void createUserSelfCall() { // 自调用是不会走事务代理的 } }

5.2 设置断点的最佳位置

我建议在以下四处打上断点,按照调用顺序依次观察:

  • AbstractAutoProxyCreator.postProcessAfterInitialization:可以看到容器中每个 Bean 都会经过这里,特别是UserService经过时,会走到wrapIfNecessary
  • AbstractAutoProxyCreator.wrapIfNecessary:重点看getAdvicesAndAdvisorsForBean返回值,如果返回DO_NOT_PROXY,说明事务切面没匹配到这个 Bean;
  • BeanFactoryTransactionAttributeSourceAdvisor$TransactionAttributeSourcePointcut.matches:这是事务切面匹配的关键入口,确认@Transactional是否被解析到;
  • TransactionInterceptor.invoke:这个方法一旦进入,说明代理生效,接下来就能看到事务的完整生命周期。

调试时,把断点停在wrapIfNecessary,用 IDEA 的 Evaluate Expression 功能调用getAdvicesAndAdvisorsForBean,能直接看到匹配结果。这是我排查事务是否被代理的最快方式。

5.3 通过日志验证代理类型和事务边界

除了断点,日志也能帮助验证。在application.yml中配置:

logging: level: org.springframework.transaction: DEBUG org.springframework.jdbc.datasource: DEBUG org.springframework.aop: DEBUG

开启后,DataSourceTransactionManager会打印Creating new transaction with nameInitiating transaction commitInitiating transaction rollback等日志。这些日志是验证事务边界最直接的证据。

如果发现打印了事务开启日志但数据没回滚,问题大概率出在回滚规则配置上;如果连事务开启日志都没有,问题就是代理没生效,回到第 2 节第 3 节的链路里查。

5.4 一个完整的排查记录:从入口点入手定位问题

举一个我最近处理的案例。一个同事反馈,某个导入接口超时后数据残留严重,明明方法标了@Transactional(rollbackFor = Exception.class)

排查的第一步,我打开了 DEBUG 日志,发现Creating new transaction with name [xxx.importData]压根没打出来,说明方法根本没有被事务代理。

于是我在wrapIfNecessary打上断点,发现getAdvicesAndAdvisorsForBean返回了DO_NOT_PROXY。继续追,发现TransactionAttributeSourcePointcut.matches里的getTransactionAttribute返回 null。

最终定位到原因:这个方法所在的类被另一个切面(自定义的MethodInterceptor)通过@Aspect拦截了,而自定义切面在shouldSkip阶段导致InfrastructureAdvisorAutoProxyCreator提前跳过了这个 Bean 的代理。具体原因是那个自定义切面实现了Ordered接口且顺序值非常小,它的matches方法里抛了一个ClassCastException,被shouldSkip捕获后标记为跳过代理。

这个案例带给我最大的教训是:shouldSkip这个方法不是只做类型过滤的,它也会调一遍切面的匹配方法。如果一个切面的匹配逻辑有 bug,可能波及整个代理创建流程。排查时一定要看这个方法的调用栈。

6. 常见问题速查与避坑指南

6.1 为什么同类内部调用事务会失效

这个问题上面已经展开讲过了。根源是this调用绕过了代理对象。解决方案有三个层级:

  • 把内部调用拆到另一个 Service 类,让调用经过容器注入的代理对象;
  • 在当前类注入自身(@Autowired@Resource),通过代理对象调用;
  • 使用TransactionTemplate手动控制事务边界,绕开声明式事务的代理机制。

方案三在复杂嵌套场景下最可控,但也意味着放弃了声明式事务的便捷性,取舍看业务场景。

6.2 捕获异常后为什么事务没有回滚

这个问题我几乎每次培训都要强调:try-catch吞掉异常,事务就永远不会知道发生错误,自然不会回滚。

@Transactional public void doSomething() { try { execute(); } catch (Exception e) { log.error("失败", e); } }

上面这段代码即使在方法内捕获了异常,事务还是会正常提交。想要在捕获异常后回滚,有两个思路:

  • 不捕获,让异常抛到invokeWithinTransaction里;
  • 捕获后手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(),标记回滚。

第二种方式的原理是向当前事务同步状态写入回滚标记,事务提交前检查这个标记发现需要回滚,就会走回滚逻辑。我用这个方式解决过不少“必须吞异常但又想回滚”的场景。

6.3 为什么非 public 方法上标注@Transactional无效

声明式事务的代理匹配逻辑,在AnnotationTransactionAttributeSource中默认只处理 public 方法。这是AbstractFallbackTransactionAttributeSourcegetTransactionAttribute逻辑决定的,它内部通过allowPublicMethodsOnly控制。

为什么这么设计?因为 JDK 动态代理只能代理接口方法,CGLIB 虽然能代理 public 和 protected 方法,但对 private 方法无能为力。为了让两种代理方式行为一致,Spring 干脆统一限制为 public 方法。如果你确实需要在非 public 方法上开启事务,就只能改用编程式事务。

6.4rollbackFor到底要不要写

这是一个经典争论。从源码角度看,默认回滚规则是RuntimeExceptionError,受检异常默认提交。因此:

  • 如果你认为业务中某个受检异常也要回滚,就必须写rollbackFor = Exception.class,或者更精确地指定异常类型;
  • 如果业务中所有操作抛出的都是运行时异常,不写也能正确回滚。

我的建议是显式写rollbackFor = Exception.class,代价很小,收益是减少了“受检异常导致数据半提交”的踩坑概率。这个建议来自我维护过的一个老系统,那里面大量IOExceptionParseException都不在默认回滚范围内,事故出了好几次。

6.5 同一个类中多个@Transactional方法相互调用会怎样

如果同一个类中methodA调用了methodB,两个方法都标了@Transactional,按照前面的分析,methodA是通过代理对象调用的,会进入事务;但methodA内部直接调用methodB时,没有经过代理,所以methodB上更细粒度的事务配置不生效。

methodA开启事务后,整个调用都在methodA定义的事务边界内。如果需要两个方法分别使用独立事务,只能拆到不同的类里,或者把methodB改成通过代理调用。

6.6 读源码时最值得复刻的三个扩展点

学习 IOC 和 AOP 源码,最终目的不是为了面试,而是能在日常开发中正确使用 Spring 的扩展点。我个人总结出三个最有价值的扩展点,建议亲手写一遍 demo:

  • BeanPostProcessor:容器生命周期干预,适合做统一初始化校验、包装器生成;
  • BeanFactoryPostProcessor:在 BeanDefinition 注册完成后修改 BeanDefinition,适合做配置统一改写;
  • ImportBeanDefinitionRegistrar:动态注册 BeanDefinition,很多框架性功能(比如 MyBatis 的@MapperScan)都依赖它。

声明式事务入口点的学习,本质上是把这几个扩展点串起来理解。InfrastructureAdvisorAutoProxyCreator是一个BeanPostProcessorTransactionManagementConfigurationSelector是一个ImportSelector,它们共同把“切面定义”和“代理创建”两件事连接到 IOC 容器上。

我在实际项目里用同样的思路实现过一个公司内部的操作审计功能:定义@AuditLog注解,写一个Advisor,通过BeanPostProcessor对所有带注解的方法生成代理,在方法前后记录操作日志。整个过程完全复刻了 Spring 声明式事务的架构。可见这套模式的可复制性有多强。

写在最后的经验体会

我把这些内容沉淀成文,最核心的收获其实是这句话:源码学习不要从容器最底层开始啃,要从一个具体功能反推。声明式事务就是一个完美的切入样本,它既涉及 IOC 容器的 Bean 生命周期,也涉及 AOP 的代理机制,还关联 JDBC 事务的本质。

最初读源码时我也走过弯路,一上来就抱着DefaultListableBeanFactory看,结果被各种继承关系绕晕。后来调整策略,从@EnableTransactionManagement这个注解作为起点,跟着调用链往下追,每一层只搞清楚三个问题:这个方法由谁调用?参数从哪来?返回给谁?追完三分之一的链路,整个体系的轮廓就清晰了。

之后遇到@Async@Cacheable@Retryable这些基于 AOP 的注解,再看源码,基本是同一套模板:注册切面定义、创建自动代理、匹配方法、执行拦截器。底层逻辑完全一致,区别只是AdvisorMethodInterceptor的实现不同。

最后分享一个小技巧:阅读源码时,一定要打开 IDEA 的Call HierarchyShow Refs功能,顺着方法调用链跳转,比直接看源码文件效率高得多。另外,发现有模糊点的地方,立刻写一个最小复现 demo 验证,不要停留在“大概明白”的程度。源码学习这件事,动手跑通一遍,胜过翻十遍文档。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 15:29:25

Apache Fesod替代EasyExcel:可控性优先的Excel处理方案

1. 项目概述&#xff1a;从EasyExcel到Apache Fesod的迁移动因 “再见了EasyExcel&#xff0c;我决定用Apache Fesod”——这句话乍看像一句情绪化吐槽&#xff0c;但背后藏着一个在Java生态中反复上演、却长期被低估的现实困境&#xff1a; 当Excel处理需求从“能导出”升级为…

作者头像 李华
网站建设 2026/9/14 15:28:33

内网穿透绑定自定义域名:CNAME解析、HTTPS证书与自动续期实战

做开发这行&#xff0c;内网穿透基本是绕不开的日常工具。早期我用免费工具自带的随机域名&#xff0c;地址又长又没规律&#xff0c;偶尔还得靠收藏夹才能找回来&#xff0c;后来在一次线上联调的时候临时域名被平台回收&#xff0c;整个对接直接卡死&#xff0c;才下定决心给…

作者头像 李华
网站建设 2026/9/14 15:26:51

2026国内知名的 AI 论文写作网站,沁言学术使用要点介绍

AI 技术正在重塑论文写作的工作方式&#xff0c;各类 AI 论文写作网站不断涌现&#xff0c;为科研人员带来便利的同时&#xff0c;也潜藏着不容忽视的风险&#xff1a;部分网站主打"一键生成完整论文"&#xff0c;输出内容无法溯源&#xff0c;极易引发学术不端隐患。…

作者头像 李华
网站建设 2026/9/14 15:25:09

磁盘告警排查指南:df/du/inode与文件句柄的7大深坑

半夜两点被磁盘告警消息吵醒&#xff0c;钉钉群里一张截图&#xff1a;某个数据分区 Use% 超过90%&#xff0c;要求立即处理。这种告警我这一两年见过不下几十次&#xff0c;从一开始慌慌张张执行 du -sh /* 从根目录一层层往下翻&#xff0c;到后来十分钟内锁定根因&#xf…

作者头像 李华