← 学习库技术专题源码原理 · 故障排查 · 架构实践

专题 05 / 完整阅读

Spring 核心原理与事务

本章使用 Spring Framework 5.3.31 和 Spring Boot 2.7.18。Spring 创建 Bean、通过代理增强方法、管理数据库事务,是相互配合的几件事。把一次调用从入口走到提交,很多“事务失效”就能解释清楚。

核心知识与原理

IoC、生命周期与循环依赖

假设 OrderService 需要 OrderRepository。IoC 容器先读取 BeanDefinition,知道要创建什么类、注入哪些依赖,再创建对象并完成注入。BeanFactory 提供基础的 Bean 管理;ApplicationContext 在此之上组织启动、资源、事件和国际化等功能。

refresh() 会先处理 Bean 定义,再注册 BeanPostProcessor,最后创建非懒加载单例。BeanFactoryPostProcessor 改的是对象还没创建前的定义;BeanPostProcessor 处理的是已经创建的实例。自动代理主要在后者这一阶段参与,二者不能混记。

一个 Bean 大致经历实例化、依赖注入、Aware 回调、初始化前处理、初始化回调、初始化后处理。@PostConstruct、afterPropertiesSet、自定义 initMethod 属于初始化相关步骤。容器关闭时会销毁它管理的单例;prototype 对象取出去后,清理通常要由使用者负责。

循环依赖的问题可以用 A、B 说明:A 已经 new 出来,但注入 B 时发现 B 又需要 A。如果 A 是可提前暴露的单例,容器可以先把 A 的早期引用交给 B,等 B 完成后再把它注入 A。构造器循环却不同:A 连 new 都还没完成,就必须先拿到 B,因此没有现成的 A 可借出去。

三级缓存分别放完整单例、早期引用和生成早期引用的工厂。第三层的工厂允许后处理器按需生成代理,避免 B 拿到原始 A,而其他调用者后来拿到代理 A。它只能处理部分单例字段/setter 循环,也会受到其他后处理器影响。Boot 2.7 默认禁止循环引用;在新代码里拆开相互依赖,通常比打开开关容易维护。

动态代理与 AOP

代理相当于方法外面的一层调用入口。例如进入订单方法前开启事务,方法结束后提交;异常时回滚。这层逻辑由拦截器执行,业务类本身不需要把开始、提交散落在每个方法里。

JDK 动态代理基于接口,CGLIB 通过子类增强。具体选择还受配置影响,Boot 默认倾向类代理,不能只凭“有接口”猜代理类型。CGLIB 无法覆盖 final 和 private 方法。

最容易踩到的是自调用:对象内部写 this.save(),调用的是自身方法,没有重新进入外部代理。save 上有 @Transactional,也可能根本没执行事务拦截器。把事务操作放到另一个 Bean,由注入的代理对象调用,通常更清楚。

// 业务示例:跨 Bean 调用,让入口穿过事务代理。
@Service
class OrderService {
    private final OrderWriter writer;
    OrderService(OrderWriter writer) { this.writer = writer; }
    public void create(Order order) { writer.persist(order); }
}
@Service
class OrderWriter {
    @Transactional(rollbackFor = Exception.class)
    public void persist(Order order) throws Exception {
        // 同一 DataSource 的持久化路径,异常交给代理作回滚判断。
    }
}

事务拦截、传播与线程上下文

事务调用进入 TransactionInterceptor 后,Spring 先读事务属性,选择 PlatformTransactionManager,再决定开启新事务还是加入已有事务。业务方法返回时提交,抛异常时按规则决定回滚,最后清理调用上下文。

使用 DataSourceTransactionManager 时,当前事务连接通过 ConnectionHolder 绑定在 TransactionSynchronizationManager 的 ThreadLocal 中。同一线程里的兼容 DAO 操作才能拿到这条连接。手动创建另一条连接,或切到异步线程,不能自动参与原事务。

默认 RuntimeException 和 Error 回滚,checked exception 需要配置 rollbackFor 等规则。异常在方法里被捕获后正常返回,拦截器可能看到的就是成功。另一种情况是内层 REQUIRED 已把共享事务标记为 rollback-only,外层虽然捕获异常,最终提交仍可能抛 UnexpectedRollbackException。

REQUIRED 加入已有物理事务;REQUIRES_NEW 挂起外层、再拿一条连接开启独立事务。外层连接并没有被释放,所以嵌套并发容易耗尽连接池。NESTED 通常使用同一连接上的保存点,外层回滚仍会撤销它;具体支持取决于事务管理器和驱动。

标准代理事务通常以 public 方法为使用范围。排查时还要检查 Bean 是否由 Spring 管理、选了哪个 manager、数据库引擎是否支持事务。Reactor 换线程后不能照搬 JDBC 的 ThreadLocal 事务,需要相应的 reactive manager 和 Context。

Boot 自动配置与 MVC

自动配置先找候选,再按条件决定是否启用。Boot 2.7 同时读取 spring.factories 和 AutoConfiguration.imports 中的候选,随后去重、过滤与排序。已有用户 Bean、缺少类或配置条件不满足,都会让某个配置跳过。排查时看条件报告,比盲目扩大扫描范围有效。

MVC 请求进入 DispatcherServlet,先找 Handler,再找能执行它的 HandlerAdapter。参数解析器准备方法参数,控制器执行后,返回值处理器决定写 JSON 还是解析视图。@ResponseBody 的内容通常由 HttpMessageConverter 写出。

Servlet Filter 在 Servlet 链上,HandlerInterceptor 则在 MVC 的 handler 流程里。preHandle、postHandle、afterCompletion 发生在不同位置;异步请求还可能再次 dispatch。需要清理的租户、日志上下文,应按实际请求边界处理。

# Boot 2.7 配置示例:保持默认禁止循环依赖,推动依赖拆分。
spring:
  main:
    allow-circular-references: false
  datasource:
    hikari:
      maximum-pool-size: 20

事务推演:注解隔离级别为何可能被忽略

外层已开启 RC 事务,内层 REQUIRED 标注 RR,内层默认仍使用外层那条连接和事务,不会凭注解把它临时切成 RR。启用现有事务属性校验时,冲突可能被拒绝。想要一个独立隔离级别,先要确认是否真的创建了新物理事务。

查事务问题可以沿五步走:调用有没有经过代理;manager 是否对应这套数据源;DAO 是否取得绑定连接;异常是否传到拦截器;事务有没有被标记 rollback-only。提交后内存回调发 MQ 也有宕机窗口,可靠投递需要同事务保存待发事件。

源码级解析与调用链

先看单例的 getSingleton 如何依次检查三层缓存,再看事务代理的 invokeWithinTransaction 怎样包住方法。对象创建的主过程是 doCreateBean,MVC 请求主入口是 DispatcherServlet.doDispatch。

自动配置读取候选的代码很短,适合直接确认 Boot 2.7 到底还读不读 spring.factories。

DefaultSingletonBeanRegistry.getSingletonSpring Framework 5.3.31 · L171–L204 · 完整源码
    /**
     * Return the (raw) singleton object registered under the given name.
     * <p>Checks already instantiated singletons and also allows for an early
     * reference to a currently created singleton (resolving a circular reference).
     * @param beanName the name of the bean to look for
     * @param allowEarlyReference whether early references should be created or not
     * @return the registered singleton object, or {@code null} if none found
     */
    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // Quick check for existing instance without full singleton lock
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                synchronized (this.singletonObjects) {
                    // Consistent creation of early reference within full singleton lock
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        singletonObject = this.earlySingletonObjects.get(beanName);
                        if (singletonObject == null) {
                            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                            if (singletonFactory != null) {
                                singletonObject = singletonFactory.getObject();
                                this.earlySingletonObjects.put(beanName, singletonObject);
                                this.singletonFactories.remove(beanName);
                            }
                        }
                    }
                }
            }
        }
        return singletonObject;
    }

早期引用只有在 Bean 正在创建时才有意义。工厂成功生成引用后,将它放入 earlySingletonObjects 并移除工厂;完整单例则仍优先从 singletonObjects 读取。

