Mybatis源碼解析(一) —— mybatis與Spring是如何整合的?
從大學開始接觸mybatis到現在差不多快3年了吧,最近尋思着使用3年了,我卻還不清楚其內部實現細節,比如:
-
它是如何加載各種mybatis相關的xml?
-
它是如何僅僅通過一個Mapper接口 + Mappe.xml實現數據庫操作的(盡管很多人可能都清楚是通過代理實現,但面試時一旦深入詢問:比如Mapper的代理類名是什么?是通過JDK還是cglib實現?)?
-
在同一個方法中,Mybatis多次請求數據庫,是否要創建多個SqlSession會話?
-
它與Spring是如何適配(整合)的?
-
在Spring中是如何保障SqlSession的生命周期的?
-
等等一系列的問題。。。
如果以上問題你自認為無法回答,或者說了解一些,那么就從現在開始,我們來一一揭開這層面紗。
一、Mybatis:最簡單測試Demo
相信只要用過Mybatis的同學看到下面的代碼一定不會陌生,如果不清楚的可以看下官網文檔
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 1、目前流行方式
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(101);
}
// 2、以前流行方式
try (SqlSession session = sqlSessionFactory.openSession()) {
User user = sqlSession.selectOne("xxx.UserMapper.selectById", "101");;
}
示列代碼演示了Mybatis進行一次數據庫操作的過程,大致分為(針對目前流行方式,其實以前的使用和目前流行的使用方式實現原理一樣):
-
1、 通過 SqlSessionFactoryBuilder 將讀取到的配置資源 build 生成 SqlSessionFactory
-
2、 通過 SqlSessionFactory 的 openSession() 獲取到 SqlSession
-
3、 通過 SqlSession 獲取到 Mapper的代理對象(MapperProxy)
-
4、 通過 Mapper 進行 數據庫請求操作
SqlSession、SqlSessionFactory、SqlSessionFactoryBuilder
我們可以輕易的發現每次去請求數據庫操作都需要通過 SqlSessionFactory 去獲取到 SqlSession,而 SqlSessionFactory 是通過 SqlSessionFactoryBuilder 構造出來的, 並且最后請求操作完成后都關閉了SqlSession。因此,不難得出:
- SqlSessionFactory 一個應用程序中最好只有1個,即單列。
- SqlSessionFactoryBuilder 只有一個作用: 創建 SqlSessionFactory對象。
- 一個SqlSession應該僅存活於一個業務請求中,也可以說一個SqlSession對應一次數據庫會話,它不是永久存活的,每次訪問數據庫時都需要創建它,並且訪問完成后都必須執行會話關閉
針對這3個類以及mapper的作用域(Scope)和生命周期的描述,個人覺得官方文檔寫得很清楚:
SqlSessionFactoryBuilder
這個類可以被實例化、使用和丟棄,一旦創建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 實例的最佳作用域是方法作用域(也就是局部方法變量)。 你可以重用 SqlSessionFactoryBuilder 來創建多個 SqlSessionFactory 實例,但是最好還是不要讓其一直存在,以保證所有的 XML 解析資源可以被釋放給更重要的事情。
SqlSessionFactory
SqlSessionFactory 一旦被創建就應該在應用的運行期間一直存在,沒有任何理由丟棄它或重新創建另一個實例。 使用 SqlSessionFactory 的最佳實踐是在應用運行期間不要重復創建多次,多次重建 SqlSessionFactory 被視為一種代碼“壞味道(bad smell)”。因此 SqlSessionFactory 的最佳作用域是應用作用域。 有很多方法可以做到,最簡單的就是使用單例模式或者靜態單例模式。
SqlSession
每個線程都應該有它自己的 SqlSession 實例。SqlSession 的實例不是線程安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。 絕對不能將 SqlSession 實例的引用放在一個類的靜態域,甚至一個類的實例變量也不行。 也絕不能將 SqlSession 實例的引用放在任何類型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你現在正在使用一種 Web 框架,要考慮 SqlSession 放在一個和 HTTP 請求對象相似的作用域中。 換句話說,每次收到的 HTTP 請求,就可以打開一個 SqlSession,返回一個響應,就關閉它。 這個關閉操作是很重要的,你應該把這個關閉操作放到 finally 塊中以確保每次都能執行關閉。
依賴注入框架可以創建線程安全的、基於事務的 SqlSession 和映射器,並將它們直接注入到你的 bean 中,因此可以直接忽略它們的生命周期。 如果對如何通過依賴注入框架來使用 MyBatis 感興趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 兩個子項目。
映射器實例(Mapper實例)
映射器是一些由你創建的、綁定你映射的語句的接口。映射器接口的實例是從 SqlSession 中獲得的。因此從技術層面講,任何映射器實例的最大作用域是和請求它們的 SqlSession 相同的。盡管如此,映射器實例的最佳作用域是方法作用域。 也就是說,映射器實例應該在調用它們的方法中被請求,用過之后即可丟棄。 並不需要顯式地關閉映射器實例,盡管在整個請求作用域保持映射器實例也不會有什么問題,但是你很快會發現,像 SqlSession 一樣,在這個作用域上管理太多的資源的話會難於控制。 為了避免這種復雜性,最好把映射器放在方法作用域內。就像示列代碼一樣。
如果SqlSession是注入的,那么映射器實例也可通過依賴注入,並且可忽略其生命周期。
二、Mybatis-Spring:將MyBatis代碼無縫地整合到Spring
前面是學習mybatis常看到的一種代碼,但缺點也很明顯: 每次請求都得創建SqlSession,並且Mapper的代理類是通過SqlSession獲取(說明耦合度很高),也就意味着每次請求都得創建一個新的Mapper代理類。為了整合Spring,並且解決前面問題,所以Mybatis-Spring 子項目來襲。
MyBatis-Spring 會幫助你將 MyBatis 代碼無縫地整合到 Spring 中。它將允許 MyBatis 參與到 Spring 的事務管理之中,創建映射器 mapper 和 SqlSession 並注入到 bean中。
上面是 Mybatis-Spring的官方介紹,其中 允許 MyBatis 參與到 Spring 的事務管理之中,創建映射器 mapper 和 SqlSession 並注入到 bean中 是我們本次解析的關鍵點。那么開始分析吧!
SqlSessionFactoryBean 、 MapperScannerConfigurer
在Spring項目中應用了Mybatis都會有下面的2個bean配置,這2個配置就是實現xml加載、mapper和SqlSession注入的起始配置。
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"></property>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
<!-- DAO接口所在包名,Spring會自動查找其下的類 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="xxx.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
一、 SqlSessionFactoryBean : 加載xml及build SqlSessionFactory對象
從配置中我們可以看的 SqlSessionFactoryBean 配置了數據源、mapper的xml路徑、mybatis-config的xml路徑。因此,不難想象,SqlSessionFactoryBean 內部實現了xml配置文件的加載及SqlSessionFactory對象的創建。我們來看下 SqlSessionFactoryBean繼承關系圖形:
在繼承關系圖中,我們發現了 InitializingBean、FactoryBean 的身影,可能清楚這個的同學,大概已經猜到了肯定有 afterPropertiesSet() 來創建 SqlSessionFactory 對象 和 getObject() 來獲取 SqlSessionFactory 對象 。 話不多說,先看下getObject()實現:
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
getObject()相對簡單,我們都知道FactoryBean子類都是通過getObject()來獲取到實際的Bean對象,這里也就是SqlSessionFactory。從源碼中我們看到當 sqlSessionFactory為null會去調用 afterPropertiesSet(),所以 SqlSessionFactory 肯定是由 afterPropertiesSet() 來實現創建的。繼續看afterPropertiesSet()實現:
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
this.sqlSessionFactory = buildSqlSessionFactory();
}
afterPropertiesSet() 內部首先 驗證了 dataSource 和 sqlSessionFactoryBuilder 部位null,最后調用 buildSqlSessionFactory()方法獲取到 SqlSessionFactory 對象,並賦值到類字段屬性 sqlSessionFactory 。 繼續查看buildSqlSessionFactory()源碼:
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
// 省略了 SqlSessionFactoryBean 的屬性(比如:ObjectFactory )賦值到 Configuration 對象中的操作
// 1 Configuration : Mybatis的核心類之一,主要存放讀取到的xml數據,包括mapper.xml
Configuration configuration;
XMLConfigBuilder xmlConfigBuilder = null;
if (this.configLocation != null) {
// 2 創建 xmlConfigBuilder 對象 : 用於解析 mybatis-config.xml 數據
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
} else {
if (logger.isDebugEnabled()) {
logger.debug("Property 'configLocation' not specified, using default MyBatis Configuration");
}
configuration = new Configuration();
configuration.setVariables(this.configurationProperties);
}
if (xmlConfigBuilder != null) {
try {
// 3 XmlConfigBuilder 解析方法執行
xmlConfigBuilder.parse();
} catch (Exception ex) {
throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
} finally {
ErrorContext.instance().reset();
}
}
if (!isEmpty(this.mapperLocations)) {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
// 4 創建 XMLMapperBuilder 對象 : 用於解析 mapper.xml 數據
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
}
}
// 5 通過 SqlSessionFactoryBuilder bulid SqlSessionFactory 對象
return this.sqlSessionFactoryBuilder.build(configuration);
}
整個 buildSqlSessionFactory() 源碼主要有以下幾個重要的點:
-
1、 XMLConfigBuilder ,通過調用其 parse() 方法來 解析 mybatis-config.xml 配置(如果 配置有 mapper.xml ,其會通過 XMLMapperBuilder 進行解析加載),並將解析的數據賦值到 Configuration(Mybatis的核心類之一,主要存放讀取到的xml數據,包括mapper.xml,該類貫穿整個mybatis,足以見得其重要性)
-
2、 XMLMapperBuilder : 通過調用其 parse() 方法來 解析 mapper.xml 配置, 並將解析的數據賦值到 Configuration
-
3、 將存放有解析數據的 Configuration 作為 sqlSessionFactoryBuilder.build() 參數,創建 sqlSessionFactory 對象。
至此
二、 MapperScannerConfigurer :掃描Mapper接口路徑,將 Mapper 偷梁換柱成 MapperFactoryBean
MapperScannerConfigurer 是 mybatis-spring 項目中為了實現方便加載Mapper接口,以及將 Mapper 偷梁換柱成 MapperFactoryBean。查看 MapperScannerConfigurer 源碼,先看下其繼承關系圖:
從中我們其繼承了 BeanDefinitionRegistryPostProcessor 接口,熟悉Spring 的同學應該 已經大致想到了 其如何將 Mapper 偷梁換柱成 MapperFactoryBean 了。話不多說,我們來看看 MapperScannerConfigurer 是如何實現 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
我們可以發現整個方法內部其實就是通過 ClassPathMapperScanner 的 scan() 方法,查看 scan() 實現,發現其內部調用了關鍵方法 doScan(),那么我們來看下 doScan() 方法實現:
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 1、調用父類 ClassPathBeanDefinitionScanner的 doScan方法 加載路徑下所有的mapper接口生成對應的 BeanDefinition
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
for (BeanDefinitionHolder holder : beanDefinitions) {
GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
// 2、 設置 被代理的 Bean(也就是Mapper) 的class信息
definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
// 3、 偷梁換柱成 MapperFactoryBean
definition.setBeanClass(MapperFactoryBean.class);
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
// 4、 設置 sqlSessionFactory
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
// 5、 設置 sqlSessionTemplate
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
return beanDefinitions;
}
整個方法分為3個部分:
-
1、 調用父類 ClassPathBeanDefinitionScanner的 doScan()方法 加載路徑下所有的mapper接口生成對應的 BeanDefinition
-
2、 通過definition.setBeanClass(MapperFactoryBean.class) 偷梁換柱成 MapperFactoryBean
-
3、 通過 definition.getPropertyValues().add() 添加 MapperFactoryBean 所需的 字段或者方法參數信息 : sqlSessionFactory 、 mapperInterface等
至此 MapperScannerConfigurer 的使命已經完成, 至於 MapperFactoryBean 的創建就完全交給Spring來完成了。
三、 MapperFactoryBean 、SqlSessionTemplate:Mapper與SqlSession解耦的利器
我們知道在mybatis中,Mapper是通過 SqlSession創建的,而SqlSession的生命周期僅僅在一次會話中,那么按照這種設計,每一次會話都要去創建SqlSession,然后再通過SqlSession去創建Mapper。我們知道Mapper其實沒有必要每次都去創建,它更加適合作為一個單列對象。那么怎么將SqlSession和Mapper解耦呢? 在mybatis-spring項目中通過 MapperFactoryBean 、SqlSessionTemplate 來實現的。接下來我們就來解析它們。
MapperFactoryBean
正如前面我們所看到的一樣,MapperFactoryBean 其實可以理解為 Mapper的代理工廠Bean,我們可以通過 MapperFactoryBean 的方法獲取到 Mapper的代理對象。先來看下 MapperFactoryBean繼承關系 :
我們可以看到 MapperFactoryBean 實現了 FactoryBean, 那么 肯定通過 實現 getObject() 獲取到 Mapper的代理對象,查看源碼如下:
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
其內部就是我們熟悉的 getSqlSession().getMapper() 創建Mapper代理對象的方法。熟悉Spring 的同學都知道 在Bean加載的過程中如果發現當前Bean對象是 FactoryBean 會去 調用getObject() 獲取真正的Bean對象。不熟悉的同學可以去看下 AbstractBeanFactory 的 getBean() 方法。
但是似乎還是沒有吧SqlSession和Mapper解耦的跡象呢?不着急,我們繼續看下 getSqlSession(), 發現其是 父類 SqlSessionDaoSupport 實現,我們看下SqlSessionDaoSupport源碼:
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSession sqlSession;
private boolean externalSqlSession;
// 創建 SqlSession子類 SqlSessionTemplate
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (!this.externalSqlSession) {
this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
}
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSession = sqlSessionTemplate;
this.externalSqlSession = true;
}
public SqlSession getSqlSession() {
return this.sqlSession;
}
....
}
我們發現我們獲取到的SqlSession其實是其子類SqlSessionTemplate, 我們查看其構造方法源碼:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 維護了一個 SqlSession的代理對象
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
我們可以清楚的發現,其內部維護了一個 SqlSession的字段 sqlSessionProxy ,其賦值的是代理對象 SqlSessionInterceptor。 我們再來看下 SqlSessionInterceptor 的源碼:
private class SqlSessionInterceptor implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 通過getSqlSession() 獲取一個 SqlSession
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
我們發現其代理實現時,通過getSqlSession() 獲取一個 全新的SqlSession。也就是說創建Mapper的SqlSession和會話請求的SqlSession不是同一個。這里就完美的解耦了Mapper和SqlSession,並且保障了每次會話SqlSession的生命周期范圍。
這里超前提下: getSqlSession().getMapper() 其實 是通過 configuration.getMapper() 來獲取的,那么就意味着 configuration內部必須添加了Mapper信息,那么configuration是何時添加的呢? 可以看下 MapperFactoryBean的checkDaoConfig()方法,源碼如下:
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Throwable t) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", t);
throw new IllegalArgumentException(t);
} finally {
ErrorContext.instance().reset();
}
}
}
由於父類實現了 InitializingBean 接口,並且其afterPropertiesSet() 調用了 checkDaoConfig() 方法 ,所以,至少在初始化創建MapperFactoryBean 時,就已經向 configuration內部必須添加了Mapper信息。
三、個人總結
本文解析了Mybatis與Spring是如何整合的,其中的關鍵對象包括:
-
SqlSessionFactoryBuilder: 用於創建 SqlSessionFactory
-
SqlSessionFactory: 用於創建 SqlSession
-
SqlSession: Mybatis工作的最頂層API會話接口,所有訪問數據庫的操作都是通過SqlSession來的
-
Configuration: 存放有所有的mybatis配置信息,包括mapper.xml、 mybatis-config.xml等
-
XMLConfigBuilder: 解析 mybatis-config.xml 配置並存放到Configuration中
-
XMLMapperBuilder: 解析 mapper.xml 配置並存放到Configuration中
-
SqlSessionFactoryBean: mybatis整合Spring時的 生成 SqlSessionFactory 的FactoryBean
-
MapperScannerConfigurer: mybatis整合Spring時的 實現方便加載Mapper接口,以及將 Mapper 偷梁換柱成 MapperFactoryBean
-
MapperFactoryBean: 生成 Mapper 代理對象的FactoryBean
-
SqlSessionTemplate: 內部維護有 SqlSession 的代理對象,解耦Mapper和SqlSession的關鍵對象。
如果您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!
本文由博客一文多發平台 OpenWrite 發布!