单元测试加上@Transactional就能实现回滚【原理】

本文详细解析了Spring框架中如何利用@Transactional注解实现单元测试的事务回滚,重点介绍了TestContextManager、TestContext及TransactionalTestExecutionListener的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

有时候我们开发需求的过程中写单测,会往数据库里插入数据,但是这样的数据其实是没有意义的,要么测试数据中含有唯一主键,每次run之前都要改单测里的参数,要么,有时候公司上线的流水线会跑一遍单测,给数据库多次插入无意义的数据。那么我们就需要让单测在run完之后可以自动回滚。

一、问题

如果保证单测插入到数据库的数据回滚?

二、回答

最简单的方式就是在方法上加上@Transactional注解了

三、源码分析

1.TestContextManager

{@code TestContextManager} is the main entry point into the <em>Spring
 * TestContext Framework</em>.
  TestContextManager是Spring测试框架的主要进入点
 *
 * <p>Specifically, a {@code TestContextManager} is responsible for managing a
 * single {@link TestContext} and signaling events to all registered
 * {@link TestExecutionListener TestExecutionListeners} at the following test
 <p>具体来说,{@code TestContextManager} 负责管理单个 {@link TestContext} 并在以下测试中向所有注册的 {@link TestExecutionListener TestExecutionListeners} 发送信号事件

根据上面的Java doc,我们便知道了两个重要的概念

TestContextTestExecutionListener

剩下的就是listener和testContext的交互了

2.TestContext

{@code TestContext} encapsulates the context in which a test is executed,
 * agnostic of the actual testing framework in use.
 {@code TestContext} 封装了执行测试的上下文,与使用的实际测试框架无关。

可以来看看默认实现DefaultTestContext

public class DefaultTestContext implements TestContext {

	private final Map<String, Object> attributes = new ConcurrentHashMap<>(4);

	private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;

	private final MergedContextConfiguration mergedContextConfiguration;

	private final Class<?> testClass;

	@Nullable
	private volatile Object testInstance;

	@Nullable
	private volatile Method testMethod;

	@Nullable
	private volatile Throwable testException;
}

对于我们一般的方法,最重要的其实就是testClass和testMethod

3.TestExecutionListener

3.1 AbstractTestContextBootstrapper#getTestExecutionListeners

testExecutionListeners是通过SpringFactory从spring.factories文件里的配置加载进来的
其中包含了我们本文关注的TransactionalTestExecutionListener

对于从spring.factories中加载配置类不熟悉的同学,可以参考这篇博客

3.2 钩子函数

其实钩子函数可以理解为静态AOP,也可以理解为spring框架开放给用户在某个时间点实现某些功能的扩展点。在spring源码里特别常见,譬如spring里的各种postProcessor的接口实现类,里面其实没有方法逻辑,用户可以通过自定义这些接口来在spring的bean的生命周期内对bean的实现进行个性化。
言归正传,咱们还是来看TestExecutionListener的钩子函数

