Mybatis原始碼與設計模式淺析
工廠方法模式
簡而言之,就是定義一個產品介面,定義一個工廠介面(包含生產產品的方法),每一個產品介面實現類都對應一個工廠介面的實現類去構造對應的產品實現類。
例如,Mybatis中的SqlSession介面和SqlSessionFactory介面,類圖如下,這裡的SqlSessionManager先暫時忽略。
圖中的DefaultSqlSessionFactory就是生產DefaultSqlSession的工廠。
沒錯,忽略了SqlSessionManager之後,這倆介面都只有一個實現類,首先來看看這倆介面。
SqlSession介面
/** * The primary Java interface for working with MyBatis. * 這是使用Mybatis的主要Java介面 * Through this interface you can execute commands, get mappers and manage transactions. * 通過這個介面,你可以執行命令,獲取mappers(對映器)以及管理事務 * * 接口裡定義了sql語句對應的方法以及快取、事務等方法 * */ public interface SqlSession extends Closeable {}
SqlSessionFactory介面
/**
* Creates an SqlSession out of a connection or a DataSource
* 基於連線或者資料來源建立一個SqlSession
*
* SqlSession 的工廠介面,接口裡定義了一系列openSession方法及獲取配置方法。
*
*/
public interface SqlSessionFactory {}
兩個介面的方法就不介紹了,畢竟也沒啥好說。使用過Mybatis的小夥伴一定對下面這段程式碼十分熟悉。(摘自官方文件)
String resource = "org/mybatis/example/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); //SqlSessionFactory的構建使用了建造者模式 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session = sqlSessionFactory.openSession(); try { BlogMapper mapper = session.getMapper(BlogMapper.class); Blog blog = mapper.selectBlog(101); } finally { session.close(); }
這是單獨使用Mybatis的基本用法,每次我們都使用工廠的openSession()來new一個DefaultSqlSession物件。所有openSession方法最終都是呼叫這兩個方法。
/** * 通過資料來源來開啟session * @return new DefaultSqlSession(configuration, executor, autoCommit) */ private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {} /** * 通過資料庫連線來開啟session * @return new DefaultSqlSession(configuration, executor, autoCommit); */ private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {}
因此,在我們單獨使用Mybatis的情況下,每次都是new一個新的session,雖然DefaultSqlSession是執行緒不安全的,但是我們單獨使用是沒問題的。
代理模式
但是我們一般很少單獨使用Mybatis,基本都是配合Spring這貨一起用的。還記得下面這段配置嗎?為什麼我們需要這樣配置呢?
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory" />
</bean>
首先,Spring中的bean預設是單例的,但是呢DefaultSqlSession又是非執行緒安全的,那麼如果將Default-SqlSession直接交由Spring管理,我把它將配成多例是不是就可以了呢?
理論上好像是沒問題的,但是每次資料庫操作都去new一個物件未免太損耗效能了。(這是我覺得不能直接使用DefaultSqlSession的一個原因 )。而且若是每次使用都new一個物件的話,那麼事務交由Spring管理的時候,那麼事務就無法生效了。因此,就有了上面一段配置了。
SqlSessionTemplate也實現了SqlSession介面,而且在類註釋的第一句就說明了他是執行緒安全的,那麼,他又是如何實現執行緒安全的呢?
SqlSessionTemplate
成員變數
//Session工廠
private final SqlSessionFactory sqlSessionFactory;
//執行的型別 有simple,batch,reuse
private final ExecutorType executorType;
//根據命名看出這是一個代理的SqlSession物件,重點就在於這個代理物件啦
private final SqlSession sqlSessionProxy;
// 根據名字翻譯為持久化異常翻譯器
private final PersistenceExceptionTranslator exceptionTranslator;
接著來看建構函式
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
//生成代理類,表示SqlSession這個藉口的方法都被SqlSessionInterceptor攔截。
//這裡就是代理模式的體現啦
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
那麼SqlSessionInterceptor又做了些什麼呢
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//這個方法就是關鍵,獲取session,
//它大概的流程就是若是在spring的事務管理之中,會共用一個SqlSession,將引用次數加1
//否則每次執行方法都是new一個新的SqlSession
//具體實現則是通過ThreadLocal來實現
//比如,我們直接在controller中呼叫dao,每次呼叫都會產生一個新的SqlSession物件
//而在service中加了事務的方法中多次呼叫dao的方法,則是共用一個SqlSession物件
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
// 因此,最終執行方法還是通過DefaultSqlSession來執行的
Object result = method.invoke(sqlSession, args);
//若不是spring事務管理的
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
//強制提交
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
......
} finally {
if (sqlSession != null) {
//若是事務交由spring管理則釋放引用次數,即減1,
//否則直接關閉
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
SqlSessionManager
現在再去看SqlSessionManager這個類,這個類除了一個作者之外再無其他註釋,所以無法直接看出他的作用,他實現SqlSession和SqlSessionFactory這兩個介面,而且他也使用了類似SqlSessionTemplate的代理模式,程式碼就不貼了,並且一樣的利用了ThreadLocal來提供了執行緒安全的SqlSession。
個人猜測,這個類是出於在單獨使用Mybtais時候能夠讓使用者方便地構造執行緒安全的SqlSession的目的而提供的。
關於代理模式,個人認為和其他設計模式是有一個很大的區別的 —— 其他的設計模式主要側重於介面和類的設計與類與類之間的配合使用,也可以說是側重於設計; 而代理模式則更側重於開發實現,換言之,學習代理模式更傾向於學習具體的實現和使用方式,因為代理模式依賴於語言特性或者說是程式語言本身提供的相關介面(動態代理)。
那麼問題來了
歸根結底,SqlSessionTemplate中保證執行緒安全主要還是依賴於ThreadLocal來實現,那麼,為什麼是將一個SqlSession繫結到一個事務中呢?比如一個controller呼叫了service層的三個加了事務的方法,那麼完成這個請求則需要新建三個SqlSession,為什麼Spring不在一個請求中共用一個SqlSession呢,這樣效能豈不是更好?
我個人的理解是如果一個請求都共用一個SqlSession,那麼close這個session的任務就歸屬到了事務方法的呼叫方,Web開發一般就是controller層,而controller層的職責應該是請求的接收和返回對應的資料,若是把close交由controller層處理,導致controller承擔了本不屬於他的責任,這樣就違背了我們分層的初衷了。