MyBatis原始碼閱讀——通過debug解析MyBatis執行流程
前言
最近在閱讀MyBatis框架的原始碼。發現它其實是一個非常值得閱讀的框架。它靈活得運用了常見的設計模式去設計。值得我們去學習。我還是比較喜歡以debug閱讀MyBatis的原始碼。下面,就一起來看看吧。
首先,我們先寫一個demo,以供除錯使用
public class Demo1SessionFactory { public static void main(String[] args) throws IOException { String resource = "mybatis/conf/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); //從 XML 中構建 SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session = sqlSessionFactory.openSession(); try { BlogMapper mapper = session.getMapper(BlogMapper.class); Blog blog = mapper.selectBlog(1L); System.out.println(blog); blog = mapper.selectBlog(1L); System.out.println(blog); } finally { session.close(); } } }
從demo分析上層流程
在demo中,我們先從最上層分析一下流程。
我們首先是建立SqlSessionFactory,這其中需要使用配置資訊。然後從SqlSessionFactory獲取SqlSession,再從SqlSession中獲取Mapper。這是我們最上層的流程。那這這幾步操作中,內部操作了什麼呢?我們需要debug去分析。在分析之前,我們需要先了解一下MyBatis的架構和使用的設計模式,這樣能幫助我們更輕鬆的去了解它的原始碼。
作用域(Scope)和生命週期
SqlSessionFactory
SqlSessionFactory 一旦被建立就應該在應用的執行期間一直存在,沒有任何理由對它進行清除或重建。使用 SqlSessionFactory 的最佳實踐是在應用執行期間不要重複建立多次,多次重建 SqlSessionFactory 被視為一種程式碼“壞味道(bad smell)”。因此 SqlSessionFactory 的最佳作用域是應用作用域。有很多方法可以做到,最簡單的就是使用單例模式或者靜態單例模式。SqlSession
每個執行緒都應該有它自己的 SqlSession 例項。SqlSession 的例項不是執行緒安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。絕對不能將 SqlSession 例項的引用放在一個類的靜態域,甚至一個類的例項變數也不行。也絕不能將 SqlSession 例項的引用放在任何型別的管理作用域中,比如 Servlet 架構中的 HttpSession。如果你現在正在使用一種 Web 框架,要考慮 SqlSession 放在一個和 HTTP 請求物件相似的作用域中。換句話說,每次收到的 HTTP 請求,就可以開啟一個 SqlSession,返回一個響應,就關閉它。這個關閉操作是很重要的,你應該把這個關閉操作放到 finally 塊中以確保每次都能執行關閉。下面的示例就是一個確保 SqlSession 關閉的標準模式:
其實我感覺這個跟Servlet中的生命週期類似,SqlSessionFactory當做上下文(Application),SqlSession當做一個會話(session),這樣就好理解了。一個SqlSession就是與資料庫進行一次互動。
從作用域和生命週期的分析,我們可以分析出,它的核心是SqlSession。
檢視org.apache.ibatis.session.defaults.DefaultSqlSessionFactory原始碼,檢視SqlSession的獲取實現過程
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
再看SqlSession類的結構:
SqlSession 主要由Configuration和Executor構成,其中有增刪改查的各種方法。
由此我們可以推理出我們用MyBatis進行操作的主要操作入口都在這裡。那麼,接下來就涉及到我們是如何獲取到具體的Mapper
具體Mapper的獲取過程
檢視 org.apache.ibatis.session.SqlSession ->getMapper() 找到實現
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
繼續深入,找到org.apache.ibatis.binding.MapperRegistry ->getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
可以分析出這是通過動態代理獲取的,而它在獲取的時候需要兩個引數Class type, SqlSession sqlSession。SqlSession是作為被代理的物件。type作為key去Map中拿MapperProxyFactory來產生代理類需要的引數。
Mapper的操作流程
現在,我們已經知道具體的Mapper的來歷了,那麼就可以去了解一下它是如何使用的。因為它是SqlSession進行了動態代理類,所以,我們debug SqlSession,進行一次查詢操作。就如下圖所示進入除錯。
debug下去,你會看到,底層是通過Executor執行的
然後繼續深入,可以看到sql的組裝過程:
Executor是MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護;
接下來就是類似傳統JDBC的操作了
MyBatis核心構件
名稱 | 作用 |
---|---|
SqlSession | 作為MyBatis工作的主要頂層API,表示和資料庫互動的會話,完成必要資料庫增刪改查功能 |
Executor | MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護 |
StatementHandler | 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數、將Statement結果集轉換成List集合 |
ParameterHandler | 負責對使用者傳遞的引數轉換成JDBC Statement 所需要的引數 |
ResultSetHandler | 負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合 |
TypeHandler | 負責java資料型別和jdbc資料型別之間的對映和轉換 |
MappedStatement | MappedStatement維護了一條select |
SqlSource | 負責根據使用者傳遞的parameterObject,動態地生成SQL語句,將資訊封裝到BoundSql物件中,並返回 |
BoundSql | 表示動態生成的SQL語句以及相應的引數資訊 |
Configuration | MyBatis所有的配置資訊都維持在Configuration物件之中 |