/**
	 * Pre-processes a test class <em>before</em> execution of all tests within
	 * the class.
	 * <p>This method should be called immediately before framework-specific
	 * <em>before class</em> lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context for the test; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @since 3.0
	 */
	default void beforeTestClass(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Prepares the {@link Object test instance} of the supplied
	 * {@link TestContext test context}, for example by injecting dependencies.
	 * <p>This method should be called immediately after instantiation of the test
	 * instance but prior to any framework-specific lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context for the test; never {@code null}
	 * @throws Exception allows any exception to propagate
	 */
	default void prepareTestInstance(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Pre-processes a test <em>before</em> execution of <em>before</em>
	 * lifecycle callbacks of the underlying test framework &mdash; for example,
	 * by setting up test fixtures.
	 * <p>This method <strong>must</strong> be called immediately prior to
	 * framework-specific <em>before</em> lifecycle callbacks. For historical
	 * reasons, this method is named {@code beforeTestMethod}. Since the
	 * introduction of {@link #beforeTestExecution}, a more suitable name for
	 * this method might be something like {@code beforeTestSetUp} or
	 * {@code beforeEach}; however, it is unfortunately impossible to rename
	 * this method due to backward compatibility concerns.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context in which the test method will be
	 * executed; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @see #afterTestMethod
	 * @see #beforeTestExecution
	 * @see #afterTestExecution
	 */
	default void beforeTestMethod(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Pre-processes a test <em>immediately before</em> execution of the
	 * {@link java.lang.reflect.Method test method} in the supplied
	 * {@link TestContext test context} &mdash; for example, for timing
	 * or logging purposes.
	 * <p>This method <strong>must</strong> be called after framework-specific
	 * <em>before</em> lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context in which the test method will be
	 * executed; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @since 5.0
	 * @see #beforeTestMethod
	 * @see #afterTestMethod
	 * @see #afterTestExecution
	 */
	default void beforeTestExecution(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Post-processes a test <em>immediately after</em> execution of the
	 * {@link java.lang.reflect.Method test method} in the supplied
	 * {@link TestContext test context} &mdash; for example, for timing
	 * or logging purposes.
	 * <p>This method <strong>must</strong> be called before framework-specific
	 * <em>after</em> lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context in which the test method will be
	 * executed; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @since 5.0
	 * @see #beforeTestMethod
	 * @see #afterTestMethod
	 * @see #beforeTestExecution
	 */
	default void afterTestExecution(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Post-processes a test <em>after</em> execution of <em>after</em>
	 * lifecycle callbacks of the underlying test framework &mdash; for example,
	 * by tearing down test fixtures.
	 * <p>This method <strong>must</strong> be called immediately after
	 * framework-specific <em>after</em> lifecycle callbacks. For historical
	 * reasons, this method is named {@code afterTestMethod}. Since the
	 * introduction of {@link #afterTestExecution}, a more suitable name for
	 * this method might be something like {@code afterTestTearDown} or
	 * {@code afterEach}; however, it is unfortunately impossible to rename
	 * this method due to backward compatibility concerns.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context in which the test method was
	 * executed; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @see #beforeTestMethod
	 * @see #beforeTestExecution
	 * @see #afterTestExecution
	 */
	default void afterTestMethod(TestContext testContext) throws Exception {
		/* no-op */
	}

	/**
	 * Post-processes a test class <em>after</em> execution of all tests within
	 * the class.
	 * <p>This method should be called immediately after framework-specific
	 * <em>after class</em> lifecycle callbacks.
	 * <p>The default implementation is <em>empty</em>. Can be overridden by
	 * concrete classes as necessary.
	 * @param testContext the test context for the test; never {@code null}
	 * @throws Exception allows any exception to propagate
	 * @since 3.0
	 */
	default void afterTestClass(TestContext testContext) throws Exception {
		/* no-op */
	}

由于本文今天关注的重点是如何实现回滚的,那么咱们需要care的其实就是TransactionalTestExecutionListener的beforeTestMethod和afterTestMethod。

3.3 TransactionalTestExecutionListener

3.3.1 beforeTestMethod

public void beforeTestMethod(final TestContext testContext) throws Exception {
		Method testMethod = testContext.getTestMethod();
		Class<?> testClass = testContext.getTestClass();
		Assert.notNull(testMethod, "Test method of supplied TestContext must not be null");

		TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
		Assert.state(txContext == null, "Cannot start new transaction without ending existing transaction");

		PlatformTransactionManager tm = null;
    // 获取方法或者类上的@Transactional注解信息
		TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod, testClass);
		// 如果加了@Transactional注解
		if (transactionAttribute != null) {
			transactionAttribute = TestContextTransactionUtils.createDelegatingTransactionAttribute(testContext,
				transactionAttribute);

			if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
				return;
			}
// transactionManager默认情况下会获取容器里的PlatformTransactionManager
			tm = getTransactionManager(testContext, transactionAttribute.getQualifier());
			Assert.state(tm != null,
					() -> "Failed to retrieve PlatformTransactionManager for @Transactional test: " + testContext);
		}
		if (tm != null) {
      // 需要重点关注isRollback方法,新建了一个事务上下文txContext
			txContext = new TransactionContext(testContext, tm, transactionAttribute, isRollback(testContext));
			runBeforeTransactionMethods(testContext);
      // 这里面通过TransactionManager的getTransaction获取了对应的transactionStatus,然后设置到了transactionContext上了
			txContext.startTransaction();
      // 将transactionContext放到了ThreadLocal上
			TransactionContextHolder.setCurrentTransactionContext(txContext);
		}
	}
3.3.3.1 isRollback
protected final boolean isRollback(TestContext testContext) throws Exception {
  // 如果类上面没有加@Rollback注解,那么此处rollback = true;只有@Rollback(value=false),此处才是false
		boolean rollback = isDefaultRollback(testContext);
  // 获取方法上的@Rollback注解
		Rollback rollbackAnnotation =
				AnnotatedElementUtils.findMergedAnnotation(testContext.getTestMethod(), Rollback.class);
		if (rollbackAnnotation != null) {
      // 返回方法上的@Rollback注解的value
			boolean rollbackOverride = rollbackAnnotation.value();
			rollback = rollbackOverride;
		}
		else {
		}
  // 所以此处默认是true
		return rollback;
	}
3.3.1.2 isDefaultRollback
protected final boolean isDefaultRollback(TestContext testContext) throws Exception {
		Class<?> testClass = testContext.getTestClass();
		Rollback rollback = AnnotatedElementUtils.findMergedAnnotation(testClass, Rollback.class);
		boolean rollbackPresent = (rollback != null);
		if (rollbackPresent) {
			boolean defaultRollback = rollback.value();
			return defaultRollback;
		}
		// else
		return true;
	}
3.3.3.3 TransactionContext
TransactionContext(TestContext testContext, PlatformTransactionManager transactionManager,
			TransactionDefinition transactionDefinition, boolean defaultRollback) {
		this.testContext = testContext;
		this.transactionManager = transactionManager;
		this.transactionDefinition = transactionDefinition;
		this.defaultRollback = defaultRollback;
  // 因此此处的flaggedForRollback被设置成了true
		this.flaggedForRollback = defaultRollback;
	}

综上,可以发现,在beforeTestMethod中,会读取方法的@Transactional注解,如果类或者方法上没有@Rollback,那么默认的会设置一个 TransactionContext,并将其flaggedForRollback设置为true

3.3.2 afterTestMethod

public void afterTestMethod(TestContext testContext) throws Exception {
		Method testMethod = testContext.getTestMethod();
		Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
		// 从ThreadLocal上拿到了事务上下文txContext
		TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
		// If there was (or perhaps still is) a transaction...
		if (txContext != null) {
			TransactionStatus transactionStatus = txContext.getTransactionStatus();
			try {
				// If the transaction is still active...
        // 获取事务状态transactionStatus,
				if (transactionStatus != null && !transactionStatus.isCompleted()) {
          // 在该方法里进行回滚
					txContext.endTransaction();
				}
			}
			finally {
				runAfterTransactionMethods(testContext);
			}
		}
	}
3.3.2.1 endTransaction
void endTransaction() {
		Assert.state(this.transactionStatus != null,
				() -> "Failed to end transaction - transaction does not exist: " + this.testContext);

		try {
			if (this.flaggedForRollback) {
        // 进入此处,调用了transactionManager的rollback方法
				this.transactionManager.rollback(this.transactionStatus);
			}
			else {
				this.transactionManager.commit(this.transactionStatus);
			}
		}
		finally {
			this.transactionStatus = null;
		}
	}

3.4 小结

大致流程如下

1. 执行beforeTestMethod,封装了一个TransactionContext,并把其中的flaggedForRollback设置成true,并把TransactionContext放到ThreadLocal里
2. 然后执行我们的单测方法
3. 执行afterTestMethod,从ThreadLocal里取出TransactionContext,判断里面的flaggedForRollback,如果为true,就rollback,否则就commit

在txContext.startTransaction();中,会调用transactionManager.getTransaction来获取当前的事务内容transactionStatus,关于这块的内容,可以参见这篇博客

总结

Spring这个盒子,很多实现方式都是类似的,特别这个SPI机制,很多地方使用,也是Spring框架开放给第三方框架用来接入的一个大杀器。

<think>嗯,用户问的是Java中事务注解的原理和失效的情况。首先,我得回忆一下事务管理在Java里是怎么工作的,特别是Spring框架中的事务管理,因为通常提到事务注解,大家都会想到Spring@Transactional。 首先,事务注解的原理。我记得Spring的事务管理是基于AOP(面向切面编程)实现的。也就是说,当我们在方法上加上@Transactional注解时,Spring会为这个方法创建一个代理,这样在方法执行前后,事务的开启、提交或回滚逻辑会被织入进去。具体来说,可能是在方法开始前开启事务,方法执行成功后提交事务,如果抛出异常则回滚事务。 不过,这里需要注意Spring的事务管理器,它负责实际的事务操作,比如使用DataSourceTransactionManager来处理JDBC事务,或者JpaTransactionManager来处理JPA的事务。而@Transactional注解可以配置事务的传播行为、隔离级别、超时时间、只读属性以及回滚的异常类型等属性。 然后是事务失效的情况,这部分用户可能更关心,因为实际开发中容易踩坑。首先想到的是自调用问题,也就是一个类中的方法A调用同一个类中的方法B,而方法B上有@Transactional注解。这时候由于代理机制的问题,事务可能不会生效,因为AOP代理只能拦截外部调用的方法,自调用不会经过代理。解决办法可能是使用AopContext.currentProxy()或者将方法B放到另一个类中。 另外,事务方法的修饰符非public的话,事务也可能不生效,因为Spring默认只对public方法进行代理。这点需要注意,如果用户把@Transactional放在private或者protected方法上,事务可能不会被处理。 还有异常类型没有被正确捕获的情况。默认情况下,Spring只在遇到RuntimeException和Error时回滚,如果方法抛出的是检查型异常(比如IOException),并且用户没有在注解里指定rollbackFor,事务就不会回滚。所以需要确认异常类型是否正确配置。 还有数据库引擎是否支持事务,比如MySQL的MyISAM引擎不支持事务,必须使用InnoDB。这点虽然基础,但有时候容易被忽略。 另外,如果在一个事务方法中,调用了另一个事务方法,并且传播行为设置不当,比如REQUIRES_NEW,但外层事务可能因为某些原因没有正确传播,导致内层事务没有按预期执行。 还有是否启用了事务管理。比如在配置类中没有使用@EnableTransactionManagement,或者没有配置PlatformTransactionManager的Bean,这会导致事务注解根本不起作用。 还有多线程的情况下,事务可能不在同一个线程里,导致上下文不同,事务无法正确管理。比如在方法内启动新线程处理数据库操作,这时候新线程的事务和原方法的事务是独立的,可能出现问题。 此外,有些ORM框架的会话或连接没有被正确管理,比如Hibernate的session没有绑定到当前事务,导致操作不在事务范围内。 总结一下,用户需要了解事务注解背后的代理机制和事务管理器,以及常见的导致事务失效的场景,比如自调用、异常处理、方法可见性、数据库支持、配置缺失等。在回答时要结构清晰,分点说明原理和失效情况,并给出例子和解决办法,这样用户更容易理解和应用。</think>### Java事务注解原理与失效场景解析 --- #### 一、事务注解的核心原理(以Spring为例) 1. **AOP代理机制** - 当使用`@Transactional`标注方法时,Spring通过**动态代理**(JDK动态代理或CGLIB)创建代理对象。 - **事务切面**(如`TransactionInterceptor`)在方法执行前后处理事务: ```java // 伪代码逻辑 try { beginTransaction(); // 开启事务 targetMethod(); // 执行业务方法 commitTransaction(); // 提交事务 } catch (Exception e) { rollbackTransaction(); // 回滚事务 throw e; } ``` 2. **事务管理器(PlatformTransactionManager)** - 底层依赖`DataSourceTransactionManager`(JDBC)、`JpaTransactionManager`(JPA)等实现。 - 关键配置示例: ```java @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } ``` 3. **注解属性控制行为** - `propagation`:事务传播(如`REQUIRED`、`REQUIRES_NEW`)。 - `isolation`:隔离级别(如`READ_COMMITTED`)。 - `rollbackFor`:指定触发回滚的异常类型。 --- #### 二、事务失效的常见场景 1. **自调用问题** - **现象**:类内部方法A调用方法B(B有`@Transactional`),事务不生效。 - **原因**:动态代理仅拦截外部调用,自调用绕过代理。 - **解决**: - 将方法B移到另一个类。 - 使用`AopContext.currentProxy()`获取代理对象。 2. **非public方法** - **现象**:`@Transactional`标注在`private`或`protected`方法上。 - **原因**:Spring默认仅代理public方法。 - **解决**:改为public方法。 3. **异常处理不当** - **现象**:抛出检查型异常(如`IOException`)未触发回滚。 - **原因**:默认仅回滚`RuntimeException`和`Error`。 - **解决**:显式指定`rollbackFor`: ```java @Transactional(rollbackFor = Exception.class) ``` 4. **数据库引擎不支持事务** - **现象**:使用MySQL的MyISAM引擎(不支持事务)。 - **解决**:改用InnoDB引擎。 5. **未启用事务管理** - **现象**:缺少`@EnableTransactionManagement`或未配置事务管理器。 - **解决**:检查Spring配置是否正确。 6. **多线程场景** - **现象**:新线程中操作数据库,与原线程事务隔离。 - **原因**:事务绑定到线程上下文(如`ThreadLocal`)。 - **解决**:避免跨线程事务操作。 7. **传播行为配置错误** - **现象**:嵌套事务未按预期执行(如`REQUIRES_NEW`未生效)。 - **原因**:外层事务传播行为覆盖内层配置。 - **解决**:检查方法调用链的传播设置。 --- #### 三、验证事务的调试技巧 1. 开启Spring日志(`logging.level.org.springframework.transaction=DEBUG`)。 2. 检查代理对象类型:输出`this.getClass().getName()`确认是否为代理类。 3. 手动抛出异常测试回滚逻辑。 --- #### 总结 事务注解依赖**AOP代理+事务管理器**协作,失效场景多与**调用方式、异常处理、配置缺失**相关。理解底层机制后,可通过代码审查和日志调试快速定位问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值