TransactionAspectSupport.invokeWithinTransactionSpring Framework 5.3.31 · L324–L471 · 完整源码
    /**
     * General delegate for around-advice-based subclasses, delegating to several other template
     * methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}
     * as well as regular {@link PlatformTransactionManager} implementations and
     * {@link ReactiveTransactionManager} implementations for reactive return types.
     * @param method the Method being invoked
     * @param targetClass the target class that we're invoking the method on
     * @param invocation the callback to use for proceeding with the target invocation
     * @return the return value of the method, if any
     * @throws Throwable propagated from the target invocation
     */
    @Nullable
    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
            final InvocationCallback invocation) throws Throwable {

        // If the transaction attribute is null, the method is non-transactional.
        TransactionAttributeSource tas = getTransactionAttributeSource();
        final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
        final TransactionManager tm = determineTransactionManager(txAttr);

        if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
            boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
            boolean hasSuspendingFlowReturnType = isSuspendingFunction &&
                    COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());
            if (isSuspendingFunction && !(invocation instanceof CoroutinesInvocationCallback)) {
                throw new IllegalStateException("Coroutines invocation not supported: " + method);
            }
            CoroutinesInvocationCallback corInv = (isSuspendingFunction ? (CoroutinesInvocationCallback) invocation : null);

            ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
                Class<?> reactiveType =
                        (isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType());
                ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
                if (adapter == null) {
                    throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
                            method.getReturnType());
                }
                return new ReactiveTransactionSupport(adapter);
            });

            InvocationCallback callback = invocation;
            if (corInv != null) {
                callback = () -> CoroutinesUtils.invokeSuspendingFunction(method, corInv.getTarget(), corInv.getArguments());
            }
            Object result = txSupport.invokeWithinTransaction(method, targetClass, callback, txAttr, (ReactiveTransactionManager) tm);
            if (corInv != null) {
                Publisher<?> pr = (Publisher<?>) result;
                return (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow(pr) :
                        KotlinDelegate.awaitSingleOrNull(pr, corInv.getContinuation()));
            }
            return result;
        }

        PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
        final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

        if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
            // Standard transaction demarcation with getTransaction and commit/rollback calls.
            TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

            Object retVal;
            try {
                // This is an around advice: Invoke the next interceptor in the chain.
                // This will normally result in a target object being invoked.
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // target invocation exception
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                cleanupTransactionInfo(txInfo);
            }

            if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                // Set rollback-only in case of Vavr failure matching our rollback rules...
                TransactionStatus status = txInfo.getTransactionStatus();
                if (status != null && txAttr != null) {
                    retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                }
            }

            commitTransactionAfterReturning(txInfo);
            return retVal;
        }

        else {
            Object result;
            final ThrowableHolder throwableHolder = new ThrowableHolder();

            // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
            try {
                result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
                    TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
                    try {
                        Object retVal = invocation.proceedWithInvocation();
                        if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                            // Set rollback-only in case of Vavr failure matching our rollback rules...
                            retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                        }
                        return retVal;
                    }
                    catch (Throwable ex) {
                        if (txAttr.rollbackOn(ex)) {
                            // A RuntimeException: will lead to a rollback.
                            if (ex instanceof RuntimeException) {
                                throw (RuntimeException) ex;
                            }
                            else {
                                throw new ThrowableHolderException(ex);
                            }
                        }
                        else {
                            // A normal return value: will lead to a commit.
                            throwableHolder.throwable = ex;
                            return null;
                        }
                    }
                    finally {
                        cleanupTransactionInfo(txInfo);
                    }
                });
            }
            catch (ThrowableHolderException ex) {
                throw ex.getCause();
            }
            catch (TransactionSystemException ex2) {
                if (throwableHolder.throwable != null) {
                    logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                    ex2.initApplicationException(throwableHolder.throwable);
                }
                throw ex2;
            }
            catch (Throwable ex2) {
                if (throwableHolder.throwable != null) {
                    logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                }
                throw ex2;
            }

            // Check result state: It might indicate a Throwable to rethrow.
            if (throwableHolder.throwable != null) {
                throw throwableHolder.throwable;
            }
            return result;
        }
    }

真正业务调用在 proceedWithInvocation。抛异常时 completeTransactionAfterThrowing 判断回滚规则;finally 恢复事务调用上下文;正常返回才走 commitTransactionAfterReturning。上下文清理本身不等于数据库已经提交。

