SpringBoot原始碼2——SpringBoot x Mybatis 原理解析(如何整合,事務如何交由spring管理,mybatis如何進行資料庫操作)

2022-12-12 06:00:56
閱讀本文需要spring原始碼知識,和springboot相關原始碼知識
對於springboot 整合mybatis,以及mybatis原始碼關係不密切的知識,本文將簡單帶過

系列文章目錄和關於我

一丶從一個問題開始——讀已提交情況下mybatis一級快取造成的問題

上圖中,已知道users1的size為5,那麼users2的大小為多少暱?

我們暫且拋棄mybatis框架中的知識,從mysql事務隔離級別進行分析,test方法第一次查詢到總數,然後重新開啟一個事務插入了一條(require_new的傳播級別),後續addOne方法將立即提交,再次查詢的時候,test方法應該可以立馬查詢到已經提交的資料,應該比第一次輸出的應該多1,這是事務隔離級別指導我們做出的判斷

但是事實上是users1大小和 users2一樣大,這是為什麼暱?

我們看下控制檯

發現mybatis並沒有進行第二次資料庫的查詢,這時候我們應該意識到mybatis具備快取,從而導致第二次查詢並沒有存取資料庫

也就是說 讀已提交的隔離級別下,mybatis如果不關閉快取將存在錯誤(這裡的快取指的一級快取,二級快取普遍是不開的)

具體原理,筆者此文講到mybatis快取後將進行解讀,下面我們從springboot 和 mybatis整合,到mybatis執行原理展開講mybatis的原理

二丶mybatis-springboot-starter的自動裝配

通常springboot整合mybatis只需要引入如下依賴

簡單描述就是SpringBoot啟動的時候會讀取META-INF/spring.factories中自動設定的類,加入到容器中,後續springboot會將這些類當作設定類進行解析

上圖是mybatis-spring-starter的META-INF/spring.factories,其中關鍵的是MybatisAutoConfiguration

1.匯入SqlSessionTemplate,SqlSessionFactory

這裡可以看到當容器中沒有SqlSessionFactory的時候,MybatisAutoConfiguration會為我們注入一個SqlSessionFactorySqlSessionTemplate同樣如此。

這裡我們簡單提一下SqlSessionFactorySqlSessionTemplate的作用

1.1.mybatis中的SqlSessionFactory

故名思意,它是建立SqlSession的工廠

這裡Spring構建SqlSessionFactory,使用了SqlSessionFactoryBean#getObject

它實現了InitializingBean,但是由於沒有被注入到容器中,所以其#afterProperties並不會被spring容器回撥,在此方法中會呼叫buildSqlSessionFactory 進行別名掃描,TypeHandler註冊,xml解析(呼叫XMLMapperBuilder#parse),攔截器註冊,並且指定事務工廠使用SpringManagedTransactionFactory(mybatis,spring事務結合的關鍵,後續詳細解析)等工作

那麼什麼是SqlSession?

1.2.mybatis中的SqlSession

SqlSession是mybatis運算元據庫抽象出來的介面,它可以執行增刪改查,提交事務,回滾事務,建立mapper。我們平時依賴注入的mapper,其實一個動態代理類,其底層其實是呼叫SqlSession進行的資料庫操作

1.3.mybatis-spring中的SqlSessionTemplate

這個類和上面兩個類都不同,它是org.mybatis.spring這個包下面的,一般是mybatis-spring這個依賴會引入的類,它的作用是SqlSession,與Spring事務管理一起工作,以確保實際使用的SqlSession與當前Spring事務關聯。此外,它還管理對談生命週期,包括根據Spring事務設定在必要時關閉、提交或回滾對談。

它是spring事務和mybatis事務結合的關鍵,後面用到了我們再詳細嘮嘮

2.注入AutoConfiguredMapperScannerRegistrar

這裡可以看到如果沒有MapperFactoryBeanMapperScannerConfigurer這兩個bean ,那麼會import一個AutoConfiguredMapperScannerRegistrar,我們簡單說下這三個類的作用,後續用到了詳細解析其原理

