啃了一周tkMyBatis源碼,51張圖,5個主要流程,構成了tkmybatis的源碼組成。tkmybatis包括了mybatis的部分,源碼相比于mybatis多了個mApperscan的注解處理,其余部分是一致的。理清了tkmybatis,就理清了mybatis源碼,同時對mybatis的機制能有更深刻的認識。
純mybatis每個持久化操作都要寫sql,會顯得有些繁瑣。現在市面上也有很多的插件,比如mybatis逆向工程,mybatisCodeHelperPro等,可以在xml文件中生成一些常用的sql和對應的mapper接口方法。也有一些mybatis的第三方工具框架,幫我們免去單表操作的sql編寫,比如tkmybatis。tkmybatis源碼版本:
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
一、tkmybatis使用案例
mybatis系列-5分鐘教你提升CRUD開發效率200%
二、整體流程
tkmybatis流程大概分為以下幾步:
- 根據properties文件中配置的xml位置,為每個mapper接口生成一個MappedStatement對象(此時對象的SQLSource為不可執行的Provider)
- 根據ScanMapper接口, 將接口加入到Spring 容器中,創建對應的BeanDefinition,然后修改BeanDefinition中的Bean類型為MapperFactoryBean
- Spring容器實例化MapperFactoryBean實例,在初始化方法中,獲取第一步每個接口生成的MappedStatement對象,并將MappedStatement對象的SQLSource修改為可執行的SqlSource(Provider生成xml格式的SQL,根據該SQL利用languageDriver創建新的sqlSource)
- 將mapper的bean實例注入到需要依賴該實例的service中,此時會Spring容器調用MapperFactoryBean的getObject方法,該方法創建了一個mapper的代理類(使用jdk的動態代理方法),并注入到service里面去
- 執行SQL時,對mapper的SQL操作,都由mapper的代理類mapperProxy來實現,實現過程跟原生mybatis用sqlsession創建的代理類一樣的,只是這里沒有顯示的使用sqlsession來創建代理類,而是放到了MapperFactoryBean的getObject里面,將生成的代理類注入給service
三、源碼解析:
1、Mapper接口掃描:
a、接口掃描入口位置
入口@MapperScan
這個注解會@Import進來一個tk.mapper的掃描器(將MapperScannerRegistrar導入到到Spring容器中,并將其聲明成一個bean,該類的功能是處理注解MapperScan,具體過程見后面)
MapperScannerRegistrar實現了spring的
ImportBeanDefinitionRegistrar接口, 并實現了registerBeanDefinitions方法。【注:如下圖Spring容器在初始化的時候,會先掃描基本注解如controller等,然后掃描第三方jar包中的組件,掃描完成后再找到實現了
ImportBeanDefinitionRegistrar接口的Bean,并將當前的AnnotationMetadata和BeanDefinitionRegistry作為參數傳入,通過該Bean掃描自定義注解的組件進來】
該bean在實例化的時候,會調用registerBeanDefinitions方法來掃描并導入mapper接口,接著來看下掃描過程。
b、創建掃描器并利用MapperScan參數初始化該掃描器
該步驟創建一個掃描器, 利用MapperScan參數初始化該掃描器,各參數意義后面說,默認為空也沒關系。同時還會根據MapperScan參數確定掃描package的范圍。
c、注冊掃描器filter
之所以把這個單獨提出來,是因為這一步決定了scanner能掃描出哪些mapper,符合filter條件的就會被掃描出來。
看下registerFilters的具體實現:
在registerFilters中,掃描器會添加includeFilter和excludeFilter,如果掃描出來的類在某一個excludeFilter中,則放棄該類,如果掃描出來的類在某一個includeFilter中,則保存該類到掃描的返回結果中,excludeFilter要先于includeFilter進行判斷。詳見(
ClassPathScanningCandidateComponentProvider的isCandidateComponent方法)。
下圖中annotationClass和markerInterface都是在前面初始化掃描器的時候,通過MapperScan注解傳入的。
d、調用doScan方法掃描mapper
ClassPathMapperScanner這個掃描器繼承了
ClassPathBeanDefinitionScanner接口,并重寫了doScan方法和isCandidateComponent
doScan:里面會直接復用父類自帶的doScan方法,因為這就是spring掃描包中的bean的方法,在該方法中,會先使用上一步調動registerFilters方法注冊的過濾器判斷掃描出來的類是否符合條件(excludefilter和includefilter判斷),然后再利用重寫后的isCandidateComponent方法進一步判斷是否符合條件(默認接口類是不符合Spring掃描條件的,這里通過重寫該方法,判斷掃描出來的類是不是接口且該類metadata中的encloseclass值為null,是的話就符合掃描條件),兩個判斷都通過,則認為是掃描出來的mapper接口類。
重寫的isCandidateComponent方法
調用父類的doScan后,會掃描到basePackage指定包下面的mapper接口,并封裝成BeanDefinitionHolder的集合。BeanDefinitionHolder包含了BeanDefinition,同時包括BeanDefinition的名稱和別名
e、使用processBeanDefinitions對掃描出來的mapper的BeanDefinition進行一些修改
processBeanDefinitions主要做了一下幾個處理:
上圖處理中,最重要的就兩個,一是更改BeanClass, 二是設置autowired-mode = by type,使得SqlSeesionTemplate可以作為MapperFactoryBean的屬性注入進來(autowired-mode常見的有三種, AUTOWIRE_NO、AUTOWIRE_BY_NAME、AUTOWIRE_BY_TYPE,是基于xml的Spring配置時,用來定義Bean的注入方式的。利用@Component等注解創建的Bean默認值都是AUTOWIRE_NO,表示無需進行屬性的注入,有@autowire等注解時按注解的方式完成屬性注入,AUTOWIRE_BY_NAME和AUTOWIRE_BY_TYPE分別表示按名稱和按類型進行參數的注入。如果該屬性為這兩個值,Spring容器在創建Bean的時候會對Bean的屬性自動完成注入,注入時會掃描set方法,調用set方法并對set方法的參數注入,從而實現屬性的注入。注意,這里因為是掃描出來的Mapper類的Bean對象且沒有@Component等注解,所以就通過修改autowired-mode的方式,通知Spring容器,對該Bean屬性進行注入)。
好了,掃描Mapper的工作到此為止,接下來就是Mapper接口的實例化了。
2、Mapper 接口Bean的實例化:
上面講到Mapper接口的BeanDefinition的BeanClass被改成了tk.mybatis中的MapperFactoryBean。那么實例化的工作主要會由這個類(實例化出另外一個Bean來替代原始的Mapper接口的Bean)來完成。下面是這個類的依賴關系圖。
在圖中,MapperFactoryBean集成的DaoSupport類實現了InitializingBean接口,那么spring在完成屬性注入后,會調DaoSupport的afterPropertiesSet方法。在該afterPropertiesSet中調用了checkDaoConfig方法,由于MapperFactoryBean重寫了checkDaoConfig方法,所以在Bean屬性注入完成后,會調用MapperFactoryBean的checkDaoConfig方法。
a、關鍵屬性注入
下面來看看MapperFactoryBean的checkDaoConfig方法都做了些什么。但是看之前,我們先簡單了解下該Bean中注入的主要屬性,這些屬性在checkDaoConfig方法中用到了,如果不講清楚的話,會很疑惑這些屬性是從哪里來的,了解了才能對tk-mybatis與Spring的集成更加的清楚。
mapperInterface的注入:
SqlSession的注入
那么被注入的SqlSessionFactory和SqlSessionTemplate是如何被創建的呢,看下圖,玄機就在
mybatis-spring-boot-starter的依賴包中的mybatis-spring-boot-autoconfigure:
查看該jar包
mapper-spring-boot-autoconfigure-2.1.5.jar,可以發現在META-INF下有個spring.factories文件。
該配置利用Spring的組件掃描機制,配置了Spring加載jar包后需要掃描的jar包內的配置類。該機制可以參考《004-JAVA基礎-02-Spring類SPI機制,如何將jar包中的類加載到BeanFactory中進行管理》。文件內容如下:
我們看下這個類,這個類被Spring加載進來后會做很多事情:
創建SqlSessionFactory的Bean:
創建SqlSessionTemplate的Bean:
到這里,MapperFactoryBean的關鍵屬性的注入講完了。
b、SqlSessionFactory初始化操作內容
前面講到MapperFactoryBean自動注入了SqlSessionFactory的Bean,為了講清楚MapperFactoryBean在屬性注入后執行的checkDaoConfig操作,我們先了解下SqlSessionFactory初始化的時候都做了什么事情。tkmybatis中SqlSessionFactory的初始化基本就是調用mybatis的初始化過程。簡要概括為以下幾點:
- 讀取Properties配置文件,提取mybatis相關配置
- 解析xml文件,將xml文件mapper節點中的mapper類解析并存放到mapperRegistr中,作為knowsmapper
- 根據mapper的接口方法,為每一個方法創建一個對應的mappedStatement(敲黑板,checkDaoConfig操作主要就是為了調整它),創建mappedStatement時會解析mapper中的method方法,創建對應的SqlSource(根據注解類型判斷,如果是insert等基本注解,則利用LanguageDriver和注解上的SQL語句來創建SqlSource,如果是Provider等注解,tkMybatis就是這一類,則創建的SqlSource為ProviderSqlSource)
下面從源碼里面看下這幾部分:
factory.getObject()方法跟下去,發現接著會用加載配置的xml文件,生成Resource列表,然后遍歷處理列表中的Resource
xmlMapperBuilder.parse()方法跟下去,發現是調用了Mybatis中Configuration的addMapper方法(實現中又是直接調用的MapperRegistry的addMapper方法),處理每一個mapper類,將其添加到mapper注冊中心。
再進一步,看addMapper方法是如何處理mapper類,注意這里knownMappers.put方法很重要,knowMappers中保存了根據xml配置獲取到的所有的mapper類,這里在put的時候,將mapper類轉換成了對應的代理類工廠MapperProxyFactory(MapperFactoryBean在完成初始化后,如果在其他類中需要注入MapperFactoryBean的實例,會由Spring容器調用它的getObject方法創建并注入進去。在getObject方法里會調用代理工廠類的實例方法,會利用jdk的動態代理技術為mapper類實例創建對應的代理類實例,返回給MapperProxyFactory的getObject方法,隨后MapperProxyFactory將該代理類實例作為mapper類的實例(比如UserMapper的實例userMapper,實際就是Mapper的代理類實例),執行sql的時候,實際SQL調用都是在MapperProxy的invoke方法中進行處理的)
在parse()方法中,做如下操作:
parseStatement方法完成MappedStatement創建,包括根據方法注解創建SqlSource,然后利用SqlSource創建MappedStatement。
getSqlSource方法會根據mapper方法上的注解類型,決定返回何種SqlSource。
到這里mapper解析的操作就完成了,MapperFactoryBean中的configuration保存了每一個mapper方法的mappedStatement以及已知的mapper類。
c、為每個方法對應的MappedStatement更新SqlSource
我們再回到MapperFactoryBean類,來看看它的checkDaoConfig方法都做了些什么。先把結論說了吧,checkDaoConfig會根據mapper接口將每個接口方法對應的sqlSource,由前面說的ProviderSqlSource轉換成languageDriver創建的sqlSource(可以理解為tkMybatis根據每個方法的定義,利用Provider為每個方法在對應的mapper.xml中添加了方法對應的SQL語句,然后利用修改后的xml創建新的sqlSource,更新到MappedStatement里面去,只有更新后的MappedStatement,才能執行Mybatis的SQL處理。當然實際上是沒有直接修改xml文件這一步的,只是打個比方,可以看后面的代碼實現)。
再繼續看processConfiguration方法做了什么操作。
再繼續看processMappedStatement對MappedStatement的處理,該處理中會對MappedStatement中的SqlSource進行替換。
看一下替換前,MappedStatement中的SqlSource如下圖所示,是個Provider實現類。
執行setSqlSource后,在下圖中可以看見,MappedStatement中的sqlSource參數已經變成了DynamicSqlSource。
接下來,我們繼續看下setSqlSource到底是怎么改變sqlSource的。
繼續看mapperTemplate的setSqlSource方法,該方法完成了xml形式sql的創建和對應sqlSource的創建。
跟進method.invoke方法,可以發現,該方法實際是在mapper接口方法對應的Provider實現類中執行的,如下圖selectByExample方法由ExampleProvider來生成xmlsql。
至此,tkMybatis就在checkDaoConfig方法中將接口方法對應的MappedStatement中的sqlSource由不能執行的Provider實現類,轉換成了Mybatis中的可執行的sqlSource(根據xmlSql生成,和mybatis掃描mapper.xml文件中創建的MapperStatement和SqlSource具有同樣的功效!)
到目前為止,經過1、Mapper接口掃描和2、Mapper 接口Bean的實例化就完成了tkMybatis的初始化工作。
3、將Mapper實例注入到依賴該實例的bean中
前面tkMybatis做了兩件事情,一是掃描Mapper接口比如UserMapper,創建對應的Bean定義,并將其中的類型修改為MapperFactoryBean;二是實例化該Bean,因為類型已經修改為MapperFactoryBean,就完成MapperFactoryBean的實例化和初始化。
接下來還有關鍵的一步,怎么用這個Bean,我們在寫service的時候,會通過@autowired注解將Mapper注入到service中,比如下面的:
以UsersMapper舉個例子,Spring在注入UsersMapper時,會調用對應MapperFactoryBean實例的getObject方法,如下圖,在該方法中,會創建UsersMapper的的代理類,進入getmapper這個方法,會跟到前面第2部分knowMappers部分,getmapper是從knowMappers獲取的Mapper,該Mapper是一個Mapper接口的代理類MapperProxy,對UserMapper的方法調用都會有MapperProxy的invoke方法來實現。
這也就解決了“明明是Mapper的接口比如UserMapper,但是在bean定義中把它的類型改成了MapperFactoryBean,MapperFactoryBean又沒有實現Mapper的接口方法,那么service中注入的mapper調用方法的時候是如何調用的問題”。
4、實際SQL語句的執行
接著上一部分,調試下實際執行SQL語句的過程。
其實,像下圖一樣,使用經過注入后的Mapper代理類,就跟原生Mybatis創建的代理類是一樣的,執行的SQL操作的過程也是一樣的。里面的每個接口方法也都有對應的MappedStatement實現。如果對invoke代理的過程感興趣,可以調試跟進去看,看看如何invoke中如何用MappedStatement做具體的SQL處理,這里就不具體說了。
總結:
和mybatis的關系:
- 在不影響mybatis原有功能的情況下,很好的拓展了mybatis的功能
- 掃描xml的工作依舊由mybatis來完成,再次掃描并注冊mapper接口的功能以拓展的方式由tk.mapper復寫,掃描后的結果依舊存放在mybatis的Configuration中,和mybatis自己掃描mapper接口的代碼邏輯幾乎一致,唯一添加的功能就是,對mybatis的Configuration中的由自己拓展的方法對應的MapperStatement的sqlSource進行更改,以此來提供具體可執行sql
- 后續執行持久化方法,依然是mybatis的代碼功能,tk.mapper僅在掃描mapper接口階段提供了SqlSource