1、SqlSession下的四大對象介紹
通過前面的分析,我們應該知道在Mybatis中的,首先是通過SqlSessionFactoryBuilder加載全局配置文件(包括SQL映射器),這些配置都會封裝在Configuration中,其中每一條SQL語句的信息都會封裝在MappedStatement中。然后創建SqlSession,這時還會初始化Executor執行器。最后通過調用sqlSession.getMapper()來動態代理執行Mapper中對應的SQL語句。而當一個動態代理對象進入到了MapperMethod的execute()方法后,它經過簡單地判斷就進入了SqlSession的delete、update、insert、select等方法,這里是真正執行SQL語句的地方。那么這些方法是如何執行呢?答:實際上SqlSession的執行過程是通過Executor、StatementHandler、ParameterHandler和ResultSetHandler來完成數據庫操作和結果返回的,它們簡稱為四大對象:
- Executor:代表執行器,由它調度StatementHandler、ParameterHandler、ResultSetHandler等來執行對應的SQL,其中StatementHandler是最重要的。
- StatementHandler:作用是使用數據庫的Statement(PreparedStatement)執行操作,它是四大對象的核心,起到承上啟下的作用,許多重要的插件都是通過攔截它來實現的。
- ParameterHandler:是用來處理SQL參數的。
- ResultSetHandler:是進行數據集(ResultSet)的封裝返回處理的,它非常的復雜,好在不常用。
這四個對象都是通過Mybatis的插件來完成的,在實例化Executor、StatementHandler、ParameterHandler、ResultSetHandler四大接口對象的時候都是調用interceptorChain.pluginAll() 方法插入進去的(Mybatis的插件實現原理可以參考:鏈接1 ,鏈接2 )。下面依次分析這四大對象的生成和運作原理。
2、四大對象的生成和運作原理的引入
這里來分析一條查詢SQL的執行過程:首先肯定是要構建SqlSessionFactory、SqlSession和動態代理Mapper對象的,前面已經介紹過了,所以不多說,主要來看具體是怎么樣執行的。當動態代理對象獲取MapperMethod對象后,通過其內部的execute()方法調用sqlSession.selectList()方法來真正執行SQL,所以繼續從這里來跟蹤代碼:
SqlSession的實現類為DefaultSqlSession,所以去DefaultSqlSession中查看selectList()方法:
***********************************************************
可以看到這里獲取了MappedStatement對象,並且調用了executor對象的query()方法來執行SQL。所以我們來看看Executor類。
3、Executor對象
Executor表示執行器,它是真正執行Java和數據庫交互的對象,所以它十分重要,每一個SqlSession都會擁有一個Executor對象,這個對象負責增刪改查的具體操作,我們可以簡單的將它理解為JDBC中Statement的封裝版。。Executor的關系圖如下:
- BaseExecutor:是一個抽象類,采用模板方法的設計模式。它實現了Executor接口,實現了執行器的基本功能。
- SimpleExecutor:最簡單的執行器,根據對應的SQL直接執行即可,不會做一些額外的操作;拼接完SQL之后,直接交給 StatementHandler 去執行。
- BatchExecutor:批處理執行器,用於將多個SQL一次性輸出到數據庫,通過批量操作來優化性能。通常需要注意的是批量更新操作,由於內部有緩存的實現,使用完成后記得調用flushStatements來清除緩存。
- ReuseExecutor :可重用的執行器,重用的對象是Statement,也就是說該執行器會緩存同一個sql的Statement,省去Statement的重新創建,優化性能。內部的實現是通過一個HashMap來維護Statement對象的。由於當前Map只在該session中有效,所以使用完成后記得調用flushStatements來清除Map。調用實現的四個抽象方法時會調用 prepareStatement()
- CachingExecutor:啟用於二級緩存時的執行器;采用靜態代理;代理一個 Executor 對象。執行 update 方法前判斷是否清空二級緩存;執行 query 方法前先在二級緩存中查詢,命中失敗再通過被代理類查詢。
我們來看看Mybatis是如何創建Executor的,其實在前面已經介紹過了,它是在Configuration類中完成的,這里不看可以跳過:
Executor對象會在MyBatis加載全局配置文件時初始化,它會根據配置的類型去確定需要創建哪一種Executor,我們可以在全局配置文件settings元素中配置Executor類型,setting屬性中有個defaultExecutorType,可以配置如下3個參數:
- SIMPLE: 簡易執行器,它沒有什么特別的,默認執行器
- REUSE:是一種能夠執行重用預處理語句的執行器
- BATCH:執行器重用語句和批量更新,批量專用的執行器
默認使用SimpleExecutor。而如果開啟了二級緩存,則用CachingExecutor進行包裝,SqlSession會調用CachingExecutor執行器的query()方法,先從二級緩存獲取數據,當無法從二級緩存獲取數據時,則委托給BaseExecutor的子類進行操作,CachingExecutor執行過程代碼如下:
如果沒有使用二級緩存並且沒有配置其它的執行器,那么MyBatis默認使用SimpleExecutor,調用父類BaseExecutor的query()方法:
注意:query方法有兩種形式,一種是直接查詢;一種是從緩存中查詢,下面來看一下源碼:
當有一個查詢請求訪問的時候,如果開啟了二級緩存,首先會經過Executor的實現類CachingExecutor,先從二級緩存中查詢SQL是否是第一次執行,如果是第一次執行的話,那么就直接執行SQL語句,並創建緩存,如果第二次訪問相同的SQL語句的話,那么就會直接從緩存中提取。如果沒有開啟二級緩存,是第一次執行,直接執行SQL語句,並創建緩存,再次執行則從一級緩存中獲取數據,如下源代碼所示:
而如果一級緩存也沒有數據,則調用queryFromDatabase()從數據庫中獲取數據:
在queryFromDatabase()方法中調用SimpleExecutor的 doQuery() 方法(注意:這里說是調用了SimpleExecutor的方法,但是還在BaseExecutor類中是因為SimpleExecutor繼承了它,所以SimpleExecutor對象中也有這個方法,而doQuery()方法在子類SimpleExecutor實現的,所以說是調用SimpleExecutor的 doQuery() 方法。),其方法代碼如下:
這里顯然是根據Configuration對象來構建StatementHandler,然后使用prepareStatement()方法對SQL編譯和參數進行初始化。實現過程是:它調用了StatementHandler的prepare() 進行了預編譯和基礎的設置,然后通過StatementHandler的parameterize()來設置參數,這個parameterize()方法實際是通過ParameterHandler來對參數進行設置。最后使用 StatementHandler的query()方法,把ResultHandler傳遞進去,執行查詢后再通過ResultSetHandler封裝結果並將結果返回給調用者來完成一次查詢,這樣焦點又轉移到了 StatementHandler 對象上。所以通過以上流程發現,MyBatis核心工作實際上是由Executor、StatementHandler、ParameterHandler和ResultSetHandler四個接口完成的,掌握這四個接口的工作原理,對理解MyBatis底層工作原理有很大幫助。
4、StatementHandler對象
StatementHandler是數據庫會話器,顧名思義,數據庫會話器就是專門處理數據庫會話的,相當於JDBC中的Statement(PreparedStatement)。StatementHandler的關系圖如下:
StatementHandler接口設計采用了適配器模式,其實現類RoutingStatementHandler根據上下文來選擇適配器生成相應的StatementHandler。三個適配器分別是SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。
- BaseStatementHandler: 是一個抽象類,它實現了StatementHandler接口,用於簡化StatementHandler接口實現的難度,采用適配器設計模式,它主要有三個實現類SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。
- SimpleStatementHandler: 最簡單的StatementHandler,處理不帶參數運行的SQL,對應JDBC的Statement
- PreparedStatementHandler: 預處理Statement的handler,處理帶參數允許的SQL, 對應JDBC的PreparedStatement(預編譯處理)
- CallableStatementHandler:存儲過程的Statement的handler,處理存儲過程SQL,對應JDBC的CallableStatement(存儲過程處理)
- RoutingStatementHandler:RoutingStatementHandler根據上下文來選擇適配器生成相應的StatementHandler。三個適配器分別是SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。
StatementHandler 接口中的方法如下:
StatementHandler的初始化過程如下(它也是在Configuration對象中完成的):
當調用到doQuery()方法時內部會通過configuration.newStatementHandler()方法來創建StatementHandler對象。
可以發現MyBatis生成StatementHandler代碼中,創建的真實對象是一個RoutingStatementHandler的對象,而不是其它三個對象中的。但是RoutingStatementHandler並不是真實的服務對象,它是通過適配器模式來找到對應的StatementHandler來執行的。在初始化RoutingStatementHandler對象時,它會根據上下文環境來決定創建哪個具體的StatementHandler。RoutingStatementHandler 的構造方法如下:
它內部定義了一個對象的適配器delegate,它是一個StatementHandler接口對象,然后構造方法根據配置來配置對應的StatementHandler對象。它的作用是給3個接口對象的使用提供一個統一且簡單的適配器。此為對象的配適,可以使用對象配適器來盡可能地使用己有的類對外提供服務,可以根據需要對外屏蔽或者提供服務,甚至是加入新的服務。我們以常用的 PreparedStatementHandler 為例,看看Mybatis是怎么執行查詢的。
繼續跟蹤到SimpleExecutor對象中的prepareStatement()方法:
可以發現Executor 執行查詢時會執行 StatementHandler 的 prepare() 和 parameterize() 方法來對SQL進行預編譯和參數的設置, 其中 PreparedStatementHandler 的 prepare 方法如下:
注意:這個 prepare 方法是先調用到 StatementHandler 的實現類 RoutingStatementHandler,再由RoutingStatementHandler 調用 BaseStatementHandler 中的 prepare 方法。
=====================================
通過prepare()方法,可知其中最重要的方法就是 instantiateStatement() 方法了,因為它要完成對SQL的預編譯。在得到 Statement 對象的時候,會去調用 instantiateStatement() 方法,這個方法位於 BaseStatementHandler 中,是一個抽象方法由子類去實現,實際執行的是三種 StatementHandler 中的一種,我們以 PreparedStatementHandler 中的為例:
從上面的代碼我們可以看到,instantiateStatement() 方法最終返回的也是Statement對象,所以經過一系列的調用會把創建好的 Statement 對象返回到 SimpleExecutor 簡單執行器中,為后面設置參數的 parametersize 方法所用。也就是說,prepare 方法負責生成 Statement 實例對象,而 parameterize 方法用於處理 Statement 實例對應的參數。所以我們來看看parameterize 方法:
可以看到這里通過調用了ParameterHandler對象來設置參數,所以下面我們來介紹一下ParameterHandler對象。
5、ParameterHandler對象
ParameterHandler 是參數處理器,它的作用是完成對預編譯的參數的設置,也就是負責為 PreparedStatement 的 SQL 語句參數動態賦值。ParameterHandler 相比於其他的組件就簡單很多了,這個接口很簡單只有兩個方法:
這兩個方法的含義為:
- getParameterObject: 用於獲取參數對象。
- setParameters:用於設置預編譯SQL的參數
ParameterHandler對象的創建,ParameterHandler參數處理器對象是在創建 StatementHandler 對象的同時被創建的,同樣也是由 Configuration 對象負責創建:
可以發現在創建 ParameterHandler 對象時,傳入了三個參數分別是:mappedStatement、parameterObject、boundSql。mappedStatement保存了一個映射器節點<select|update|delete|insert>中的內容,包括我們配置的SQL、SQL的id、緩存信息、resultMap、ParameterType、resultType、resultMap等重要配置內容等。parameterObject表示讀取的參數。boundSql表示要實際執行的SQL語句,它是通過SqlSource 對象來生成的,就是根據傳入的參數對象,動態計算這個 BoundSql, 也就是 Mapper 文件中節點的計算,是由 SqlSource 完成的,SqlSource 最常用的實現類是 DynamicSqlSource。然后就我們進入newParameterHandler()方法中:
上面是在 Configuration 對象創建 ParameterHandler 的過程,它實際上是交由 LanguageDriver 來創建具體的參數處理器,LanguageDriver 默認的實現類是 XMLLanguageDriver,由它調用 DefaultParameterHandler 中的構造方法完成 ParameterHandler 的創建工作:
上面創建完成之后,該進行具體的解析工作,那么 ParameterHandler 如何解析SQL中的參數呢?ParameterHandler 由實現類DefaultParameterHandler執行,使用TypeHandler將參數對象類型轉換成jdbcType,完成預編譯SQL的參數設置,這是在setParameters()方法中完成的,setParameters()方法的實現如下:
至此,我們的參數就處理完成了。一切都准備就緒之后就肯定可以執行了唄!在SimpleExecutor 的 doQuery()方法中最后會調用query()方法來執行SQL語句。並且把創建好的Statement和結果處理器以參數傳入進去,我們進入query()方法:
可以看到這里執行了我們的SQL語句,然后對執行的結果進行處理,這里用到的是MyBatis 四大對象的最后一個神器也就是 ResultSetHandler,所以下面我們繼續來介紹ResultSetHandler對象。
6、ResultSetHandler對象
ResultSetHandler 是結果處理器,它是用來組裝結果集的。ResultSetHandler 接口的定義也挺簡單的,只有三個方法:
ResultSetHandler 對象的創建,ResultSetHandler 對象是在處理查詢請求時創建 StatementHandler 對象同時被創建的,同樣也是由 Configuration 對象負責創建,示例如下:
Configuration對象中的newResultSetHandler()方法:
ResultSetHandler 創建好之后就可以處理結果映射了。還記得在前面Executor的doQuery()方法中,我們最后是通過調用handler.query()方法來完成結果集的處理,如下:
進入query()方法,它是在PreparedStatementHandler實現的:
ResultSetHandler接口只有一個默認的實現類是DefaultResultSetHandler,我們通過SELECT語句執行得到的結果集由其 handleResultSets() 方法處理,方法如下:
上面涉及的主要對象有:
ResultSetWrapper:結果集的包裝器,主要針對結果集進行的一層包裝。這個類中的主要屬性有:
- ResultSet : Java JDBC ResultSet接口表示數據庫查詢的結果。 有關查詢的文本顯示了如何將查詢結果作為java.sql.ResultSet返回。 然后迭代此ResultSet以檢查結果。
- TypeHandlerRegistry: 類型注冊器,TypeHandlerRegistry 在初始化的時候會把所有的 Java類型和類型轉換器進行注冊。
- ColumnNames: 字段的名稱,也就是查詢操作需要返回的字段名稱
- ClassNames: 字段的類型名稱,也就是 ColumnNames 每個字段名稱的類型
- JdbcTypes: JDBC 的類型,也就是java.sql.Types 類型
ResultMap:負責處理更復雜的映射關系
multipleResults:用於封裝處理好的結果集。其中的主要方法是 handleResultSet:
可以看到,handleResultSet() 方法中又分為:嵌套和不嵌套處理這兩種方法,這里我們只管理不嵌套的處理,嵌套的雖然會比不嵌套復雜一點,但總體類似,差別並不大。
最后 handleResultSets() 方法返回的是 collapseSingleResultList(multipleResults) ,我們來看一下:
它是判斷的 multipleResults 的數量,如果數量是 1 ,就直接取位置為0的元素,如果不是1,那就返回 multipleResults 的真實數量。
以上在 DefaultResultSetHandler 中處理完結果映射,並把上述得到的結果返回給調用的客戶端,從而執行完成一條完整的SQL語句。結果集的處理就看到這里了,因為ResultSetHandler的實現非常復雜,它涉及了CGLIB或者JAVASSIST作為延遲加載,然后通過typeHandler和ObjectFactory解析組裝結果在返回,由於實際工作需要改變它的幾率不高加上他比較復雜,所以這里就不在論述了,有興趣的可自行去百度信息。
8、小結
一條SQL語句在Mybatis中的執行過程小結:首先是創建Mapper的動態代理對象MapperProxy,然后將Mappe接口中的方法封裝至MapperMethod對象,通過MapperMethod對象中的execute()方法來執行SQL,其本質是通過SqlSession下的方法來實現的,SQL語句的具體的執行則是通過SqlSession下的四大對象來完成。Executor先調用StatementHandler的prepare()方法預編譯SQL,然后用parameterize()方法啟用ParameterHandler設置參數,完成預編譯,執行查詢,update()也是這樣的。如果是查詢,MyBatis則會使用ResultSetHandler來封裝結果並返回給調用者,從而完成查詢。其執行的完整流程圖如下:
一個超級詳細解析圖 (圖片來自《Mybatis:執行一個Sql命令的完整流程》):
參考鏈接:
- 《Java EE 互聯網輕量級框架整合開發》
- https://www.cnblogs.com/abcboy/p/9656302.html
- https://www.cnblogs.com/cxuanBlog/tag/MyBatis/