2.1MapperFactoryBean

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操作

2.2AutoConfiguredMapperScannerRegistrar

在沒有MapperScannerConfigurer,mybatis自動裝配會為我們注入它,它是一個ImportBeanDefinitionRegistrar

spring解析設定類的時候,若發現一個bean是ImportBeanDefinitionRegistrar的實現,那麼會呼叫其registerBeanDefinitions方法,從而注入其他bean的BeanDefinition,這裡bean便是MapperScannerConfigurer(ImportBeanDefinitionRegistrar注入的MapperScannerConfigurer掃描的時候要求mapper標註@Mapper註解)

2.3 MapperScannerConfigurer

MapperScannerConfigurer還可以使用@MapperScan或者@MapperScans註解,進行引入,若我們使用了@MapperScan或者@MapperScans,上面的AutoConfiguredMapperScannerRegistrar將不會被Import,AutoConfiguredMapperScannerRegistrar的作用便是預設設定一個MapperScannerConfigurer是一個BeanDefinitionRegistryPostProcessor

spring容器在啟動的時候,會回撥它的postProcessBeanDefinitionRegistry在這個方法裡面會掃描所有的mapper介面,指定其class為MapperFactoryBean,從而在後續的範例化中,呼叫MapperFactoryBean#getObject生成mapper介面的動態代理物件

三丶mybatis掃描mapper介面,註冊mapper的Beanfinition

1.MapperScannerConfigurer是如何被註冊到spring容器中的

上文中,我們說到,如果我們沒有使用@MapperScan或者@MapperScans註解標註在設定類上面,那麼會預設新增一個MapperScannerConfigurer,進行mapper介面的掃描註冊工作

通常啟動類都有這樣的@MapperScan

@MapperScan上面存在@Import,會匯入一個MapperScannerRegistrar,這是一個ImportBeanDefinitionRegistrar會在這裡註冊MapperScannerConfigurer的bean定義資訊

其實就是把@MapperScan註解上的設定,繫結到MapperScannerConfigurer的屬性上,

@MapperScan註解,可以指定mapper在的包,mapper介面必須標註的註解,Mapper介面動態代理物件生成使用的MapperFactoryBean

2.MapperScannerConfigurer 如何進行掃描註冊mapper的

其實掃描註冊的工作委託給了ClassPathMapperScanner,呼叫scan方法進行掃描註冊

它是一個ClassPathBeanDefinitionScanner的子類,ClassPathBeanDefinitionScanner就是負責包路徑掃描,註冊BeanDefinition的

這裡的掃描呼叫了ClassPathBeanDefinitionScanner的doScan方法,這個方法會根據包路徑解析成Resouce物件,然後根據路徑下的類包裝成BeanDefinition(ScannedGenericBeanDefinition)

重點看下processBeanDefinitions

這裡最關鍵的是definition.setBeanClass(this.mapperFactoryBeanClass),即將mapper介面的BeanDefinition型別指定為MapperFactoryBean,這樣在spring後續範例化mapper的時候就呼叫MapperFactoryBean#getObject方法進行範例化了

至此我們學習了SpringBoot是如何和mybatis進行結合的,下面總結成一圖

四丶Mapper bean的範例化

當我們一個Service需要注入一個mapper的時候,會從Spring容器中找對應的範例,這時候邊會涉及到這個mapper的範例化,但是我們mapper明明是一個介面呀,如何範例化暱?

雖然我們mapper是一個介面,但是注入到service屬性上的是這個介面的實現類,它是mybatis動態代理後生成的物件。

這個範例化的入口便是AbstractBeanFactory#getBean方法

1.獲取mapper對應的BeanDefinition

這裡獲取的beanDefinition便是源自ClassPathMapperScanner 註冊到容器中的

2.範例化MapperFactoryBean

我們上面說到過,範例化mapper需要呼叫MapperFactoryBean#getObject,那麼首先需要範例化一個MapperFactoryBean

