閱讀本文需要spring原始碼知識,和springboot相關原始碼知識
對於springboot 整合mybatis,以及mybatis原始碼關係不密切的知識,本文將簡單帶過
上圖中,已知道users1的size為5,那麼users2的大小為多少暱?
我們暫且拋棄mybatis框架中的知識,從mysql事務隔離級別進行分析,test方法第一次查詢到總數,然後重新開啟一個事務插入了一條(require_new的傳播級別),後續addOne方法將立即提交,再次查詢的時候,test方法應該可以立馬查詢到已經提交的資料,應該比第一次輸出的應該多1,這是事務隔離級別指導我們做出的判斷
但是事實上是users1大小和 users2一樣大,這是為什麼暱?
我們看下控制檯
發現mybatis並沒有進行第二次資料庫的查詢,這時候我們應該意識到mybatis
具備快取,從而導致第二次查詢並沒有存取資料庫
也就是說 讀已提交的隔離級別下,mybatis如果不關閉快取將存在錯誤
(這裡的快取指的一級快取,二級快取普遍是不開的)
具體原理,筆者此文講到mybatis快取後將進行解讀,下面我們從springboot 和 mybatis整合,到mybatis執行原理展開講mybatis的原理
通常springboot整合mybatis只需要引入如下依賴
簡單描述就是SpringBoot啟動的時候會讀取META-INF/spring.factories中自動設定的類,加入到容器中,後續springboot會將這些類當作設定類進行解析
上圖是mybatis-spring-starter的META-INF/spring.factories
,其中關鍵的是MybatisAutoConfiguration
這裡可以看到當容器中沒有SqlSessionFactory
的時候,MybatisAutoConfiguration
會為我們注入一個SqlSessionFactory
,SqlSessionTemplate
同樣如此。
這裡我們簡單提一下SqlSessionFactory
和SqlSessionTemplate
的作用
SqlSessionFactory
故名思意,它是建立SqlSession
的工廠
這裡Spring構建SqlSessionFactory,使用了SqlSessionFactoryBean#getObject
它實現了InitializingBean
,但是由於沒有被注入到容器中,所以其#afterProperties
並不會被spring容器回撥,在此方法中會呼叫buildSqlSessionFactory
進行別名掃描,TypeHandler
註冊,xml解析(呼叫XMLMapperBuilder#parse
),攔截器註冊,並且指定事務工廠使用SpringManagedTransactionFactory(mybatis,spring事務結合的關鍵,後續詳細解析)
等工作
那麼什麼是SqlSession
?
SqlSession
SqlSession
是mybatis運算元據庫抽象出來的介面,它可以執行增刪改查,提交事務,回滾事務,建立mapper。我們平時依賴注入的mapper,其實一個動態代理類,其底層其實是呼叫SqlSession
進行的資料庫操作
SqlSessionTemplate
這個類和上面兩個類都不同,它是org.mybatis.spring
這個包下面的,一般是mybatis-spring
這個依賴會引入的類,它的作用是SqlSession,與Spring事務管理一起工作,以確保實際使用的SqlSession與當前Spring事務關聯。此外,它還管理對談生命週期,包括根據Spring事務設定在必要時關閉、提交或回滾對談。
它是spring事務和mybatis事務結合的關鍵,後面用到了我們再詳細嘮嘮
AutoConfiguredMapperScannerRegistrar
這裡可以看到如果沒有MapperFactoryBean
和MapperScannerConfigurer
這兩個bean ,那麼會import一個AutoConfiguredMapperScannerRegistrar
,我們簡單說下這三個類的作用,後續用到了詳細解析其原理
MapperFactoryBean
MapperFactoryBean 是一個FactoryBean,FactoryBean
中有一個方法叫getObject
負責建立一個物件交給spring容器管理,通常我們定義的Controller,Service都具備實現類,而非一個介面,spring可以範例化一個service的實現,但是mybatis中的mapper往往是一個介面,spring不知道如何範例化這個mapper,這時候發現mapper的BeanDefinition
中標記了這個class是MapperFactoryBean
就會呼叫MapperFactoryBean#getObject
範例化一個mapper,這個mapper便是我們注入到service中使用的mapper,它是源mapper的動態代理實現類,從而在代理類中呼叫Sqlsesession
執行對應的sql操作
AutoConfiguredMapperScannerRegistrar
在沒有MapperScannerConfigurer
,mybatis自動裝配會為我們注入它,它是一個ImportBeanDefinitionRegistrar
spring解析設定類的時候,若發現一個bean是ImportBeanDefinitionRegistrar
的實現,那麼會呼叫其registerBeanDefinitions
方法,從而注入其他bean的BeanDefinition
,這裡bean便是MapperScannerConfigurer
(ImportBeanDefinitionRegistrar注入的MapperScannerConfigurer
掃描的時候要求mapper標註@Mapper註解)
MapperScannerConfigurer
MapperScannerConfigurer
還可以使用@MapperScan
或者@MapperScans
註解,進行引入,若我們使用了@MapperScan
或者@MapperScans
,上面的AutoConfiguredMapperScannerRegistrar
將不會被Import,AutoConfiguredMapperScannerRegistrar
的作用便是預設設定一個MapperScannerConfigurer
是一個BeanDefinitionRegistryPostProcessor
spring容器在啟動的時候,會回撥它的postProcessBeanDefinitionRegistry
在這個方法裡面會掃描所有的mapper介面,指定其class為MapperFactoryBean
,從而在後續的範例化中,呼叫MapperFactoryBean#getObject
生成mapper介面的動態代理物件
上文中,我們說到,如果我們沒有使用@MapperScan
或者@MapperScans
註解標註在設定類上面,那麼會預設新增一個MapperScannerConfigurer
,進行mapper介面的掃描註冊工作
通常啟動類都有這樣的@MapperScan
@MapperScan上面存在@Import,會匯入一個MapperScannerRegistrar
,這是一個ImportBeanDefinitionRegistrar
會在這裡註冊MapperScannerConfigurer
的bean定義資訊
其實就是把@MapperScan註解上的設定,繫結到MapperScannerConfigurer
的屬性上,
@MapperScan註解,可以指定mapper在的包,mapper介面必須標註的註解,Mapper介面動態代理物件生成使用的MapperFactoryBean
等
其實掃描註冊的工作委託給了ClassPathMapperScanner
,呼叫scan
方法進行掃描註冊
它是一個ClassPathBeanDefinitionScanner
的子類,ClassPathBeanDefinitionScanner就是負責包路徑掃描,註冊BeanDefinition的
這裡的掃描呼叫了ClassPathBeanDefinitionScanner
的doScan方法,這個方法會根據包路徑解析成Resouce
物件,然後根據路徑下的類包裝成BeanDefinition(ScannedGenericBeanDefinition)
重點看下processBeanDefinitions
這裡最關鍵的是definition.setBeanClass(this.mapperFactoryBeanClass)
,即將mapper介面的BeanDefinition型別指定為MapperFactoryBean
,這樣在spring後續範例化mapper的時候就呼叫MapperFactoryBean#getObject
方法進行範例化了
至此我們學習了SpringBoot是如何和mybatis進行結合的,下面總結成一圖
當我們一個Service需要注入一個mapper的時候,會從Spring容器中找對應的範例,這時候邊會涉及到這個mapper的範例化,但是我們mapper明明是一個介面呀,如何範例化暱?
雖然我們mapper是一個介面,但是注入到service屬性上的是這個介面的實現類,它是mybatis動態代理後生成的物件。
這個範例化的入口便是AbstractBeanFactory#getBean
方法
這裡獲取的beanDefinition便是源自ClassPathMapperScanner
註冊到容器中的
我們上面說到過,範例化mapper需要呼叫MapperFactoryBean#getObject
,那麼首先需要範例化一個MapperFactoryBean
這裡範例化MapperFactoryBean邊是使用的createBean
方法,然後Spring會使用反射呼叫構造方法範例化出MapperFactoryBean(Spring還存在使用CGLIB生成子類然後範例化的方式),其中呼叫的是
這個構造方法需要一個入參,表示Mapper介面型別,那麼這個mapperInterface入參來自那麼暱?ClassPathMapperScanner掃描完mapper介面,生成BeanDefinition後,還會在BeanDefinition中記錄全限定型別,這個全限定類名將作為MapperFactoryBean的構造器入參
上面我們得到一個MapperFactoryBean,但是它構造出一個mapper需要藉助SqlSession,這裡使用的SqlSession其實是SqlSessionTemplate
,我們指導MybatisAutoConfiguration會讓容器中注入一個SqlSessionTemplate
,那麼spring是如何把這個SqlSessionTemplate
設定到mapperFactoryBean的屬性上的暱?
這一步就發生在populateBean
方法中,其會呼叫applyPropertyValues
,它會根據javaBean的內省,獲取其需要SqlSessionFactory和SqlSessionTemplate,然後從容器中獲取MybatisAutoConfiguration
注入的範例,進行反射呼叫Set方法注入
MapperFactory的父類別SqlSessionDaoSupport
繼承自DaoSupport()
,其中DaoSupport
又實現了InitializingBean
,在Spring範例化MapperFactory,完成依賴注入後將回撥InitializingBean#afterPropertiesSet
其中checkDaoConfig
方法被MapperFactoryBean重寫
這裡會呼叫configuration.addMapper
解析xml和mybaits相關的註解,然後進行註冊和介面進行繫結,但是這一步解析xml操作通常不會真正進行,因為在建立SqlSessionFactory的時候已經進行了
範例化出一個Mapper介面的動態代理物件,呼叫的是SqlSessesionTemplate#getMapper
那麼到底mapper方法呼叫的時是如何運算元據庫的暱?這一點我們後面繼續說
至此我們知道了我們service注入的mapper其實是mybatis使用動態代理生成的物件,表面是一個什麼方法實現都沒有的介面,其實是動態代理"負重前行",下圖展示了一個mapper被創造出來的全流程
上面我們知道了xxMapper其實是一個jdk動態代理生成的物件 ,其InvocationHandler
是MapperProxy
當mapper被呼叫其介面中宣告的方法的時候,會呼叫到InvocationHandler#invoke
這時候MapperProxy就會大顯身手
MapperProxy內部使用了一個Map快取方法和對應的執行器(MapperMethodInvoker
),這個map通常來自MapperProxyFactory的ConcurrentHashMap屬性。而真正方法的呼叫又委託給了MapperMethod#execute
,MapperMethod根據方法呼叫的型別(增刪改查)呼叫MapperProxy中的屬性SqlSession(spring環境下的sqlSession實現類是SqlSessionTemplate)`對應的方法
SqlSessionTemplate實現了SqlSession
介面,但是真正進行資料庫操作的時候,都是委託給屬性SqlSessionProxy
,SqlSessionTemplate
存在的意義在於"模板"
——複用SqlSession,那麼為什麼需要複用,為何要複用?我們接著看下它的構造方法
可以看到,其內部的sqlSessionProxy
是一個動態代理類,我們看下SqlSessionInterceptor
,它是一個InvocationHandler
上圖可以看到如果事務並非交給spring管理(呼叫mapper執行單條增刪改查的資料庫操作,會自動提交事務)在反射呼叫sqlsession方法後,會進行事務提交。
//上面無事務註解 下面這條語句會呼叫到sqlsession的動態代理物件,進行自動提交
public void test(){
xxxMapper.insertOne(xx);
}
筆者校招的時候,面試官問過這個問題,我尼瑪扯到了mysql的自動提交