Mybatis的源碼解析(一)
寫在前面的話
隨着越來越多的Java工程師湧入開發的行列,以往的只是簡單會用已經越來越容易被這個行業所淘汰,尤其是隻能靠着開發賺取這一微薄的薪水。在公司往復開發平凡的業務SQL接口等,這些都將非常容易被淘汰,而擺脫此困境只能更加需要深入的學習,形成自己的體系和認知,以便跳出這個風口,不做平凡職業者,向頂端看齊,讓我們一起學習一起努力進步,只有這樣,薪水纔會隨着年限的增加而增加。
學習方法
學習這件事情很奇妙,有時候靜下心來好好學居然也會上癮。但偶爾間斷了幾天不學,似乎又不想要繼續學了,除了最開始的興奮,只剩下乏力和枯燥。然後感覺經常記憶只是放了一個錨:知道問題可以在哪兒解決,而不是自己去整理解決方法,遇到問題都是搜索完以後又忘卻,下次遇到又繼續去搜索。這樣往往復復什麼都沒學到,後來我總結了一些方法,我也正嘗試着使用這些方法(個人方法,不喜可跳過),希望自己以後遇到問題時也會回來回顧和修改之前的一些想法:
- 做筆記並學會總結 ,當學習到一個新知識時,我當場可能會截圖,或者留下一個筆記,但並沒有做好總結,以至於我面試時感覺自己好像什麼都會,又感覺什麼都不會,沒有有亮點的技術也沒留下像樣的文稿和資料。當你學會並開始做筆記時就好像把學到的知識在腦海裏過了一遍,可以深刻並在這裏留下了一個錨,以便於以後更方便尋找或者向下挖掘。
- 講給別人聽,當你學完了一個新知識,你可以試着講給別人聽,如果別人也能在你複述的情況下能聽懂,那麼你對新知識的理解和全局觀又進了一步,並且能深刻記住這些知識。
- 從點到線到面 學習,有些學習例如源碼,我原以爲我會從一個大的框架開始解讀源碼,我發現我錯得很離譜,那些大而廣的知識只適合和外行吹牛,而對你真正使用的時候幫助不是很大,而且解讀源碼將會非常頭痛,讓人更容易放棄深入學習。
正文
源碼解讀是對自己學習的一個檢驗,希望自己能學到的同時也能幫助更多的人理解相應的知識,然後反哺自己學習的短板。那我們開始學習吧!
JDBC的執行過程
我們知道,mybatis執行最終還是還是在使用jdbc的執行,同時當做複習,我們應該也知道jdbc的執行過程:
我們使用jdbc時一共執行的五步:
1.獲取connection
2.預編譯.prepareStatment
3.execute執行結果
4.獲取返回值
5.close連接
JDBC三種執行器
1.statement簡單執行器
基本功能:執行靜態的SQL
傳輸相關:批處理,設置加載的行數
@Test
public void statementBatchTest() throws SQLException {
String sql = "INSERT INTO `db_user` (`name`,`nickname`,`status`,`password`) VALUES ('toto','ynwrd',1,'md5');";
Statement statement = connection.createStatement();
// statement.setFetchSize(20);//一次可以讀取多少行結果
long nowTime = System.currentTimeMillis();
for (int i = 0; i < 20; i++) {
// statement.execute(); //單條執行
statement.addBatch(sql); //添加進批量量,相當於填充進彈藥
// statement.addBatch(); // 添加批處理參數
}
statement.executeBatch(); // 一次全部提交
System.out.println(System.currentTimeMillis() - nowTime); //批處理效率明顯高於單條執行
statement.close();
}
2.preparedStatement預處理執行器
prepardStatement除了簡單執行器的功能,還包括對參數進行預編譯,可以有效的防止SQL注入
能注入的例子:
// sql注入測試
public int selectListByName(String name) throws SQLException {
//傳入參數爲 admin' or '1'='1 時就會有SQL注入的問題,能直接拉全庫,
// 甚至改動參數能刪除數據庫或者拉下其它表的數據,造成不可預估的危險
String sql = "SELECT * FROM db_user WHERE `name`='" + name + "'";
System.out.println(sql);
Statement statement = connection.createStatement();
statement.executeQuery(sql);
ResultSet resultSet = statement.getResultSet();
int count=0;
while (resultSet.next()){
count++;
}
statement.close();
return count;
}
使用prepardStatement 處理器後有效防止注入的例子:
public int selectListByName2(String name) throws SQLException {
// 參數再怎麼變也沒用,有效防止sql注入
String sql = "SELECT * FROM db_user WHERE `name`=?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1,name);
System.out.println(statement);
statement.executeQuery();
ResultSet resultSet = statement.getResultSet();
int count=0;
while (resultSet.next()){
count++;
}
statement.close();
return count;
}
prepardStatement 批量提交的例子:
@Test
public void prepareBatchTest() throws SQLException {
String sql = "INSERT INTO `db_user` (`name`,`nickname`,`status`,`password`) VALUES ('toto','ynwrd',1,?);";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setFetchSize(20);//一次可以讀取多少行結果
long nowTime = System.currentTimeMillis();
for (int i = 0; i < 20; i++) {
preparedStatement.setString(1, i+"號player");
// preparedStatement.execute(); //單條執行
preparedStatement.addBatch(); //添加批處理參數
// preparedStatement.addBatch(sql); // 添加進批量,如果要用這個則必須是靜態SQL
}
preparedStatement.executeBatch(); // 一次全部提交
System.out.println(System.currentTimeMillis() - nowTime); //批處理效率明顯高於單條執行
preparedStatement.close();
}
簡單執行器和預編譯執行器對比圖例:
批量提交時圖例:
3.存儲過程處理器CallableStatement
能設置出參,讀取出參
mybatis執行過程
從現在開始說mybatis了,我認爲步子要一步一步走,飯要一口一口的喫,由於mapper映射和出參設置那些都比較難,我們先看大框架,再從小螺絲一個一個擰。
sqlsession:sql會話,我們所有的mybatis都要通過它來調用,它擁有的功能包括基本的增刪改查,還有輔助功能包括提交和關閉會話。
Executor:執行器,除了提供基本的改、查、緩存維護,還有輔助功能包括提交、關閉執行器、批處理刷新。
StatementHandler:聲明處理器,執行器調用聲明處理器並且做參數處理和結果處理。
後面在總結和描述中會用更加細粒度來解釋這些詞語,如果沒有基礎,你現在強行記會比較費力,我只是在帶入後面需要說的部分
我們看一下sqlsession給我們的查詢方法:
我們看到sqlsession中有很多名字的方法重用,這是一種門面模式
select(String,Object,RowBounds,ResultHandler)
String:statementId,即mapper中的方法的全路徑+方法名:例如:com.toto.UserMapper#selectByid
Object: 傳入的參數
RowBounds:分頁類,可以用來做分頁的類,默認值分頁的大小是 Integer.MAX_VALUE
ResultHandler:結果集處理對象
注意:我們的會話一次可能會對應多條SQL,所以我們的會話不是線程安全的,一個會話只能由一個線程來控制,同時,我們的會話對應了一個執行器,所以我們的會話下的執行器和聲明處理器都不能跨線程使用。一個會話對應一個執行器,一條sql對應一個聲明處理器,對應比例是1:1:n(sql數量)。這些知識可能難以消化,但經過後面的學習會逐步理解整個流程。
根據這個體系,我們一個體系一個體系來講,由於篇幅較長,本文只介紹執行器的體系
Executor執行器體系
//源代碼路徑:org.apache.ibatis.session.defaults.selectList
//門面模式最典型就是不管擴充了多少,最終都會用最多參數的那個類,本文以selectList來追溯源碼
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
我們看到會話執行的查詢最終使用的是executor執行器,同時它的構造方法也有傳入執行器的類型,那我們應該選擇哪種執行器呢?執行器中又有哪些故事呢?它們不同的特別又是什麼呢?讓我們帶着問題進入執行器的探究。
@Test
public void sessionTest(){
//ExecutorType是一個枚舉類,它有SIMPLE、REUSE、BATCH
SqlSession sqlSession = factory.openSession(ExecutorType.SIMPLE,true);
// 降低調用複雜性
List<Object> list = sqlSession.selectList("com.toto.UserMapper.selectByid", 1);
System.out.println(list.get(0));
}
然後我們Debug來跑程序,隨後又發現了不同的執行器
當我們斷點繼續往下走,我們進了CachingExecutor,它好像沒有真的執行器實現,彷彿只是依靠傳入的executor來做事情:
想起來了!這是一個裝飾者模式:最輕簡的實現方式,通過獲取實例,來切面完成基礎功能和追加的功能。後面我們再細講這個裝飾者模式,隨後Debug繼續深入,我們看到它又調用了一個query方法,也就是追加了一部分功能,這些功能後面我寫二級緩存時會講解到,然後使用delegate來繼續調用query方法,然後我們深入到了BaseExecutor,原來SimpleExecutor繼承了BaseExecutor,隨後BaseExecutor轉向後又調用了doQuery方法,最後纔到我們的SinpleExecutor。總結一下,我們發現了一共5個執行器,我先畫出來關係圖,隨後逐個講解這些執行器能做哪些事情吧。。
//源碼位置在org.apache.ibatis.executor的322行
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// doQuery方法是抽象方法,由SimpleExecutor、ReuseExecutor、BatchExecutor 來實現
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
SimpleExecutor 簡單執行器
不緩存,有多少條SQL執行多少條
// 簡單執行器測試
@Test
public void simpleTest() throws SQLException {
SimpleExecutor executor = new SimpleExecutor(configuration, jdbcTransaction);
ms = configuration.getMappedStatement("com.toto.UserMapper.selectByid");
List<Object> list = executor.doQuery(ms, 1, RowBounds.DEFAULT,
SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
executor.doQuery(ms, 1, RowBounds.DEFAULT,
SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
System.out.println(list.get(0));
}
Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, ynwrd, 2016-08-07 14:07:10
Total: 1
Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, ynwrd, 2016-08-07 14:07:10
Total: 1
根據日誌顯示運行了兩次doQuery打印了兩遍日誌
ReuseExecutor 重用執行器
同樣的SQL(不管是不是同一個statementId,只要SQL一樣,那麼就會重用),並且緩存週期是這個會話
// 重用執行器
@Test
public void ReuseTest() throws SQLException {
ReuseExecutor executor = new ReuseExecutor(configuration, jdbcTransaction);
List<Object> list = executor.doQuery(ms, 1, RowBounds.DEFAULT,
SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
// 相同的SQL 會緩存對應的 PrepareStatement 它的緩存週期:這個會話
executor.doQuery(ms, 10, RowBounds.DEFAULT,
SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
System.out.println(list.get(0));
}
Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, ynwrd, 2016-08-07 14:07:10
Total: 1
Parameters: 10(Integer)
Total: 0
根據日誌顯示運行了兩次doQuery只打印了一遍SQL的日誌,但兩次參數都打印了。
BatchExecutor 批處理執行器
批處理執行器一般用在修改SQL的場景時使用,將對SQL進行一次性打包插入,使用這個性能比SimpleExecutor性能要高一些的,但一定要執行doFlushStatements才能生效
// 批處理執行器
@Test
public void BatchTest() throws SQLException {
BatchExecutor executor = new BatchExecutor(configuration, jdbcTransaction);
MappedStatement setName = configuration
.getMappedStatement("com.toto.UserMapper.setNickName");
Map param = new HashMap();
param.put("arg0", 1);
param.put("arg1", "管理員大哥");
executor.doUpdate(setName, param); //修改的第一條sql
executor.doUpdate(setName, param);// 修改的第二條sql
executor.doFlushStatements(false);
}
==> Preparing: update db_user set nickname=? where id=?
==> Parameters: 管理員大哥(String), 1(Integer)
==> Parameters: 管理員大哥(String), 1(Integer)
能將數據批量新增或者修改,如果遇到需要批量新增或者修改時,可以使用這種執行器,效率將得到極大提升
CachingExecutor
注意:1.二級緩存只有提交後才能使用 2.Bean開啓緩存需要實例化 3.需要開啓二級緩存,如:@CacheNamespace
錄了一個gif,使用commit後能獲取到緩存的圖片
代碼流程:commit以後就提交到二級緩存上,就可以允許被其它會話使用。二級緩存我們後面再開篇幅詳細講解,因爲緩存也存在很多級,東西比較多
代碼示例:
@Test
public void cacheExecutorTest() throws SQLException {
// BaseExecutor
Executor executor = new SimpleExecutor(configuration,jdbcTransaction);
// 裝飾器模式
Executor cachingExecutor=new CachingExecutor(executor);// 二級緩存相關邏輯 執行數據操作邏輯
cachingExecutor.query(ms, 1, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
cachingExecutor.commit(true);
// 提交之後纔會更新
cachingExecutor.query(ms, 1, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
}
輸出SQL時:
Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, 管理員大哥, 2016-08-07 14:07:10
Total: 1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@130520d]
Cache Hit Ratio [com.toto.UserMapper]: 0.5
緩存命中率0.5,因爲我們只跑了兩次,還有一次存儲緩存,一次命中緩存
Mybaits生成CachingExecutor執行流程
關於spring與mybatis的故事我們後面再聊,在這裏我們先簡單說一下我們生成的流程:
1.SqlSessionFactory.openSession
2.Configuration.newExecutor()
3.構建Executor:例如SimpleExecutor
4.包裝CacheExecutor
Executor執行過程
我這裏簡單展示一下查詢的執行過程,也將分爲四步,debug跑
1.查詢開始
2.sqlSession有執行器的所有功能,因爲它手上拿着執行器:
當沒能觸發二級緩存時(二級緩存應用程序不倒,緩存就不會被GC,除非調用清空方法,後續有講解),就會往下繼續調用:
3.BaseExecutor執行query方法,處理1級緩存和相關邏輯
4.BaseExecutor執行到實際查詢時:
本篇結尾
由於篇幅較長,第一篇解析就到這裏,請期待更多後續,也希望大家多實踐多總結,這樣東西才能學到並掌握