1. 程式人生 > >spring管理mybatis事務原始碼理解隨記

spring管理mybatis事務原始碼理解隨記

mysql中session和connection的關係:
mysql中一個session就是一個connection,區別在於,connection是物件池中的一個可複用物件,所以它就是一個物理連線,而session就是connection從物件池中被取出後做的一系列事情,直到connection再次被施加物件池(連線池)中時,發生的所有事情(這裡指資料庫互動),叫做一個session。

跟蹤程式碼分析:

事務攔截類 org.springframework.transaction.interceptor.TransactionInterceptor extends org.springframework.transaction.interceptor.TransactionAspectSupport implements org.aopalliance.intercept.MethodInterceptor, java.io.Serializable

MethodInterceptor 介面同cglib代理介面,只有一個方法Object invoke(MethodInvocation invocation) throws Throwable; 實現該方法則會在代理類的方法執行時先執行此方法。這就是入口。cglib的原理有興趣的同學可以自行研究。

TransactionInterceptor 的實現則是

@Override
public Object invoke(final 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, new InvocationCallback() { @Override public Object proceedWithInvocation() throws Throwable { return invocation.proceed(); } }); }

其中呼叫了 invokeWithinTransaction(Method, Class, InvocationCallback) 方法,這個方法是其父類 TransactionAspectSupport 的。
其中的實現比較長,有興趣的同學可以自行研究。

重點來了,
invokeWithinTransaction(Method, Class, InvocationCallback)
方法呼叫了
protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager, TransactionAttribute, String) 方法,其含義是在有必要的情況下建立一個新的事務,那麼就是說沒必要的時候就不要建立新事務了。
這其中則呼叫org.springframework.transaction.PlatformTransactionManager.getTransaction(TransactionDefinition)來建立新的事務。

這個介面的實現類就是mybatis事務控制使用的org.springframework.jdbc.datasource.DataSourceTransactionManager extends org.springframework.transaction.support.AbstractPlatformTransactionManager

AbstractPlatformTransactionManager.getTransaction(TransactionDefinition)
首先會呼叫doGetTransaction()獲取一個新的事務物件,然而在這裡,這個事務物件可能並不完全是一個新的。因為存在多個事務巢狀的情況存在(propagation使用預設值require),此處會使用資料來源從執行緒中獲取(ThreadLocal),以確定是否是巢狀的事務。事務物件中都會有一個屬性connectionHolder,每一個事務都會重新生成一個,並不一樣,但是隻會把第一個儲存線上程 ThreadLocal中,因為它們裡面包含的Connection是一樣的。同時這也是其它位置的程式碼獲取同一個connetion的唯一方式。
如果是巢狀的事務,則不會重新建立新的事務,而是在此基礎上處理。handleExistingTransaction(TransactionDefinition, Object, boolean)
處理方式則是根據事務配置中propagation的配置內容分別處理,有興趣的同學可以自行研究。這裡只討論TransactionDefinition.PROPAGATION_REQUIRED。因此巢狀的事務只是建立了一個新的事務狀態資訊TransactionInfo,並沒有再次建立資料庫事務,此TransactionInfo包含它的父級,也就是上一個事務資訊用於回溯(此時回到invokeWithinTransaction(Method, Class, InvocationCallback)方法)。

注意DataSourceTransactionManager.doBegin(Object transaction, TransactionDefinition definition)方法,第一個事務過來時,走的是這裡,但是隻走一次,後面的都不會再執行這裡的程式碼,因為事務只需要開啟一次,如果事務隔離級別需要修改,則是在這個方法中呼叫org.springframework.jdbc.datasource.DataSourceUtils.prepareConnectionForTransaction(con, definition)處理的,如果不需要修改,則需要等到第一次需要執行sql時,由sqlSessionFactory.openSession(ExecutorType execType)開啟事務。

然後就會通過InvocationCallback.proceedWithInvocation()回撥到最開始的入口方法處,進行業務處理。

接著走過層層代理之後,來到 org.apache.ibatis.binding.MapperProxy.invoke(Object proxy, Method method, Object[] args) 方法中。它會回撥真正的執行方法中。

其中的
sqlSession.insert(command.getName(), param)
會進入到
org.mybatis.spring.SqlSessionTemplate
的下面方法

@Override
public int insert(String statement, Object parameter) {
  return this.sqlSessionProxy.insert(statement, parameter);
}

這裡需要注意,這已經從mybatis的原始碼跳轉到mybatis-spring原始碼了,原因是,在配置mybatis的mapper和java介面時都是使用的mybatis-spring的配置,所以sqlSession也在內部被替換成spring的了。

而這裡的sqlSessionProxy是在SqlSessionTemplate建構函式中生成的代理類

this.sqlSessionProxy = (SqlSession) newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class },
    new SqlSessionInterceptor());

下面是這個內部代理類的資訊:
位置:org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor
原始碼:

  /**
 * Proxy needed to route MyBatis method calls to the proper SqlSession got
 * from Spring's Transaction Manager
 * It also unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to
 * pass a {@code PersistenceException} to the {@code PersistenceExceptionTranslator}.
 */
private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  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;
    } catch (Throwable t) {
      Throwable unwrapped = unwrapThrowable(t);
      if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
        // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        sqlSession = null;
        Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
        if (translated != null) {
          unwrapped = translated;
        }
      }
      throw unwrapped;
    } finally {
      if (sqlSession != null) {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

從註釋可以看出,這就是為了把mybatis的方法呼叫轉到spring方法呼叫,從而可以替換掉sqlSession,而達到管理事務的目的。(是在MapperFactoryBean中替換的,如果使用自動掃描,則是在ClassPathMapperScanner類中會有相關類建立MapperFactoryBean,從而在MapperFactoryBean的父類SqlSessionDaoSupport中。)
通過org.mybatis.spring.SqlSessionUtils.getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) 這個方法,回撥後會到mybatis中進行sql執行,而此時有sqlSession也有dataSource,但是卻沒有connetion(因為前兩者本地建立,而後者需要連線),這時mybatis的SimpleExecutor.prepareStatement() 會呼叫 getConnection()獲取連線,而這個連線是之前用SqlSessionUtils.getSqlSession()建立的,裡面包含有sqlSessionFactory,也就是會有 SpringManagedTransaction 進行事務連線建立。建立連線就在 SpringManagedTransaction.getConnection(),它是通過 DataSourceUtils.getConnection(this.dataSource) 呼叫 doGetConnection(DataSource dataSource) 來獲取連線的,所以通過和之前事務管理中一樣的dataSource,就能獲取到spring事務管理中的connectionHoler,再獲取其connection即可。然,如果spring事務和mybatis資料來源配置的不是同一個(即配置出錯),則無法獲取到spring事務中的connectionHolder,這時,doGetConnection(DataSource dataSource) 會呼叫傳來的dataSource建立一份新的connection,也就是不受事務控制的一個connection,返回給myabatis使用,所以一定需要保證spring和mybatis使用的是同一個資料來源(spring中的是真實生效的,mybatis中的只是一個引用,作為key用來獲取spring中相應的資料。)。

再轉回到一開始定義的ConnectionHolder中,從ThreadLocal中獲取出相應的connection,然後直接method.invoke執行程式中的sql。結束。

為什麼放在threadLocal中?
因為一個任務是一個執行緒執行,所以不管到了哪一行程式碼,都能獲取到。而datasource和sqlSessionFactory都是spring管理,所以可以直接作為key來使用。

到此為止:
今日分析的目標已經達到,這其中的程式碼,思想都是非常值得參考,學習的。學到自己的思想裡,才能運用自如。

2016-09-16 06:29:16
一夜,又過去了。