這裡範例化MapperFactoryBean邊是使用的createBean方法,然後Spring會使用反射呼叫構造方法範例化出MapperFactoryBean(Spring還存在使用CGLIB生成子類然後範例化的方式),其中呼叫的是

這個構造方法需要一個入參,表示Mapper介面型別,那麼這個mapperInterface入參來自那麼暱?ClassPathMapperScanner掃描完mapper介面,生成BeanDefinition後,還會在BeanDefinition中記錄全限定型別,這個全限定類名將作為MapperFactoryBean的構造器入參

3.MapperFactory進行屬性注入

上面我們得到一個MapperFactoryBean,但是它構造出一個mapper需要藉助SqlSession,這裡使用的SqlSession其實是SqlSessionTemplate,我們指導MybatisAutoConfiguration會讓容器中注入一個SqlSessionTemplate,那麼spring是如何把這個SqlSessionTemplate設定到mapperFactoryBean的屬性上的暱?

這一步就發生在populateBean方法中,其會呼叫applyPropertyValues,它會根據javaBean的內省,獲取其需要SqlSessionFactory和SqlSessionTemplate,然後從容器中獲取MybatisAutoConfiguration注入的範例,進行反射呼叫Set方法注入

4.MapperFactory的初始化

MapperFactory的父類別SqlSessionDaoSupport繼承自DaoSupport(),其中DaoSupport又實現了InitializingBean,在Spring範例化MapperFactory,完成依賴注入後將回撥InitializingBean#afterPropertiesSet

其中checkDaoConfig方法被MapperFactoryBean重寫

這裡會呼叫configuration.addMapper解析xml和mybaits相關的註解,然後進行註冊和介面進行繫結,但是這一步解析xml操作通常不會真正進行,因為在建立SqlSessionFactory的時候已經進行了

5.呼叫MapperFactory#getObject範例化出一個mapper

範例化出一個Mapper介面的動態代理物件,呼叫的是SqlSessesionTemplate#getMapper

那麼到底mapper方法呼叫的時是如何運算元據庫的暱?這一點我們後面繼續說

至此我們知道了我們service注入的mapper其實是mybatis使用動態代理生成的物件,表面是一個什麼方法實現都沒有的介面,其實是動態代理"負重前行",下圖展示了一個mapper被創造出來的全流程

五丶Mybatis 和spring事務的結合

上面我們知道了xxMapper其實是一個jdk動態代理生成的物件 ,其InvocationHandlerMapperProxy

當mapper被呼叫其介面中宣告的方法的時候,會呼叫到InvocationHandler#invoke這時候MapperProxy就會大顯身手

1.MapperProxy#invoke

MapperProxy內部使用了一個Map快取方法和對應的執行器(MapperMethodInvoker),這個map通常來自MapperProxyFactory的ConcurrentHashMap屬性。而真正方法的呼叫又委託給了MapperMethod#execute,MapperMethod根據方法呼叫的型別(增刪改查)呼叫MapperProxy中的屬性SqlSession(spring環境下的sqlSession實現類是SqlSessionTemplate)`對應的方法

2.SqlSessionTemplate 與mybatis spring事務

SqlSessionTemplate實現了SqlSession介面,但是真正進行資料庫操作的時候,都是委託給屬性SqlSessionProxySqlSessionTemplate存在的意義在於"模板"——複用SqlSession,那麼為什麼需要複用,為何要複用?我們接著看下它的構造方法

可以看到,其內部的sqlSessionProxy是一個動態代理類,我們看下SqlSessionInterceptor,它是一個InvocationHandler

2.1 mybatis 和 spring結合後即使沒有開啟事務也能自動提交的原因

上圖可以看到如果事務並非交給spring管理(呼叫mapper執行單條增刪改查的資料庫操作,會自動提交事務)在反射呼叫sqlsession方法後,會進行事務提交。

//上面無事務註解 下面這條語句會呼叫到sqlsession的動態代理物件,進行自動提交
public void test(){
 
 	xxxMapper.insertOne(xx);
 }

筆者校招的時候,面試官問過這個問題,我尼瑪扯到了mysql的自動提交