Mybatis是java開發者非常熟悉的ORM框架,Spring整合Mybatis更是我們的日常開發姿勢。
本篇主要講Mybatis與Spring整合所做的事情,讓讀過本文的開發者對Mybatis和Spring的整合過程,有清晰的理解。
注:若文中有錯誤或其他疑問,歡迎留下評論。
以mybatis-spring-2.0.2 為例,工程劃分六個模組。
1、annotation 模組
定義了@MapperScan和@MapperScans,用於掃描mapper介面。以及mapper掃描註冊器(MapperScannerRegistrar),掃描註冊器實現了 ImportBeanDefinitionRegistrar介面, 在Spring容器啟動時會執行所有實現了這個介面的實現類,
註冊器內部會註冊一系列MyBatis相關Bean。
2、batch 模組
批處理相關,基於優秀的批處理框架Spring batch 封裝了三個批處理相關類:
- MyBatisBatchItemWriter(批量寫)
- MyBatisCursorItemReader(遊標讀)
- MyBatisPagingItemReader(分頁讀)
在使用Mybatis時,方便的應用Spring batch,詳見 Spring-batch使用。
3、config模組
解析、處理讀取到的配置資訊。
4、mapper模組
這裡是處理mapper的地方:ClassPathMapperScanner(根據路徑掃描Mapper介面)與MapperScannerConfigurer 配合,完成批量掃描mapper介面並註冊為MapperFactoryBean。
5、support 模組
支援包,SqlSessionDaoSupport 是一個抽象的支援類,用來為你提供 SqlSession呼叫getSqlSession()方法會得到一個SqlSessionTemplate。
6、transaction 模組,以及凌亂類
與Spring整合後,事務管理交由Spring來做。
還有包括異常轉換,以及非常重要的SqlSessionFactoryBean,在外散落著。
下面重點講述幾個核心部分:
一、初始化相關
1)SqlSessionFactoryBean
在基礎的MyBatis中,通過SqlSessionFactoryBuilder建立SqlSessionFactory。整合Spring後由SqlSessionFactoryBean來建立。
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>...
需要注意SqlSessionFactoryBean實現了Spring的FactoryBean介面。這意味著由Spring最終建立不是SqlSessionFactoryBean本身,而是 getObject()的結果。我們來看下getObject()
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
//配置載入完畢後,建立SqlSessionFactory
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
getObject()最終返回了當前類的 SqlSessionFactory,因此,Spring 會在應用啟動時建立 SqlSessionFactory,並以 sqlSessionFactory名稱放進容器。
2) 兩個重要屬性:
1. SqlSessionFactory 有一個唯一的必要屬性:用於 JDBC 的 DataSource不能為空,這點在afterPropertisSet()中體現。
2. configLocation,它用來指定 MyBatis 的 XML 配置檔案路徑。通常只用來配置 <settings>相關。其他均使用Spring方式配置
5 public void afterPropertiesSet() throws Exception { 6 //dataSource不能為空 7 notNull(dataSource, "Property 'dataSource' is required"); 8 //有預設值,初始化 = new SqlSessionFactoryBuilder() 9 notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); 10 //判斷configuration && configLocation有且僅有一個 11 state((configuration == null && configLocation == null) ||
!(configuration != null && configLocation != null), 12 "Property 'configuration' and 'configLocation' can not specified with together"); 13 //呼叫build方法建立sqlSessionFactory 14 this.sqlSessionFactory = buildSqlSessionFactory(); 15 }
buildSqlSessionFactory()方法比較長所以,這裡省略了一部分程式碼,只展示主要過程,看得出在這裡進行了Mybatis相關配置的解析,完成了Mybatis核心配置類Configuration的建立和填充,最終返回SqlSessionFactory。
1 protected SqlSessionFactory buildSqlSessionFactory() throws Exception { 2 3 final Configuration targetConfiguration; 4 5 XMLConfigBuilder xmlConfigBuilder = null; 6 // 如果自定義了 Configuration,就用自定義的 7 if (this.configuration != null) { 8 targetConfiguration = this.configuration; 9 if (targetConfiguration.getVariables() == null) { 10 targetConfiguration.setVariables(this.configurationProperties); 11 } else if (this.configurationProperties != null) { 12 targetConfiguration.getVariables().putAll(this.configurationProperties); 13 } 14 // 如果配置了原生配置檔案路徑,則根據路徑建立Configuration物件 15 } else if (this.configLocation != null) { 16 xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream()
, null, this.configurationProperties); 17 targetConfiguration = xmlConfigBuilder.getConfiguration(); 18 } else {21 // 兜底,使用預設的 22 targetConfiguration = new Configuration(); 23 //如果configurationProperties存在,設定屬性 24 Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables); } 26 //解析別名,指定包 27 if (hasLength(this.typeAliasesPackage)) { 28 scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream() 29 .filter(clazz -> !clazz.isAnonymousClass())
.filter(clazz -> !clazz.isInterface()) 30 .filter(clazz -> !clazz.isMemberClass())
.forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias); 31 } 32 //解析外掛 33 if (!isEmpty(this.plugins)) { 34 Stream.of(this.plugins).forEach(plugin -> { 35 targetConfiguration.addInterceptor(plugin);38 } 39 ... 40 //如果需要解決原生配置檔案,此時開始解析(即配置了configLocation) 41 if (xmlConfigBuilder != null) { 42 try { 43 xmlConfigBuilder.parse(); 44 ... //有可能配置多個,所以遍歷處理(2.0.0支援可重複註解) 52 if (this.mapperLocations != null) { 53 if (this.mapperLocations.length == 0) {
for (Resource mapperLocation : this.mapperLocations) { 57 ... //根據mapper路徑,載入所以mapper介面 62 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), 63 targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments()); 64 xmlMapperBuilder.parse(); 65 //構造SqlSessionFactory 70 return this.sqlSessionFactoryBuilder.build(targetConfiguration); 71 }
二、事務管理
1)事務管理器配置
MyBatis-Spring 允許 MyBatis 參與到 Spring 的事務管理中。 藉助 Spring 的 DataSourceTransactionManager 實現事務管理。
/** 一、XML方式配置 **/
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource" />
</bean>
/** 一、註解方式配置 **/
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
注意:為事務管理器指定的 DataSource 必須和用來建立 SqlSessionFactoryBean 的是同一個資料來源,否則事務管理器就無法工作了。
配置好 Spring 的事務管理器,你就可以在 Spring 中按你平時的方式來配置事務。並且支援 @Transactional 註解(宣告式事務)和 AOP 風格的配置。在事務處理期間,一個單獨的 SqlSession 物件將會被建立和使用。當事務完成時,這個 session 會以合適的方式提交或回滾。無需DAO類中無需任何額外操作,MyBatis-Spring 將透明地管理事務。
2) 程式設計式事務:
推薦TransactionTemplate 方式,簡潔,優雅。可省略對 commit 和 rollback 方法的呼叫。
1 TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); 2 transactionTemplate.execute(txStatus -> { 3 userMapper.insertUser(user); 4 return null; 5 });
注意:這段程式碼使用了一個對映器,換成SqlSession同理。
三、SqlSession
在MyBatis 中,使用 SqlSessionFactory 來建立 SqlSession。通過它執行對映的sql語句,提交或回滾連線,當不再需要它的時候,可以關閉 session。使用 MyBatis-Spring 之後,我們不再需要直接使用 SqlSessionFactory 了,因為我們的bean 可以被注入一個執行緒安全的 SqlSession,它能基於 Spring 的事務配置來自動提交、回滾、關閉 session。
SqlSessionTemplate
SqlSessionTemplate 是SqlSession的實現,是執行緒安全的,因此可以被多個DAO或對映器共享使用。也是 MyBatis-Spring 的核心。
四、對映器
1) 對映器的註冊
1 /**
2 *@MapperScan註解方式
3 */
4 @Configuration
5 @MapperScan("org.mybatis.spring.sample.mapper")
6 public class AppConfig {
8 }
10 /**
11 *@MapperScanS註解 (since 2.0.0新增,java8 支援可重複註解)
12 * 指定多個路徑可選用此種方式
13 */
14 @Configuration
15 @MapperScans({@MapperScan("com.zto.test1"), @MapperScan("com.zto.test2.mapper")})
16 public class AppConfig {
18 }
<!-- MapperScannerConfigurer方式,批量掃描註冊 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.zto.test.*" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
無論使用以上哪種方式註冊對映器,最終mapper介面都將被註冊為MapperFactoryBean。既然是FactoryBean,我們來跟它的getObject()方法看下。
2) MapperFactoryBean原始碼解析
1.查詢MapperFactoryBean.getObject()
1 /**
2 * 通過介面型別,獲取mapper
3 * {@inheritDoc}
4 */
5 @Override
6 public T getObject() throws Exception {
7 //getMapper 是一個抽象方法
8 return getSqlSession().getMapper(this.mapperInterface);
9 }
2.檢視實現類,SqlSessionTemplate.getMapper()
( 為什麼是SqlSessionTemplate,而不是預設的DefaultSqlSession?SqlSessionTemplate是整合包的核心,是執行緒安全的SqlSession實現,是我們@Autowired mapper介面程式設計的基礎 )
4 @Override
5 public <T> T getMapper(Class<T> type) {
6 return getConfiguration().getMapper(type, this);
7 }
3.呼叫Configuration.getMapper()
1 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
2 return mapperRegistry.getMapper(type, sqlSession);
3 }
4.呼叫MapperRegistry.getMapper()
1 @SuppressWarnings("unchecked")
2 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
3 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
4 if (mapperProxyFactory == null) {
5 throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
6 }
7 try {
8 return mapperProxyFactory.newInstance(sqlSession);
9 } catch (Exception e) {
10 throw new BindingException("Error getting mapper instance. Cause: " + e, e);
11 }
12 }
5.呼叫MapperProxyFactory.newInstance()
1 @SuppressWarnings("unchecked")
2 protected T newInstance(MapperProxy<T> mapperProxy) {
3 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
4 }
最終看到動態代理生成了一個新的代理例項返回了,也就是說,我們使用@Autowired 註解進來一個mapper介面,每次使用時都會由代理生成一個新的例項。
為什麼在Mybatis中SqlSession是方法級的,Mapper是方法級的,在整合Spring後卻可以注入到類中使用?
因為在Mybatis-Spring中所有mapper被註冊為FactoryBean,每次呼叫都會執行getObject(),返回新例項。
五、總結
MyBatis整合Spring後,Spring侵入了Mybatis的初始化和mapper繫結,具體就是:
1)Cofiguration的例項化是讀取Spring的配置檔案(註解、配置檔案),而不是mybatis-config.xml
2)mapper物件是方法級別的,Spring通過FactoryBean巧妙地解決了這個問題
3)事務交由Spring管理
注:如對文中內容有疑問,歡迎留下評論共同探討。