《深入淺出MyBatis技術原理與實戰》——6. MyBatis的解析和運行原理


MyBatis的運行分為兩大部分,第一部分是讀取配置文件緩存到Configuration對象,用以創建SqlSessionFactory,第二部分是SqlSession的執行過程。

6.1 涉及的技術難點簡介

Mapper是一個接口,而接口是沒有辦法去執行的,那么它是怎么運行的呢?答案是動態代理,MyBaits會為Mapper產生代理類,為此先來學習下動態代理。一般而言,動態代理分為兩種,一種是JDK反射機制提供的代理,另一種是CGLIB代理。

6.1.1 反射技術

6.1.2 JDK動態代理

由java.lang.reflect.*包提供支持的,我們需要完成這么幾個步驟。

  • 編寫服務類和接口,這個是真正的服務提供者,在JDK代理中接口是必須的
  • 編寫代理類,提供綁定和方法。

JDK最大的缺點是需要提供接口,而MyBatis的Mapper就是一個接口,他采用的就是JDK動態代理。我們首先提供一個服務接口,如:

然后寫一個實現類:

現在讓我們寫一個代理類,提供真實對象的綁定和代理方法。代理類的要求是實現InvocationHandler接口的代理方法,當一個對象被綁定后,執行其方法的時候就會進入到代理方法里,如:

下面這段代碼:

讓JDK產生一個代理對象。這個代理對象有3個參數,第一個參數target.getClass().getClassLoader()是類加載器,第二個參數target.getClass().getInterfaces()是接口(代理對象掛在哪個接口下),第三個參數this代表當前HelloServiceProxy類。

一旦綁定后,在進入代理對象方法調用的時候就會到HelloServiceProxy的代理方法上,代理方法有三個參數:第一個proxy是代理對象,第二個是當前調用的那個方法,第三個是方法的參數。比方說,現在HelloServiceImpl對象用bind方法綁定后,返回其占位,我們再調用proxy.sayHello("張三"),那么它就會進入到HelloServiceProxy的invoke()方法。而invoke參數中第一個便是代理對象proxy,方法便是sayHello(),參數是張三。

我們已經用HelloServiceProxy類的屬性target保存了真實的服務對象,那么我們可以通過反射技術調度真實對象的方法:

現在讓我們測試一下動態代理:

運行結果:

6.1.3 CGLIB動態處理

JDK提供的動態代理存在一個缺陷,就是必須要提供接口才可以使用,為了克服這個缺陷,我們可以使用開源框架——CGLIB。讓我們看看如何使用CGLIB動態代理。

HelloService.java和HelloServiceImpl.java都不需要改變,但是我們要實現CGLIB的代理類:

這樣便能夠實現CGLIB的動態代理。在MyBatis中通常在延遲加載的時候才會用到CGLIB的動態代理。

 

6.2 構建SqlSessionFactory過程

第一步,通過org.apache.ibaits.builder.xml.XMLConfigBuilder解析配置的XML文件,讀出配置參數,並將讀取的數據存入這個org.apache.ibatis.session.Configuration類中。注意,MyBatis幾乎所有的配置都是存在這里的。

第二步,使用Configuration對象去創建SqlSessionFactory實現類,我們一般都會使用org.apache.ibatis.session.defaults.DefaultSqlSessionFactory這個默認的實現類。

這種創建的方式就是一種Builder模式。對於復雜的對象而言,直接使用構造方法構建室友困難的,這會導致大量的邏輯放在構造方法中。這個時候使用一個參數總領全局,例如,Configuration類,然后分布構建。

6.2.1 構建Configuration

6.2.2 映射器的內部組成

一般而言,一個映射器是由3個部分組成:

這里只列舉了主要的屬性和方法。MappedStatement對象涉及的東西較多,一般不去修改它。SqlSource是一個接口,主要作用是根據參數和其他的規則組裝SQL,一般也不需要修改。對於參數和SQL而言,主要的規則都反映在BoundSql類對象上,在插件中往往需要拿到它進而可以拿到當前運行的SQL和參數以及參數規則,做出適當的修改,來滿足我們特殊的需求。

BoundSql會提供3個主要的屬性:parameterMappings、parameterObject和sql。

6.2.3 構建SqlSessionFactory

 

6.3 SqlSession運行過程

6.3.1 映射器的動態代理

Mapper映射是通過動態代理來實現的,我們首先來看看代碼清單:

這里可以看到動態代理對接口的綁定,作用就是生成動態代理對象,而代理的方法則被放到了MapperProxy中。MapperProxy的源碼:

上面運用了invoke方法。一旦mapper是一個代理對象,那么它就會運行到invoke方法里面,invoke首先判斷他是否是一個類,顯然這里Mapper是一個接口不是類,所以判定失敗。那么就會生成MapperMethod對象,它是通過cachedMapperMethod方法對其初始化的,人后執行execute方法,把sqlSession和當前運行的參數傳遞進去。讓我們看看這個execute方法的源碼:

MapperMethod采用命令模式運行,根據上下文跳轉到許多方法中。看以看到里面的executeForMany方法,實際上它最后就是通過sqlSession對象去運行對象的SQL。現在便可以明白MyBatis為什么只用Mapper接口便能夠運行SQL,因為映射器的XML文件的命名空間對應的便是這個接口的全路徑,那么根據全路徑和方法名便能夠綁定起來,通過動態代理技術,讓這個接口跑起來。然后采用命令模式,最后還是使用SqlSession接口的方法使得它能夠執行查詢,有了這層封裝我們便可以使用接口編程。

6.3.2 SqlSession下的四大對象

我們已經知道了映射器其實就是一個動態代理對象,進入到了MapperMethod的execute方法。它經過簡單的判斷就進入了SqlSession的刪除,更新,插入,選擇等方法,那么這些方法如何執行呢?

通過類名和方法名字就可以匹配到我們配置的SQL,我們不需要去關心這些細節,我們關心的是設計框架。Mapper的執行過程是通過Executor、StatementHandler、ParameterHandler和ResultHandler來完成數據庫操作和結果返回的。

6.3.2.1 執行器

執行器起到了至關重要的作用。它是一個真正地執行java和數據庫交互的東西。在MyBatis中存在三種執行器。我們可以在MyBatis的配置文件中進行選擇:

讓我們看看MyBatis如何闖將Executor:

在創建對象后,他會執行下面這樣一行代碼:

這就是MyBatis的插件,這里它將我們構建一層層的動態代理對象。在調度真實的Executor方法之前執行配置插件的代碼可以修改。現在我們可以看看執行器方法內部,以SIMPLE執行器SimpleExecutor的查詢方法作為例子盡心講解,如:

顯然MyBatis根據Configuration來構建StatementHandler,然后使用prepareStatement方法,對SQL編譯並對參數進行初始化。調用了StatementHandler的prepare()進行了預編譯和基礎設置,然后通過StatementHandler的parameterize()來設置參數並執行,resultHandler再組裝查詢結果返回給調用者來完成一次查詢。這樣我們的焦點又轉移到了StatementHandler上。

6.3.2.2 數據庫會話器

StatementHandler是用來專門處理數據庫會話的,讓我門先來看看MyBatis是如何創建StatementHandler的,再看Configuration.java生成會話器的地方,如:

很顯然創建的真實對象是一個RoutingStatementHandler對象,它實現了接口StatementHandler。和Executor一樣,用代理對象做一層層的封裝。

RoutingStatement不是我們真實的服務對象,它是通過適配器模式找到對應的StatementHandler來執行的。在MyBatis中,StatementHandler和Executor一樣分為三種:SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler。它所對應的是之前提到過的三種執行器。

在初始化RoutingStatementHandler對象的時候它會根據上下文環境決定創建哪個StatementHandler對象,下面是RoutingStatementHandler的源碼:

數據庫會話器定義了一個對象的適配器delegate。它是一個StatementHandler接口對象,構造方法根據配置來適配對應的StatementHandler對象。它的作用是給實現類對象的使用提供一個統一,簡易的使用適配器。此為對象的適配器模式,可以讓我們使用現有的類和方法對外提供服務,也可以根據實際的需求對外屏蔽一些方法,甚至是加入新的服務。現在已最常用的PreparedStatementHandler為例,看看MyBatis是怎么執行查詢的。看它三個主要的方法,prepare,parameterize和query

然后會調用parameterize()方法去設置參數:

這個時候它是調用ParameterHandler去完成的,這里先我們先來看看StatementHandler的查詢方法:

在Executor里,會先調用StatementHandler的prepare()方法預編譯SQL語句,同時設置一些基本運行的參數。然后用parameterize()方法啟用ParameterizeHandler設置參數,完成預編譯,跟着就是執行查詢,而update()也是這樣的,最后如果需要查詢,我們就用ResultHandler封裝結果返回給調用者。

下面再討論另外兩個對象的使用,ParameterHandler和ResultSetHandler

6.3.2.3 參數處理器

在6.3.2.2節中看到了MyBatis是通過參數處理器(ParameterHandler)對預編譯語句進行參數設置的。它的作用是完成對預編譯參數的設置。它的定義如下:

其中,getParameterObject()方法的作用是返回參數對象,setParameter()方法的作用是設置預編譯SQL語句的參數。

MyBatis為ParameterHandler提供了一個實現類DefaultParameterHandler,我們來看看setParameters的實現:

從parameterObject對象中取參數,然后使用typeHandler進行參數處理。而typeHandler也是在MyBatis初始化的時候,注冊在Configuration里面的,我們需要的時候可以直接拿來用。這樣就完成了參數的設置。

6.3.2.4 結果處理器

組裝結果集返回。下面是結果處理器(ResultSetHandler)的接口定義:

其中,handleOutputParameter()方法是處理存儲過程輸出參數的,重點看一下handleResultSets()方法,它是包裝結果集的。MyBatis同樣為我們提供了一個DefaultResultSetHandler類,默認情況下都是通過這個類處理的。這個實現有些復雜,涉及到JAVASSIST或者CGLIB作為延遲加載,然后通過typeHandler和ObjectFactory進行組裝結果再返回。

6.3.3 SqlSession總結

SqlSession是通過Executor創建StatementHandler來運行的,而StatementHandler要經過下面三步:

  • prepared預編譯SQL
  • parameterize設置參數
  • query/update執行SQL

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM