Mybatis是java开发者非常熟悉的ORM框架,Spring集成Mybatis更是我们的日常开发姿势。

  本篇主要讲Mybatis与Spring集成所做的事情,让读过本文的开发者对Mybatis和Spring的集成过程,有清晰的理解。

  注:若文中有错误或其他疑问,欢迎留下评论。

  以mybatis-spring-2.0.2
<https://github.com/mybatis/spring/tree/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使用
<http://www.mybatis.org/spring/zh/batch.html>。

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-argref="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方式,批量扫描注册 --> <
beanclass="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管理

    注:如对文中内容有疑问,欢迎留下评论共同探讨。