spring基于注解的声明式事务原理分析

spring声明式事务管理jdbc数据源的流程和实现原理## 标题
首先我们要搞清楚两个核心问题:
1、spring声明式事务管理是基于springAOP的,是在目标方法上环绕执行事务逻辑。
2、对于jdbc事务管理是基于同一个连接的。也就是jdbc一个事物中所有的操作必须在同一个连接中完成。事务逻辑的大致伪代码如下:
Connection conn = DriverManager.getConnection(…);
try{
con.setAutoCommit(false);
//do some business work

con.commit();
}catch(Exception e){
con.rollback();
}finally{
con.close();
}

在声明式事务中,我们会在需要开启事务的方法上面加上@Transactional注解,以及用@EnableTransactionManagement注解开启spring事务。
spring会用aop相关技术为声明了事务的方法所属的对象创建一个代理对象。在建立代理对象时会把我们的事务配置属性初始化到一个TransactionAttribute类中,当我们访问开启了事务的方法时,会通知AOP拦截器进行拦截,如下图:

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// Adapt to TransactionAspectSupport’s invokeWithinTransaction…
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

@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 PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
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);
}
commitTransactionAfterReturning(txInfo);
return retVal;

在invokeWithinTransaction方法中把我们配置的事务属性(TransactionAttribute)、事务管理器(PlatformTransactionManager)、切点信息(joinpointIdentification)信息封装成TransactionInfo对象并开始执行目标方法:retVal = invocation.proceedWithInvocation(),这行代码其实就是执行代理类的代理方法,也就是执行实际业务方法。整个大流程走到这个方法最后就完了,这和我们上面的伪代码十分相似,但是短短几行代码根本无法理解其原理和流程,有两个问题需要弄清楚:
1、在哪一行代码开启的事务?
2、怎么保证在执行业务方法的过程中始终用的是同一个connection对象呢?
先说第一个问题:
看到createTransactionIfnecessary方法了吗?事务就是在这个方法里面开启的:
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

// If no name specified, apply method identification as transaction name.
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}

TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug(“Skipping transactional joinpoint [” + joinpointIdentification +
“] because no transaction manager has been configured”);
}
}
}
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

status = tm.getTransaction(txAttr)这行代码就是获取事务,tm对象其实就是DataSourceTransactionManager类的对象,这个方法其实就是我们自己配置的DataSourceTransactionManager的父类AbstractPlatformTransactionManager里面的方法,在这个方法里面就执行了开启事务的相关代码。

doBegin方法是DataSourceTransactionManager重写的AbstractPlatformTransactionManager的方法,进到dobegin方法里面我们就知道怎么开启的事务了:
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;

try {
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug(“Acquired Connection [” + newCon + “] for JDBC transaction”);
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}

txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();

Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);

// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don’t want to do it unnecessarily (for example if we’ve explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug(“Switching JDBC Connection [” + con + “] to manual commit”);
}
con.setAutoCommit(false);
}

prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);

int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}

// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}

catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException(“Could not open JDBC Connection for transaction”, ex);
}
}

因为jdbc默认就是开启事务的,只是如果我们要控制事务提交就得把自动提交关闭,所以开启事务的方法里面大致做了以下这些事情:
1、从数据库中获取连接,把连接包装成ConnetionHolder设置到事务对象中
2、关闭事务自动提交
3、获取并设置事务隔离级别
4、设置事务超时时间
到这里,在执行业务方法之前的所有事务的准备工作就做完了,也就是说事务就算开启成功了,上面方法里面的这些属性的设置,其实就是设置我们配置好的值,只不过有些是spring配置的默认值。
通过以上的流程分析我们就清楚了spring声明式事务是如何开启事务的,那么接下来我们就一起来探讨一下spring的声明式事务管理是怎么保证在执行业务方法的过程中始终用的是同一个connection对象的。
我们知道在上面的事务开启的方法中,dobegin方法是会从数据库中获取一个连接让后设置到事务对象中,那么怎么保证mybatis在多次操作数据库的时候都使用这个连接?那我们就继续流程往下走,看看是在流程的哪个环节使用到这个connection并且保证每次都使用这个connection的。
从上面的分析可以看出来,现在spring已经把事务开始的准备工作都做好了,接下来就是执行业务方法了,业务方法中必定会至少一次调用mybatis的mapper接口的方法,调用mapper接口的方法其实就是执行mapper接口的代理的invoke方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

如果我们正常配置的mapper接口,这个invoke方法最终会执行到mapperMethod.execute(sqlSession,args)这行代码,这行代码会走到SessionTemplate的invoke里面去:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;

从这个里面可以看出来它会先去获取SqlSession,其实这个SqlSession里面就是封装了Connection对象,这里的getSqlSession方法肯定有如何找到事务里面的connection的逻辑。这个getSqlSession方法在SqlSessionUtils里面,我们进到这个方法看看:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}

if (LOGGER.isDebugEnabled()) {
LOGGER.debug(“Creating a new SqlSession”);
}

session = sessionFactory.openSession(executorType);

registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

return session;
}

session = sessionFactory.openSession(executorType)才是真正获取session的逻辑方法,上面的SqlSessionHolder其实每次进来都是空的,因为SqlSessionHolder对象每次使用完都是销毁的并没有放到事务对象中,至于为什么spring要这么做,而且这里又有设置SqlSessionHolder的逻辑,就不多讨论了,可能有其他用途。
这里的openSessionfang方法返回的对象其实是DefaultSqlSession类型的对象,然后把这个对象作为参数传入mybatis的Excutor中执行:
Excutor是mybatis的执行器,它负责执行具体的数据库操作,在向数据库发送命令的时候,必然要先获得连接,它获得连接具体逻辑调用的是
DataSourceUtils类里面的doGetConnection方法:
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, “No DataSource specified”);

ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug(“Fetching resumed JDBC Connection from DataSource”);
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.

logger.debug(“Fetching JDBC Connection from DataSource”);
Connection con = fetchConnection(dataSource);

if (TransactionSynchronizationManager.isSynchronizationActive()) {
try {
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
catch (RuntimeException ex) {
// Unexpected exception from external delegation call -> close Connection and rethrow.
releaseConnection(con, dataSource);
throw ex;
}
}

return con;
}

这个方法的大致逻辑就是:
1、把connectionHolder从事务对象中取出来
2、如果是已经存在holder并且connection存在,就用已经存在的
3、如果没有,就从新获取一个连接并绑定到当前线程
其实这个地方理论上来说connectionHolder都是存在的,因为在启动事务的时候,我们的DataSourceTransactionManager已经把连接绑定到当前线程了,如下图所示:
spring基于注解的声明式事务原理分析
现在这个流程就已经很清楚了,上面的整个流程虽然看起来很复杂,其实大部分流程都是mybatis的流程,spring事务的核心思想其实还是很简单,就是在事务开始时把connection对象首先从dataSource里面获取出来放到事务对象的TreadLocal里面(把连接对象绑定到当前线程),每次mybatis的Excutor对象执行数据库操作的时候获取连接就直接从事务对象里面获取刚才绑定的连接,这样就可以保证在一个线程中多次获取连接都是同一个连接。