AutoConfigurationImportSelector.getCandidateConfigurationsSpring Boot 2.7.18 · L170–L189 · 完整源码
    /**
     * Return the auto-configuration class names that should be considered. By default
     * this method will load candidates using {@link ImportCandidates} with
     * {@link #getSpringFactoriesLoaderFactoryClass()}. For backward compatible reasons it
     * will also consider {@link SpringFactoriesLoader} with
     * {@link #getSpringFactoriesLoaderFactoryClass()}.
     * @param metadata the source metadata
     * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
     * attributes}
     * @return a list of candidate configurations
     */
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = new ArrayList<>(
                SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
        ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
        Assert.notEmpty(configurations,
                "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
                        + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

两行加载来源分别是 SpringFactoriesLoader 与 ImportCandidates。它们一起加入候选列表,后续再过滤;这能直接回答 Boot 2.7 是否已经完全不用 spring.factories。

refresh / getBeancreateBeanInstancesingletonFactories / early proxypopulateBean / 注入Aware / BPP before / 初始化BPP after / AOP proxysingletonObjects / 完整单例循环依赖读取 early reference容器关闭 / 销毁回调
图 1 · 对照上文阅读流程
查看 Mermaid 图表源码
flowchart TD
 A["refresh / getBean"] --> B["createBeanInstance"]
 B --> C["singletonFactories / early proxy"]
 C --> D["populateBean / 注入"]
 D --> E["Aware / BPP before / 初始化"]
 E --> F["BPP after / AOP proxy"]
 F --> G["singletonObjects / 完整单例"]
 C -.-> H["循环依赖读取 early reference"]
 H -.-> D
 G --> I["容器关闭 / 销毁回调"]

三层原理问答

1. 为什么三级缓存而非简单提前放对象?

提前给 B 一个原始 A,后面 A 又被做成代理,B 就可能绕过增强。三级缓存中的工厂允许按需生成早期代理,并与最终引用协调。

getSingleton 按什么顺序找? 先找 singletonObjects;创建中再找 earlySingletonObjects;允许早期引用时调用 singletonFactories,得到结果后移入第二层并移除工厂。getEarlyBeanReference 让相关后处理器参与。

需要靠它设计循环吗? 不建议。它只解决部分单例注入循环,构造器和 prototype 不同。Boot 2.7 默认禁止,先拆职责或显式延迟,初始化过程更容易测试。

2. @Transactional 自调用为何失效?

因为 this.method 没有经过注入的代理对象。@Transactional 是让拦截器识别的配置,不是 JVM 在方法体里自动开启事务的指令。

事务具体在哪开始? 调用进入 TransactionInterceptor 后解析属性,交给 manager 创建或加入事务,再调用目标方法。JDK/CGLIB 形式和方法可拦截性也影响是否到这里。

怎么改得清楚? 把事务写操作抽到另一 Bean,从容器注入后调用;也可明确使用 TransactionTemplate。测试调用真实代理并核对数据库结果,避免只做普通对象单测。

3. 内层回滚,外层捕获异常为什么仍提交失败?

内外层 REQUIRED 通常共用同一个物理事务。内层标记 rollback-only 后,外层捕获异常并不能把它清除,所以最后提交仍可能失败。

为什么抛 UnexpectedRollbackException? 管理器发现共享事务必须回滚,就不能让外层调用者误以为提交成功。捕获 Java 异常与改变数据库事务状态是两件事。

需要独立审计怎么办? 可以按业务选择独立事务或可靠异步记录,但要承认审计与主业务不再一起提交。不要只为了消掉异常随意改 REQUIRES_NEW,它还占额外连接。

4. REQUIRES_NEW 与 NESTED 如何选?

REQUIRES_NEW 是独立物理事务,外层失败不一定撤销已提交的内层。NESTED 常是在同一事务中建立保存点,外层回滚仍会把内层结果一起撤销。

管理器怎样支持? DataSourceTransactionManager 在允许且驱动支持时可用 savepoint;其他 manager 不一定相同。新事务挂起外层资源后,再借连接,外层连接仍占着。

如何选择? 独立审计需要单独提交时可以考虑新事务;同一批工作中局部失败可考虑保存点。按极端并发检查连接需求和锁持有,不把传播选项当成无成本开关。

5. 异步方法能继承 JDBC 事务吗?

不能自动继承。JDBC 连接通常绑定当前线程,异步任务换了线程,不会自然取得原事务。把同一 Connection 手工塞给多个线程也不是安全替代。

源码体现在哪里? TransactionSynchronizationManager 保存 ThreadLocal 资源。Reactor Context 跟订阅走,两者不能等同;reactive 事务要配对应 manager。

业务如何设计? 真正必须一起成功的本地修改保持同一事务执行;异步动作定义独立事务,并通过 Outbox 或事务消息可靠通知。普通 afterCommit 回调仍有宕机丢任务的可能。

6. 自动配置不生效如何定位?

先确认候选配置有没有加载,再查 classpath、属性和现有 Bean。一个用户 Bean 已存在,可能恰好触发 MissingBean 条件不成立,不是扫描失败。

Boot 2.7 读哪些资源? getCandidateConfigurations 同时读取 spring.factories 和 AutoConfiguration.imports,再去重和过滤。条件报告可展示哪条条件没有满足。

自定义 starter 怎么维护? 固定支持版本,测试有依赖、缺依赖、用户覆盖等情况。排除自动配置时清楚接替它的职责,别复制内部类后期待跨版本一直可用。

模拟生产案例:新事务耗尽连接池

连接池大小 20,20 个请求都先开启外层事务,随后一起调用 REQUIRES_NEW 写审计。它们全部等新连接直到超时。这是一个可用于测试的模拟条件。

看连接池 active/pending,线程是否停在 borrowConnection,再确认外层事务仍持连接。如果数据库几乎没有执行新 SQL,不能简单认为是 SQL 慢;请求可能还没有拿到连接。

原因是挂起外层事务不等于归还连接,每个请求内层还要再借一条。先限制这类请求并发;是否增加池,要看数据库能力。审计也可按业务改同事务,或通过 Outbox 异步写,明确成功和失败时要留下什么。

测试用屏障让外层同时进入内层,检查是否可有界完成。以后把嵌套事务纳入连接需求,分别设置获取连接和事务超时,避免配置只看正常单层请求。

知识梳理与核心总结

一分钟要点回顾

Spring 先创建和注入 Bean,再通过后处理器做代理。三级缓存只帮助部分单例循环,Boot 2.7 默认禁止循环引用。事务必须经过代理,manager 决定创建或加入,JDBC 连接通常绑定当前线程。自调用、捕获异常和异步换线程都要检查。REQUIRES_NEW 独立提交却再占连接,NESTED 常用保存点。排查沿代理、manager、连接和异常一路看,不只确认注解在不在。

深入理解与机制串联

我会先说明对象创建和方法增强是两个过程。Spring 按 BeanDefinition 实例化、注入,再执行初始化与 BeanPostProcessor。提前暴露单例时用工厂生成早期引用,让循环中拿到的引用尽量与最终代理协调,但这不解决构造器循环,Boot 2.7 默认也禁止。

事务的真正入口是代理里的 TransactionInterceptor。它读属性、选择 manager,创建或加入事务,执行方法,最后提交或按异常规则回滚。JDBC manager 把连接绑定当前线程,所以自己新连接或另起线程,不会自动加入原事务。this 调用没有经过代理,是常见失效原因。

默认运行时异常和 Error 回滚,checked exception 需要规则。异常被业务方法吞掉,拦截器可能看到成功;内层 REQUIRED 已标 rollback-only,外层捕获又不能清掉这个标记,最后仍会失败。

传播要看物理资源。REQUIRED 共用现有事务,内层声明隔离不一定重新生效。REQUIRES_NEW 挂起外层并借新连接,外层还占一条;NESTED 在支持时用保存点,外层回滚仍撤销它。连接池要考虑嵌套并发。

排查时我会依次确认调用经过代理、manager 匹配、DAO 使用绑定连接、异常传播与最终状态。提交后通知下游,也不能只靠内存回调:宕机仍可漏消息,需要可靠待发记录。自动配置则从候选和条件报告查起,MVC 按 handler、adapter、参数和返回值处理解释请求去向。

延伸问题、常见误解与速记

  • 高频追问:checked exception 默认回滚吗?捕获异常为何仍提交失败?Boot 2.7 是否仍读 spring.factories?
  • 容易答错:三级缓存解决所有循环;有接口一定 JDK 代理;挂起释放外层连接;把同一事务连接交给异步线程。
  • 常看的源码:doCreateBean/getSingleton/getEarlyBeanReference、TransactionInterceptor.invoke、invokeWithinTransaction、AutoConfigurationImportSelector、DispatcherServlet.doDispatch。
  • 理解事务时,先看这次调用实际经过哪里、使用哪条连接,再看注解配置。

官方资料与版本来源

本文按上述版本阅读官方源码,函数窗口保留完整分支与边界,声明窗口聚焦所讨论的字段或配置。版权见 source-notices.txt,下载记录见 sources.json。

先记住这几件事

  • Bean 实例化与注入之后,初始化和后处理器参与代理。
  • 三级缓存只解决部分单例循环,Boot 2.7 默认禁止循环引用。
  • 事务入口在代理,this 自调用和异步线程不会自动经过它。
  • REQUIRES_NEW 再借一条连接,NESTED 常用同事务保存点。
  • Boot 2.7 同时读取两类自动配置候选,排查先看条件报告。

四个重点问题

1. 为什么三级缓存而非简单提前放对象?

提前给 B 一个原始 A,后面 A 又被做成代理,B 就可能绕过增强。三级缓存中的工厂允许按需生成早期代理,并与最终引用协调。

getSingleton 按什么顺序找? 先找 singletonObjects;创建中再找 earlySingletonObjects;允许早期引用时调用 singletonFactories,得到结果后移入第二层并移除工厂。getEarlyBeanReference 让相关后处理器参与。

需要靠它设计循环吗? 不建议。它只解决部分单例注入循环,构造器和 prototype 不同。Boot 2.7 默认禁止,先拆职责或显式延迟,初始化过程更容易测试。

2. @Transactional 自调用为何失效?

因为 this.method 没有经过注入的代理对象。@Transactional 是让拦截器识别的配置,不是 JVM 在方法体里自动开启事务的指令。

事务具体在哪开始? 调用进入 TransactionInterceptor 后解析属性,交给 manager 创建或加入事务,再调用目标方法。JDK/CGLIB 形式和方法可拦截性也影响是否到这里。

怎么改得清楚? 把事务写操作抽到另一 Bean,从容器注入后调用;也可明确使用 TransactionTemplate。测试调用真实代理并核对数据库结果,避免只做普通对象单测。

3. 内层回滚,外层捕获异常为什么仍提交失败?

内外层 REQUIRED 通常共用同一个物理事务。内层标记 rollback-only 后,外层捕获异常并不能把它清除,所以最后提交仍可能失败。

为什么抛 UnexpectedRollbackException? 管理器发现共享事务必须回滚,就不能让外层调用者误以为提交成功。捕获 Java 异常与改变数据库事务状态是两件事。

需要独立审计怎么办? 可以按业务选择独立事务或可靠异步记录,但要承认审计与主业务不再一起提交。不要只为了消掉异常随意改 REQUIRES_NEW,它还占额外连接。

4. REQUIRES_NEW 与 NESTED 如何选?

REQUIRES_NEW 是独立物理事务,外层失败不一定撤销已提交的内层。NESTED 常是在同一事务中建立保存点,外层回滚仍会把内层结果一起撤销。

管理器怎样支持? DataSourceTransactionManager 在允许且驱动支持时可用 savepoint;其他 manager 不一定相同。新事务挂起外层资源后,再借连接,外层连接仍占着。

如何选择? 独立审计需要单独提交时可以考虑新事务;同一批工作中局部失败可考虑保存点。按极端并发检查连接需求和锁持有,不把传播选项当成无成本开关。

核心要点

Spring 先创建和注入 Bean,再通过后处理器做代理。三级缓存只帮助部分单例循环,Boot 2.7 默认禁止循环引用。事务必须经过代理,manager 决定创建或加入,JDBC 连接通常绑定当前线程。自调用、捕获异常和异步换线程都要检查。REQUIRES_NEW 独立提交却再占连接,NESTED 常用保存点。排查沿代理、manager、连接和异常一路看,不只确认注解在不在。

常见误解与源码

  • 高频追问:checked exception 默认回滚吗?捕获异常为何仍提交失败?Boot 2.7 是否仍读 spring.factories?
  • 容易答错:三级缓存解决所有循环;有接口一定 JDK 代理;挂起释放外层连接;把同一事务连接交给异步线程。
  • 常看的源码:doCreateBean/getSingleton/getEarlyBeanReference、TransactionInterceptor.invoke、invokeWithinTransaction、AutoConfigurationImportSelector、DispatcherServlet.doDispatch。
  • 理解事务时,先看这次调用实际经过哪里、使用哪条连接,再看注解配置。

全文搜索

输入关键词,搜索本篇完整正文与答案。