之前被同事問了一個問題:在我們的工程裡,事務的開啟跟關閉是由Spring負責的,但具體的SQL語句卻是由Mybatis執行的。那麼問題來了,Mybatis怎麼保證自己執行的SQL語句是處在Spring的事務上下文中?
原文地址:www.jianshu.com/p/6a880d20a…
注:這篇文章重點不是分析Spring事務的實現原理,但卻需要讀者提前瞭解Spring事務原理的一些知識點,這樣讀起來才會容易些
現在公司主流的開發框架大部分是使用spring+mybatis來運算元據庫,所有的事務操作都是交給spring去管理。當我們需要一個有事務上下文的資料庫操作時,我們的做法就是寫一個運算元據庫的方法,並在該方法上面加上@Transactional註解就可以了。
仔細思考一下這個過程,@Transactional是由spring進行處理的,spring做的事情是從資料來源(一般為資料庫連線池,比如說druid,c3p0等)獲取一個資料庫連線,然後在進入方法邏輯前執行setAutoCommit(false)操作,最後在處理成功或者出現異常的時候分別執行commit或者rollback操作。
那麼問題來了,開啟跟結束事務是由spring獲取到資料庫連線以後進行操作的,但我們實際執行的update或者insert語句卻是由mybatis獲取資料庫連線進行操作的,可以想到如果想讓事務生效,那麼spring跟mybatis使用的必須是同一個連線,真實情況是什麼樣呢?它們之間如何進行無縫銜接?讓我們通過原始碼來分析一下。
首先如果想在spring中使用mybatis,我們除了引入mybatis依賴以外,還需要引入一個包:mybatis-spring。
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>x.x.x</version>
</dependency>複製程式碼
可以猜測這個依賴包應該就是Spring跟Mybatis進行無縫連線的關鍵。
一般來說,我們在工程中的配置檔案往往是這樣:
<!--會話工廠 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
<!--spring事務管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--使用註釋事務 -->
<tx:annotation-driven transaction-manager="transactionManager" />複製程式碼
注:
1.會話工廠sqlSessionFactory跟Spring事務管理器transactionManager所使用的資料來源dataSource必須是同一個。
2.這裡的sqlSessionFactory型別是org.mybatis.spring.SqlSessionFactoryBean,該類是由我們引入的包mybatis-spring提供的。
看名字就知道SqlSessionFactoryBean是一個工廠bean,也就是說它交給Spring的真正例項是由getObject()方法提供的,那麼我們去看下它真正例項初始化原始碼:
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
//可以看出邏輯都在這裡面
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
//此處省略一些校驗邏輯
//...
this.sqlSessionFactory = buildSqlSessionFactory();
}
//最後來看這個最核心的方法
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
//...
//省略一些其他初始化資訊,我們重點關注事務處理邏輯
if (this.transactionFactory == null) {
//可以看出,mybatis中把事務操作交給了SpringManagedTransactionFactory去做
this.transactionFactory = new SpringManagedTransactionFactory();
}
configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
//省略後續邏輯
//...
}複製程式碼
下面我們再去看看SpringManagedTransactionFactory類的原始碼:
public class SpringManagedTransactionFactory implements TransactionFactory {
/**
* {@inheritDoc}
*/
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new SpringManagedTransaction(dataSource);
}
/**
* {@inheritDoc}
*/
@Override
public Transaction newTransaction(Connection conn) {
throw new UnsupportedOperationException("New Spring transactions require a DataSource");
}
/**
* {@inheritDoc}
*/
@Override
public void setProperties(Properties props) {
// not needed in this version
}
}複製程式碼
程式碼很少,且只有一個方法是有效的,看來離成功越來越近了,繼續跟進去看看SpringManagedTransaction的原始碼:
@Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
}複製程式碼
省略該類中其他部分,我們重點看獲取連線的地方,這裡最關鍵的地方就在this.connection = DataSourceUtils.getConnection(this.dataSource);,
DataSourceUtils全名是org.springframework.jdbc.datasource.DataSourceUtils,沒錯,它是由Spring提供的類,根據我們之前的猜測,Spring開啟事務以後,Mybatis要想讓自己的SQL語句處在這個事務上下文中操作,那必須拿到跟Spring開啟事務同一個資料庫連線才行,由於DataSourceUtils類是由Spring提供的,看來跟我們開始猜測的結果類似,我們接下來看看DataSourceUtils原始碼驗證一下:
//獲取資料庫連線最終落在該方法上,我刪除一些不重要的程式碼
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
//TransactionSynchronizationManager重點!!!有沒有很熟悉的感覺??
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
ConnectionHolder holderToUse = conHolder;
if (conHolder == null) {
holderToUse = new ConnectionHolder(con);
} else {
conHolder.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
} else {
conHolder.requested();
if (!conHolder.hasConnection()) {
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
}複製程式碼
看到TransactionSynchronizationManager有沒有很親切的感覺?對Spring事務管理原始碼熟悉的同學會馬上聯想到Spring開啟事務以後,就是把相應的資料庫連線放在這裡,我擷取原始碼看一下:
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}複製程式碼
這段程式碼具體就是在我們上面配置的org.springframework.jdbc.datasource.DataSourceTransactionManager類中的doBegin方法裡。至於TransactionSynchronizationManager類的實現原理其實我覺得你已經猜到了,沒錯,就是Java中經典類庫ThreadLocal類!!!
最後補上一張圖來說明spring+mybatis事務過程資料來源獲取邏輯: