1. 文檔介紹
1.1. 為什么要寫這個文檔
接觸Spring和MyBatis也挺久的了,但是一直還停留在使用的層面上,導致很多時候光知道怎么用,而不知道其具體原理,這樣就很難做一些針對性的優化工作,Spring和MyBatis都已經是很龐大的框架了,分析起來會需要很多的時間,所以我先從兩者之間的中間件MyBatis-Spring開始,一步一步開始學習兩個框架的原理和精髓
1.2. MyBatis-Spring是什么
當我們在使用MyBatis時,一般是編寫一個Mapper接口和一個Mapper.xml文件,我們都知道接口是不能直接被實例化的,然而我們一般在service層中編寫的注入屬性都是Mapper接口,那么Spring是如何對該接口進行實例化的呢?
一般而言,如果我們使用Spring和MyBatis作為我們的開發框架時,在搭建開發環境的時候,都會做一個Spring與MyBatis的整合,使用到的就是MyBatis-Spring這個中間件,MyBatis-Spring中間件幫我們把mapper接口和mapper.xml文件對應的代理類注冊到Spring中,因此,我們在service層中就能根據類型注入,將對應mapper接口的代理類注入到service層中,我們才能夠調用到對應的方法
1.3 整體原理
在Spring開發中,我們通常是在service層中通過依賴注入Dao層的實例,在MyBatis中,Mapper接口即對應着Dao實例,MyBatis-Spring中間件就是把MyBatis中的mapper.xml和mapper.java對應的Mapper接口注冊到Spring容器中,使得service層可以直接通過以來注入獲取到Mapper接口
1.3.1 注冊
在Spring中所有的Mapper接口都會被注冊為MapperFactoryBean,所有的MapperFactoryBean會共享一個SqlSessionFactory,該SqlSessionFactory由SqlSessionFactoryBean創建,而在sqlSessionFactory的configuration屬性中存的是一個Configuration對象,configuratiao對象中的mapperRegistry屬性中存儲了一個MapperRegistry對象,MapperRegistry對象中的knownMappers屬性是一個key為mapper.java文件對應接口的類型,value為MapperProxyFactory的對象。
1.3.2 獲取
當從Spring中獲取Mapper接口時,將會調用對應的MapperFactoryBean的getObjects方法,該方法返回值即為對應的MapperProxyFactory創建的MapperProxy動態代理
2 掃描需要注冊到Spring中的Mapper接口
在Spring中注冊MapperFactoryBean的流程
2.1 整體流程圖
2.2 配置MapperScannerConfigurer
還是來看一下在文檔最開頭Spring整合MyBatis時的配置
在這里,我只配置了basePackage和sqlSessionFactoryBeanName兩個屬性,還有一些其他配置,這里我們就不一一解析了,只分析注冊MapperFactoryBean最主要的流程。
2.3 掃描basePackage下的所有候選對象
先來看看MapperScannerConfigurer類的定義
首先實現的是postProcessBeanDefinitionRegistry接口,實現了這個接口的類會再Spring初始化Bean定義的時候被調用postProcessBeanDefinitionRegistry方法,用來自定義注冊Bean的定義邏輯,先來看看這個方法做了些什么事情
在一開始先是調用了本類中的processPropertyPlaceHolders方法,用來設置自己本身的Bean屬性,接下來定義ClassPathMapperScanner即Mapper掃描器來掃描對應的Mapper文件,該掃描器繼承了Spring中的ClassPathBeanDefinitionScanner,該掃描器是掃描需要實例化的Bean並把它們加載到BeanDefinitionHolder集合中,以便后續的初始化Bean,這里我們直接來看其中的掃描邏輯
這里先調用了父類的doScan方法,我們需要先看看父類的doScan方法做了什么事情,
核心方法即findCandidateCompents,即從指定的包中找到需要初始化的候選人(即需要初始化的Bean)的定義並把他們放進Set<BeandDefinitionHolder>中,
這兩個方法都是在獲取候選者,上面的是當設置了ResourceLoader時的調用,其實兩個方法在本質上是一樣的,我們直接來看下面這個方法
可以看到,在尋找候選人的過程中,直接獲取對應basePackage下的所有資源,然后會對資源進行判斷,是否滿足候選人的條件
2.4 候選人的條件
上節中提到了對候選人進行篩選的方法,idCandilateComponent這個方法實際上是由ClassPathMapperScanner覆蓋的,因此,當調用這個方法時,將會執行ClassPathMapperScanner#isCandidateComponent方法
即對應掃描的應該為basePackage下獨立的接口(即非內部接口),至此,找到了需要實例化的接口,
2.5 將所有候選對象定義為Spring中的Bean
再獲取完候選對象之后,即父類的doScan掃描完畢之后,Spring會將所有滿足條件的對象存儲到beanDefinition,即Spring中的bean定義對象,這也是Spring初始化Bean的基礎,
2.6 設置候選對象的Bean屬性
在Spring中獲取到了這些Bean的定義之后,MyBatis-Spring中間件還需要對這些Bean做一些屬性上的設置,讓其能滿足使用的條件,我們接下來看看都有哪些屬性的配置
繼續來看ClassPathMapperScanner#doScan方法,
ClassPathMapperScanner#processBeanDefinitions就是在Spring處理完Bean定義后由MyBatis-Spring來處理的邏輯
2.6.1 設置bean定義的Class類型
上圖中最重要的邏輯即先設置構造函數參數為原本讀取的BeanDefinition中的類名(即Mapper接口的名稱),把所有的Mapper接口定義BeanClass類型設置為MapperFactoryBean,並設置其構造器參數為對應的Mapper接口類型
上圖為MapperFactoryBean的構造函數
2.6.2 設置SqlSessionFactoryBean
在processBeanDefinitions()方法中還有一段比較重要的邏輯
在這里,是需要配置MapperFactoryBean的父類SqlSessionDaoSupport中的sqlSessionTemplate屬性,當配置的時sqlSessionFactory屬性時,MapperFactoryBean的在初始化時,會使用sqlSessionFactory屬性來構建sqlSessionTemplate
從2.6節可知,設置到MapperFactoryBean中sqlSessionFactory或者是sqlSessionTemplate都是從spring中獲取的,這就意味着,
當設置的是sqlSessionFactory時,每個MapperFactoryBean中的sqlSessionTemplate是不同的,但是最終的sqlSessionFactory是相同的,
當設置的是sqlSessionTemplate時,每個MapperFactoryBean中sqlSessionTemplate就是同一個
3. SqlSessionFactory初始化
構建SqlSessionFactory並存儲MapperProxyFactory流程
3.1 整體流程圖
3.2 配置SqlSessionFactoryBean
首先看段Spring整合MyBatis時的經典配置
在Spring使用MyBatis-Spring中間件來整合MyBatis必須配置這兩個Bean,首先我們從sqlSessionFactoryBean開始,SqlSessionFactoryBean,見名思意,實際上是一個factoryBean,即用來生產對象的工廠Bean,SqlSessionFactoryBean是用來創建上文中提到的SqlSessionFactory的
3.3 構建SqlSessionFactoryBean
先來看看SqlSessionFactoryBean類的定義
實現了InitializingBean的類會在Spring初始化完該Bean后的時候會執行afterPropertiesSet方法,我們來看看這個方法里做了寫什么
先做了一些數據校驗,即必須配置dataSource,sqlSessionFactoryBuilder會在創建對象的時候初始化,不用關心,而對於configuration或configLocation而言,兩者不能同時配置,只能配置其中一個,或者一個都不配置,這樣在構建sqlSessionFactory時就會采用默認配置,然后開始構建sqlSessionFactory實例
3.4 配置了configuration或configLocations的處理
在這個方法中,首先會判斷是否SqlSessionFactoryBean的Spring配置里針對是否配置了configuration屬性或configLocation屬性進行針對處理,沒有的話就新建一個新的configuration即采用默認配置,接下來,我們先對配置了configLocation的情況進行一次分析,先來看下MyBatis-configuration.xml的內容,先來看一段經典配置
在這個配置里面,我們配置了一個Mapper接口,其接口類型為UserMapper,
再回到構建sqlSessionFactory的過程中
在該方法中,使用XMLconfigBuilder初始化了對改配置文件的讀取,然后對該配置進行了解析,我們先進去看看解析的過程
這里我們跳過對其他配置的解析,直接來看對<mappers>節點的解析,對<mappers>節點下的所有<mapper>節點進行循環遍歷,並調用了configuration#addMapper方法
3.5 配置了mapperLocations的情況處理
我們再來看下配置了mapperLocations的情況
當我們配置了mapperLocations后,在buildSqlSessionFactory中也會對這一屬性進行處理
這里循環遍歷mapperLocations的配置,並使用XMLMapperBuilder來進行讀取並解析,我們直接來看對應解析過程
獲取到的<mapper>節點就是mapperLocation下所有的mapper.xml文件下的mapper節點,再對這些文件進行初始化過程,這里我們也只看注冊對應Mapper接口的邏輯
跟之前一樣,這里也是使用了configuration.addMapper方法,
3.6 configuration.addMapper
上文中提到了無論是配置了configuration或configLocation還是配置了mapperLocations,都會調用Configuration#addMapper方法,下面我們就來看下這個方法,下圖為Configuration#addMapper方法
這里委托給mapperRegistry來添加對應的代理,再進去MapperRegistry#addMapper方法,這個方法的工作就是在初始化mapper接口對應的動態代理類了
先校驗此Mapper的類型是否是接口類型,因為只有MyBatis只處理接口,再校驗是否已存在此mapper,可以看到這里初始化了一個MapperProxyFactory並放到已知的Mapper類型Map中,這個MapperProxyFactory即Mapper動態代理的工廠,使用工廠模式來創建Mapper動態代理
4 MapperFactoryBean初始化
4.1 整體流程圖
4.2 afterPropertiesSet
在設置完MapperFactoryBean的beanDefinition信息之后,Spring在初始化Bean時就會初始化這個Bean,再來看看這個Bean的定義
MapperFactoryBean繼承了SqlSessionDaoSupport,而SqlSessionDaoSupport又繼承了DaoSupport類,DaoSupport類又實現了InitializingBean接口,意味着當MapperFactoryBean初始化完畢后會調用DaoSupport的afterPropertiesSet方法
來看DaoSupport類中的afterPropertiesSet方法
這里調用了checkDaoConfig方法,最終會調用到MapperFactoryBean中的checkDaoConfig方法
4.3 checkDaoConfig
接下來來看MapperFactoryBean的checkDaoConfig方法
可以看到,在MapperFactoryBean屬性設置完畢之后,會調用這個checkDaoConfig來檢查mapperRegistry中是否存在對應的MapperProxyFactory,如果沒有,將會把對應的MapperProxyFactory添加進去,
4.4 解決的問題
不知道看過上文的讀者發現沒有,如果在SqlSessionFactoryBean中未配置configuration,configLocation和mapperLocations時,在MyBatis-Spring初始化的第一步完成后(即章節3.1中所提及的內容),SqlSessionFactory中的mapperRegistry中是沒有與章節3.2中所注冊的Mapper接口的對應關系的,那么,本節中的內容,就是確保mapperRegistry中一定存在兩者之間的對應關系,這也是從Spring中獲取到Mapper接口的基礎
5 從Spring中獲取Mapper接口
5.1 整體流程圖
5.2 從Spring中獲取Bean的流程介紹
在上一節中,我們掃描到的Mapper接口在Spring中都設置成了MapperFactoryBean,那這么設置有什么用呢?
這里就需要對從Spring中獲取Bean有一些了解了,這里先簡單介紹一下從Spring中獲取Bean的流程
5.2.1 從Spring中獲取Bean的流程圖
5.2.2 從Spring中獲取Bean具體邏輯
從Spring獲取Bean時,都是從BeanFactory即Bean工廠中構建並獲取,我們先來看下BeanFactory的源碼,當獲取對應的Bean時,會調用對應的BeanFactory#getBean方法
里面有幾個getBean方法的重載
以及判斷Singleton或Prototype的方法,
我們直接看它的默認實現AbstractBeanFactory#getBean方法
這里我們直接以單例模式舉例了,該方法主要是判斷需要的Bean是Singleton還是prototype的,然后調用對應的方法,我們這里直接看單例模式下的獲取Bean
這里我們可以看到,對於Bean的類型有一個區別判斷,如果是普通Bean的話會直接返回實例,而對於FactoryBean而言,會執行另外一段邏輯
其實不用管其他的一些判斷邏輯,里面的核心邏輯就一個,調用對應的FactoryBean的getObject方法,
5.3 MapperFactoryBean#getObject()
那對於我們的MapperFactoryBean而言會怎樣呢?
很明顯,MapperFactoryBean實際上是一個FactoryBean,那我們再進去看看它獲取Bean的邏輯
對於MapperFactoryBean即為調用其getObjeact方法,而在MapperFactoryBean對該方法有一個重寫
在這個方法中,getSqlSession為其父類的方法,
在sqlSessionDaoSupport中
這個sqlSessionTemplate即為在doScan中設置Beandifinition時設置的,
然后再調用SqlSessionTemplate中的getMapper方法
再進去SqlSessionTemplate的getConfiguration方法,實際上獲取的是sqlSessionFactory的configuration,
5.4 Configuration#getMapper
然后再調用Configuration#getMapper方法
可以看到,實際上還是轉交給mapperRegistry中的getMapper方法
5.5 MapperRegistry#getMapper
這里我們可以看到,最終是從章節2,章節3中存儲的knownMappers中獲取到對應Mapper接口的MapperProxyFactory,並最終調用到MapperProxyFactory#newInstance方法獲取到對應的MapperProxy
這里就是使用newInstance來構建一個新的MapperProxy,即Mapper接口的動態代理類,這里使用的是java自帶的動態代理,這里的泛型T就是對應的Mapper接口的類型
6 總結及驗證
6.1 總結
從上面的分析中可以看出,由SqlSessionFactoryBean來掃描mapper接口並配置對應的MapperProxyFactory到mapperRegistry中的knownMappers Map中,由MapperScannerConfigurer來掃描對應的Mapper接口並生成對應的MapperFactoryBean,從Spring中獲取mapper接口動態代理類時會調用MapperFactoryBean的getObjects方法,並最終調用到mapperRegistry中的getMapper方法調用mapperProxyFactory的newInstance生成對應的MapperProxy即Mapper接口的動態代理,
可以看到,這里就調用了MapperProxyFactory的newInstance方法獲取到對應Mapper接口的動態代理類了,至此,我們基本完成了將Mapper接口注冊到Spring的過程
6.2 驗證
接下來我們來看一個簡單的驗證
Spring配置如下
對應的markerInterface如下,如果設置了該屬性,只有繼承了該接口的Mapper接口會被注冊
FooMapper接口如下
測試類如下
其對應的運行結果為
即我們獲取到的fooMapper實際上是一個動態代理,其在Spring中的類型為FooMapper接口類型,創建它的工廠Bean為MapperFactoryBean類型,與我們分析源代碼的結果是一致的
至此,我們初步分析了MyBatis-Spring中間件的原理,對於Mapper接口是如何注冊到Spring中的原理有了一個不錯的了解
7 寫在最后
任何問題請聯系hei12138@outlook.com