1. 程式人生 > >學習 MyBatis 的一點小總結 —— 底層原始碼初步分析

學習 MyBatis 的一點小總結 —— 底層原始碼初步分析

[toc] 在過去程式設計師使用 JDBC 連線資料庫,總會帶來諸多不便。MyBatis 是一款優秀的持久層框架,可以替代 JDBC 幫助我們更好的進行開發。要了解 MyBatis 的實現原理,首先我們要明白 MyBatis 的大致操作步驟。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405165733398.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 資料庫源告訴我們連線哪個資料庫,獲得要執行的SQL語句,再進行操作,這點者缺一不可。接下來要看的就是這三點在底層如何實現。
## MyBatis 如何獲取資料庫源? 使用 Mybatis 第一步肯定是要寫好配置檔案。官方給出的指導文件告訴我們,XML 配置檔案中包含了對 MyBatis 系統的核心設定,包括獲取資料庫連線例項的資料來源。 ```xml ``` 想要連線資料庫,必然要獲得資料來源資訊。既然上述配置檔案有資料庫源資訊,那我們只要進行解析就好了。 ```java String resource = "org/mybatis/example/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); ``` 由程式碼可見,我們要的是一個 SqlSessionFactory 例項,SqlSessionFactory 裡面就有我們所需的資料庫源資訊。通過new SqlSessionFactoryBuilder().build(inputStream) 返回SqlSessionFactory 例項,進入 build 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170229122.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) build 方法返回值就是是 SqlSessionFactory,注意到有 return build(parser.parse()),關注點在 parser.parse(),進入 parse 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170256905.png) parse 方法返回 Configuration 。parsed 是一個布林型別成員變數,預設值是 false,作判斷的目的是為了防止多執行緒情況下該方法被二次呼叫。**這個方法返回一個 Configuration 型別的例項,Configuration 是 BaseBuilder 類的一個成員變數,Configuration 其實儲存了配置檔案所有的資訊,只是現在還是一張白紙,需要再操作一番**。進入 parseConfiguration 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020040517032587.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 上一張圖的 parse.evalNode 方法將配置檔案中 configuration 標籤下的內容進行解析,封裝到一個物件,這個物件作為引數傳入 parseConfiguration 方法中。在 parseConfiguration 方法我們見到了很多熟悉的字樣,諸如 properties、typeAliases 之類的配置資訊,但我們的目的是要拿到資料庫源資訊,因此我們把目標放在包裹了資料庫源資訊的 environments 標籤上,進入 environmentsElement 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170433440.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 作為引數的 context 物件與之前一樣,封裝了 environments 標籤中的內容,我們還需要進一步解析 dataSource 標籤,關注 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")) 這段程式碼,進入dataSourceElement 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170629729.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 到這裡 context 物件只有 dataSource 裡的內容了。發現 type 的值為 POOLED(預設值),props 儲存最終的資料庫配置資訊。DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance() 這一段程式碼,進入 resolveClass 方法,最終再跳轉 resolveAlias 方法中。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170659513.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 注意 value = (Class) typeAliases.get(key),typeAliases 實際上是一個 HashMap,將 POOLED 作為 key 得到了儲存的對應的 Class 型別。回到 dataSourceElement 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170730734.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 得到返回的 Class 返回值,利用反射 newInstance 建立對應的 DataSourceFactory 物件,set 方法儲存 props ,回到 environmentsElement 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170759767.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 繼續執行後面的方法,最終資料庫源資訊封裝到一個 Environment 型別的例項,這個例項又通過 set 方法儲存到了 configuration 。configuration 已經處理就緒,被 parse 方法返回。回到之前的 build 方法,將 configuration 作為引數傳入至另一個過載的 build 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170825308.png) SqlSessionFactory 本身是一個介面,DefaultSqlSessionFactory 則是實現了 SqlSessionFactory 的實現類,儲存好 configuration 之後返回,就得到了我們開頭需要的 SqlSessionFactory 例項。
## MyBatis 如何獲取 sql 語句? 與獲取資料庫源類似,只要解析 Mapper 配置檔案中的對應標籤,就可以獲得對應的 sql 語句。之前我們講過,SqlSessionFactory 中的 configuration 屬性儲存資料庫源資訊,事實上這個 configuration 屬性將整個配置檔案的資訊都給封裝成一個類來儲存了。解析的前半部分與之前一樣,分歧點在之前提到的 parseConfiguration 方法,其中在 environmentsElement 方法下面還有一個 mapperElement 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170900979.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 配置檔案中 mappers 標籤載入mapper檔案的方式共有四種:resource、url、class、package。程式碼中的 if-else 語句塊分別判斷四種不同的載入方式,可見 package 的優先順序最高。parent 是配置檔案中 mappers 標籤中的資訊,通過外層的迴圈一個一個讀取多個 Mapper 檔案。這裡使用的方式是 resource ,所以會執行游標所在行的程式碼塊,進入 mapperParser.parse() 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405170944473.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 我們要的是 mapper 標籤的內容,因此我們關注 configurationElement(parser.evalNode("/mapper")) 這一句,進入 configurationElement 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171012991.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) context 就是我們解析整個 Mapper 檔案 mapper 標籤中的內容,既然現在得到了內容,那隻需再找到對應的標籤就能獲得sql語句了。注意 buildStatementFromContext(context.evalNodes("select|insert|update|delete")),我們看到了熟悉的 select、insert、update、delete,這些標籤裡就有我們寫 sql 語句。進入 buildStatementFromContext 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171040182.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) list 儲存了我們在 Mapper 檔案中寫的所有含有 sql 語句的標籤元素,用一個迴圈遍歷 list 的每一個元素,分別將每一個元素的資訊儲存到 statementParser 中。進入 parseStatementNode 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171059630.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171124154.png) 這個方法程式碼內容很多,僅摘出節選,裡面定義了很多區域性變數,這些變數用來儲存sql語句標籤(例如)的引數資訊(例如快取 useCache)。再把所有引數傳到 addMappedStatement 中。進入 addMappedStatement 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171151465.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) MappedStatement statement = statementBuilder.build(),使用 build 方法得到 MappedStatement 例項,這個類封裝了每一個含有sql語句標籤中所有的資訊,再是 configuration.addMappedStatement(statement),儲存到 configuration 中。
## MyBatis 如何執行 sql 語句? 既然有了 SqlSessionFactory,我們可以從中獲得 SqlSession 的例項。開啟 session 的語句是 SqlSession session = sessionFactory.openSession(),進入 openSession 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171220518.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) 最終會執行 openSessionFromDataSource 方法。在之前 environment 已經有了資料庫源資訊,呼叫 configuration.newExecutor 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171242900.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) Executor 叫做執行器,Mybatis 一共有三種執行器,用一個列舉類 ExecutorType 儲存,分別是 SIMPLE,REUSE,BATCH,預設就是 SIMPLE。if-else 語句判斷對應的型別,建立不同的執行器。在程式碼末端處有個 if 判斷語句,如果 cacheEnabled 為 true,則會建立快取執行器,預設是為 true,即預設開啟一級快取。 回到 openSessionFromDataSource 方法,最終返回一個 DefaultSqlSession 例項。得到 session 我們就可以執行 sql 語句了。SqlSession 提供了在資料庫執行 SQL 命令所需的所有方法。你可以通過 SqlSession 例項來直接執行已對映的 SQL 語句,以 selectOne 方法為例,進入該方法後發現,最終會呼叫到 selectList 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171306851.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) configuration.getMappedStatement(statement) 得到了我們之前儲存的 MappedStatement 物件,再呼叫 executor.query 方法,呼叫 query 方法之前會執行 wrapCollection 方法,儲存 sql 語句中使用者傳入的引數。進入 query 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171329682.png) boundSql 裡面就有我們要執行的 sql 語句,CacheKey 是用來開啟快取的。執行父類 BaseExecutor 中的 createCacheKey 方法,通過 id,offsetid,limited,sql 組成一個唯一的 key,呼叫下一個 query 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171352515.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) Cache cache = ms.getCache() 是二級快取,二級快取為空,直接呼叫 query 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171413301.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) list = resultHandler == null ? (List) localCache.getObject(key) : null 傳入 key 值在本地查詢,如果有返回證明 key 已經快取到本地,直接從本地快取獲取結果。否則 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql),去資料庫查詢。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171435927.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) localCache.putObject(key, EXECUTION_PLACEHOLDER) 首先將 key 快取至本地,下一次查詢就能找到這個 key 了。進入 doQuery 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020040517145755.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) stmt = prepareStatement(handler, ms.getStatementLog()),得到一個 Statement。進入 prepareStatement 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171521349.png) 我們看到了一個熟悉的 Connection 物件,這個就是原生 JDBC 的例項物件。回到 doQuery 方法,進入 handler.query(stmt, resultHandler) 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171541555.png) statement 強轉型為 PreparedStatement 型別,這下我們又得到了 PreparedStatement 的型別例項了,呼叫 execute 方法,這個方法也是屬於原生 JDBC。執行完成後 return resultSetHandler.handleCursorResultSets(ps),進入 handleCursorResultSets 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171605178.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) ResultSetWrapper rsw = getFirstResultSet(stmt),看到 getFirstResultSet 方法中的 ResultSet rs = stmt.getResultSet(),在這裡我們得到了 ResultSet 例項物件,最終 return rs != null ? new ResultSetWrapper(rs, configuration) : null,返回最終結果集。
## MyBatis 如何實現不同型別資料之間的轉換? 進入上一張圖中 ResultSetWrapper 中可以看到,其中包含三個成員變數 columnNames、classNames、jdbcTypes,三者都是 ArrayList 集合。看一下構造方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200405171632799.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5faGFuZHNvbWU=,size_16,color_FFFFFF,t_70) final ResultSetMetaData metaData = rs.getMetaData(),metaData 就是資料庫相關的資料,getColumnCount 統計有多少個欄位,迴圈加入到 columnNames、jdbcTypes、classNames。columnNames 儲存的就是實體類中的屬性名,jdbcTypes 儲存的是欄位在資料庫中的資料型別,classNames 儲存的是欄位在 Java 中的資料型別,比如 Java 的 String 與資料庫 VARCHAR,MyBatis 充當一箇中介完成轉換,真正實現 ORM 的核心