单独使用mybatis是有很多限制的(比如无法实现跨越多个session的事务),而且很多业务系统本来就是使用spring来管理的事务,因此mybatis最好与spring集成起来使用。
1. Mybatis-Spring使用
MyBatis-Spring:Mybatis 在 Spring 中的使用
2. Spring集成源码分析
2.1 SqlSessionFactoryBean
在Mybatis的所有操作都是基于一个SqlSession的,而SqlSession是由SqlSessionFactory来产生的,SqlSessionFactory又是由SqlSessionFactoryBuilder来生成的。
但是Mybatis-Spring是基于SqlSessionFactoryBean的。在使用Mybatis-Spring的时候,我们也需要SqlSession,而且这个SqlSession是内嵌在程序中的,一般不需要我们直接访问。它的SqlSession也是由SqlSessionFactory来产生的,但是Mybatis-Spring给我们封装了一个SqlSessionFactoryBean,在这个bean里面还是通过SqlSessionFactoryBuilder来建立对应的SqlSessionFactory,进而获取到对应的SqlSession。
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {@Overridepublic SqlSessionFactory getObject() throws Exception {if (this.sqlSessionFactory == null) {afterPropertiesSet();}return this.sqlSessionFactory;}@Overridepublic void afterPropertiesSet() throws Exception {notNull(dataSource, "Property 'dataSource' is required");notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),"Property 'configuration' and 'configLocation' can not specified with together");//这里最后会调用 SqlSessionFactoryBuilder.build(targetConfiguration);this.sqlSessionFactory = buildSqlSessionFactory();}}
XML中的配置
通过SqlSessionFactoryBean我们可以通过对其指定一些属性来提供Mybatis的一些配置信息。在Spring的applicationContext配置文件中定义一个SqlSessionFactoryBean。
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><!-- dataSource属性是必须指定的,它表示用于连接数据库的数据源 --><property name="dataSource" ref="dataSource" /><property name="mapperLocations"value="classpath:com/tiantian/ckeditor/mybatis/mappers/*Mapper.xml" /><property name="typeAliasesPackage" value="com.tiantian.ckeditor.model" /></bean>
- mapperLocations:它表示我们的Mapper文件存放的位置,当我们的Mapper文件跟对应的Mapper接口处于同一位置的时候可以不用指定该属性的值。
- configLocation:用于指定Mybatis的配置文件位置。如果指定了该属性,那么会以该配置文件的内容作为配置信息构建对应的SqlSessionFactoryBuilder,但是后续属性指定的内容会覆盖该配置文件里面指定的对应内容。
- typeAliasesPackage:它一般对应我们的实体类所在的包,这个时候会自动取对应包中不包括包名的简单类名作为包括包名的别名。多个package之间可以用逗号或者分号等来进行分隔。
- typeAliases:数组类型,用来指定别名的。指定了这个属性后,Mybatis会把这个类型的短名称作为这个类型的别名,前提是该类上没有标注@Alias注解,否则将使用该注解对应的值作为此种类型的别名。
<property name="typeAliases"><array><value>com.tiantian.mybatis.model.Blog</value><value>com.tiantian.mybatis.model.Comment</value></array></property>
- plugins:数组类型,用来指定Mybatis的Interceptor。
- typeHandlersPackage:用来指定TypeHandler所在的包,如果指定了该属性,SqlSessionFactoryBean会自动把该包下面的类注册为对应的TypeHandler。多个package之间可以用逗号或者分号等来进行分隔。
- typeHandlers:数组类型,表示TypeHandler。
2.2 MapperFactoryBean
通过MapperFactoryBean可以获取到我们想要的Mapper对象。MapperFactoryBean实现了Spring的FactoryBean接口,所以MapperFactoryBean是通过FactoryBean接口中定义的getObject方法来获取对应的Mapper对象的。
在定义一个MapperFactoryBean的时候有两个属性需要我们注入,一个是Mybatis-Spring用来生成实现了SqlSession接口的SqlSessionTemplate对象的sqlSessionFactory方法;另一个就是我们所要返回的对应的Mapper接口了。
定义好相应Mapper接口对应的MapperFactoryBean之后,我们就可以把我们对应的Mapper接口注入到由Spring管理的bean对象中了,比如Service bean对象。这样当我们需要使用到相应的Mapper接口时,MapperFactoryBean会从它的getObject方法中获取对应的Mapper接口,而getObject内部还是通过我们注入的属性调用SqlSession接口的getMapper(Mapper接口)方法来返回对应的Mapper接口的。这样就通过把SqlSessionFactory和相应的Mapper接口交给Spring管理实现了Mybatis跟Spring的整合。
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {//通过这里的getObject()和getObjectType()最后取出的是T对应的Mapper接口的代理类@Overridepublic T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);}@Overridepublic Class<T> getObjectType() {return this.mapperInterface;}}
关于FactotyBean的用法见下文:
Spring官网 FactoryBean用法说明
MapperFactoryBean的出现为了代替手工使用SqlSessionDaoSupport或SqlSessionTemplate编写数据访问对象(DAO)的代码,使用动态代理实现
XML中的配置
比如下面这个官方文档中的配置
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"><property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" /><property name="sqlSessionFactory" ref="sqlSessionFactory" /></bean>
UserMapper是一个接口,我们创建一个MapperFactoryBean实例,然后注入这个接口和sqlSessionFactory(mybatis中提供的SqlSessionFactory接口,MapperFactoryBean会使用SqlSessionFactory创建SqlSession)这两个属性。
之后想使用这个UserMapper接口的话,直接通过spring注入这个Mapper类型的bean,然后就可以直接使用了,spring内部会创建一个这个接口的动态代理。这里如果要获取MapperFactoryBean本身是ApplicationContext中的getBean(“$userMapper”)
当发现要使用多个MapperFactoryBean的时候,一个一个定义肯定非常麻烦,于是mybatis-spring提供了MapperScannerConfigurer这个类,它将会查找类路径下的映射器并自动将它们创建成MapperFactoryBean。
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"><property name="basePackage" value="org.mybatis.spring.sample.mapper" /><property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate" /></bean>
这段配置会扫描org.mybatis.spring.sample.mapper下的所有接口,然后创建各自接口的动态代理类。
2.3 MapperScannerConfigurer
如果我们需要使用MapperScannerConfigurer来帮我们自动扫描和注册Mapper接口的话我们需要在Spring的applicationContext配置文件中定义一个MapperScannerConfigurer对应的bean。对于MapperScannerConfigurer而言有一个属性是我们必须指定的,那就是basePackage。basePackage是用来指定Mapper接口文件所在的基包的,在这个基包或其所有子包下面的Mapper接口都将被搜索到。多个基包之间可以使用逗号或者分号进行分隔。最简单的MapperScannerConfigurer定义就是只指定一个basePackage属性,如:
XML中的配置
package org.format.dynamicproxy.mybatis.dao;public interface UserDao {public User getById(int id);public int add(User user);public int update(User user);public int delete(User user);public List<User> getAll();}<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><!--dataSource属性指定要用到的连接池--><property name="dataSource" ref="dataSource"/><!--configLocation属性指定mybatis的核心配置文件--><property name="configLocation" value="classpath:sqlMapConfig.xml"/><property name="mapperLocations" value="classpath:sqlMapper/*Mapper.xml" /></bean><bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"><property name="basePackage" value="org.format.dynamicproxy.mybatis.dao"/><property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/></bean>
有时候我们指定的基包下面的并不全是我们定义的Mapper接口,为此MapperScannerConfigurer还为我们提供了另外两个可以缩小搜索和注册范围的属性。一个是annotationClass,另一个是markerInterface。
- annotationClass:当指定了annotationClass的时候,MapperScannerConfigurer将只注册使用了annotationClass注解标记的接口。
- markerInterface:markerInterface是用于指定一个接口的,当指定了markerInterface之后,MapperScannerConfigurer将只注册继承自markerInterface的接口。
如果上述两个属性都指定了的话,那么MapperScannerConfigurer将取它们的并集,而不是交集。即使用了annotationClass进行标记或者继承自markerInterface的接口都将被注册为一个MapperFactoryBean。
- sqlSessionFactory:
这个属性已经废弃。当我们使用了多个数据源的时候我们就需要通过sqlSessionFactory来指定在注册MapperFactoryBean的时候需要使用的SqlSessionFactory,因为在没有指定sqlSessionFactory的时候,会以Autowired的方式自动注入一个。换言之当我们只使用一个数据源的时候,即只定义了一个SqlSessionFactory的时候我们就可以不给MapperScannerConfigurer指定SqlSessionFactory。 - sqlSessionFactoryBeanName:它的功能跟sqlSessionFactory是一样的,只是它指定的是定义好的SqlSessionFactory对应的bean名称。
- sqlSessionTemplate:
这个属性已经废弃。它的功能也是相当于sqlSessionFactory的,因为就像前面说的那样,MapperFactoryBean最终还是使用的SqlSession的getMapper方法取的对应的Mapper对象。当定义有多个SqlSessionTemplate的时候才需要指定它。对于一个MapperFactoryBean来说SqlSessionFactory和SqlSessionTemplate只需要其中一个就可以了,当两者都指定了的时候,SqlSessionFactory会被忽略。 - sqlSessionTemplateBeanName:指定需要使用的sqlSessionTemplate对应的bean名称。
注意:由于使用sqlSessionFactory和sqlSessionTemplate属性时会使一些内容在PropertyPlaceholderConfigurer之前加载,导致在配置文件中使用到的外部属性信息无法被及时替换而出错,因此官方现在新的Mybatis-Spring中已经把sqlSessionFactory和sqlSessionTemplate属性废弃了,推荐大家使用sqlSessionFactoryBeanName属性和sqlSessionTemplateBeanName属性。
我们先通过测试用例debug查看userDao的实现类到底是什么:

我们可以看到,userDao是1个MapperProxy类的实例。看下MapperProxy的源码,没错,实现了InvocationHandler,说明使用了jdk自带的动态代理:
public class MapperProxy<T> implements InvocationHandler, Serializable {private static final long serialVersionUID = -6424540398559729838L;private final SqlSession sqlSession;private final Class<T> mapperInterface;private final Map<Method, MapperMethod> methodCache;public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {this.sqlSession = sqlSession;this.mapperInterface = mapperInterface;this.methodCache = methodCache;}public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (Object.class.equals(method.getDeclaringClass())) {try {return method.invoke(this, args);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}}final MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);}private MapperMethod cachedMapperMethod(Method method) {MapperMethod mapperMethod = methodCache.get(method);if (mapperMethod == null) {mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());methodCache.put(method, mapperMethod);}return mapperMethod;}}
MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,BeanDefinitionRegistryPostProcessor接口是一个可以修改spring工厂中已定义的bean的接口,该接口有个postProcessBeanDefinitionRegistry方法。

然后我们看下ClassPathMapperScanner中的关键是如何扫描对应package下的接口的。

其实MapperScannerConfigurer的作用也就是将对应的接口的类型改造为MapperFactoryBean,而这个MapperFactoryBean的属性mapperInterface是原类型。MapperFactoryBean本文开头已分析过。
所以最终我们还是要分析MapperFactoryBean的实现原理!
MapperFactoryBean继承了SqlSessionDaoSupport类,SqlSessionDaoSupport类继承DaoSupport抽象类,DaoSupport抽象类实现了InitializingBean接口,因此实例个MapperFactoryBean的时候,都会调用InitializingBean接口的afterPropertiesSet方法。
DaoSupport的afterPropertiesSet方法:

MapperFactoryBean重写了checkDaoConfig方法:

然后通过spring工厂拿对应的bean的时候:

这里的SqlSession是SqlSessionTemplate,SqlSessionTemplate的getMapper方法:

Configuration的getMapper方法,会使用MapperRegistry的getMapper方法:(这里就回到了mybatis包)

MapperRegistry的getMapper方法:

MapperProxyFactory构造MapperProxy:

没错! MapperProxyFactory就是使用了jdk组带的Proxy完成动态代理。MapperProxy本来一开始已经提到。MapperProxy内部使用了MapperMethod类完成方法的调用:(这里在Mybait-SQL执行流程中有讲过)
Mybatis SQL执行流程 (四)

下面,我们以UserDao的getById方法来debug看看MapperMethod的execute方法是如何走的:
@Testpublic void testGet() {int id = 1;System.out.println(userDao.getById(id));}<select id="getById" parameterType="int" resultType="org.format.dynamicproxy.mybatis.bean.User">SELECT * FROM users WHERE id = #{id}</select>


2.4 SqlSessionTemplate
Mybatis-Spring为我们提供了一个实现了SqlSession接口的SqlSessionTemplate类,它是线程安全的,可以被多个Dao同时使用。同时它还跟Spring的事务进行了关联,确保当前被使用的SqlSession是一个已经和Spring的事务进行绑定了的。而且它还可以自己管理Session的提交和关闭。当使用了Spring的事务管理机制后,SqlSession还可以跟着Spring的事务一起提交和回滚。
使用SqlSessionTemplate时我们可以在Spring的applicationContext配置文件中如下定义:
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"><constructor-arg index="0" ref="sqlSessionFactory" /></bean>
通过源码我们何以看到 SqlSessionTemplate 实现了SqlSession接口,也就是说我们可以使用SqlSessionTemplate 来代理以往的DefailtSqlSession完成对数据库的操作,但是DefaultSqlSession这个类不是线程安全的,所以这个类不可以被设置成单例模式的。
如果是常规开发模式 我们每次在使用DefaultSqlSession的时候都从SqlSessionFactory当中获取一个就可以了。但是与Spring集成以后,Spring提供了一个全局唯一的SqlSessionTemplate示例 来完成DefaultSqlSession的功能,问题就是:无论是多个dao使用一个SqlSessionTemplate,还是一个dao使用一个SqlSessionTemplate,SqlSessionTemplate都是对应一个sqlSession,当多个web线程调用同一个dao时,它们使用的是同一个SqlSessionTemplate,也就是同一个SqlSession,那么它是如何确保线程安全的呢?让我们一起来分析一下。
如何保证线程安全
- 首先,通过如下代码创建代理类,表示创建SqlSessionFactory的代理类的实例,该代理类实现SqlSession接口,定义了方法拦截器,如果调用代理类实例中实现SqlSession接口定义的方法,该调用则被导向SqlSessionInterceptor的invoke方法:
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;this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),new Class[] { SqlSession.class },new SqlSessionInterceptor());}
- 核心代码就在 SqlSessionInterceptor的invoke方法当中:
private class SqlSessionInterceptor implements InvocationHandler {public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//获取SqlSession(这个SqlSession才是真正使用的,它不是线程安全的)//这个方法可以根据Spring的事务上下文来获取事务范围内的sqlSession//一会我们在分析这个方法final SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,SqlSessionTemplate.this.executorType,SqlSessionTemplate.this.exceptionTranslator);try {//调用真实SqlSession的方法Object result = method.invoke(sqlSession, args);//然后判断一下当前的sqlSession是否被Spring托管 如果未被Spring托管则自动commitif (!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) {Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);if (translated != null) {unwrapped = translated;}}throw unwrapped;} finally {//关闭sqlSession//它会根据当前的sqlSession是否在Spring的事务上下文当中来执行具体的关闭动作//如果sqlSession被Spring管理 则调用holder.released(); 使计数器-1//否则才真正的关闭sqlSessioncloseSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);}}}
在上面的invoke方法当中使用了俩个工具方法 分别是:
SqlSessionUtils.getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator)
- SqlSessionUtils.closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory)
那么这个俩个方法又是如何与Spring的事务进行关联的呢?
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {//根据sqlSessionFactory从当前线程对应的资源map中获取SqlSessionHolder,当sqlSessionFactory创建了sqlSession,就会在事务管理器中添加一对映射:key为sqlSessionFactory,value为SqlSessionHolder,该类保存sqlSession及执行方式SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);//如果holder不为空,且和当前事务同步if (holder != null && holder.isSynchronizedWithTransaction()) {//hodler保存的执行类型和获取SqlSession的执行类型不一致,就会抛出异常,也就是说在同一个事务中,执行类型不能变化,原因就是同一个事务中同一个sqlSessionFactory创建的sqlSession会被重用if (holder.getExecutorType() != executorType) {throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");}//增加该holder,也就是同一事务中同一个sqlSessionFactory创建的唯一sqlSession,其引用数增加,被使用的次数增加holder.requested();//返回sqlSessionreturn holder.getSqlSession();}//如果找不到,则根据执行类型构造一个新的sqlSessionSqlSession session = sessionFactory.openSession(executorType);//判断同步是否激活,只要SpringTX被激活,就是trueif (isSynchronizationActive()) {//加载环境变量,判断注册的事务管理器是否是SpringManagedTransaction,也就是Spring管理事务Environment environment = sessionFactory.getConfiguration().getEnvironment();if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {//如果是,则将sqlSession加载进事务管理的本地线程缓存中holder = new SqlSessionHolder(session, executorType, exceptionTranslator);//以sessionFactory为key,hodler为value,加入到TransactionSynchronizationManager管理的本地缓存ThreadLocal<Map<Object, Object>> resources中bindResource(sessionFactory, holder);//将holder, sessionFactory的同步加入本地线程缓存中ThreadLocal<Set<TransactionSynchronization>> synchronizationsregisterSynchronization(new SqlSessionSynchronization(holder, sessionFactory));//设置当前holder和当前事务同步holder.setSynchronizedWithTransaction(true);//增加引用数holder.requested();} else {if (getResource(environment.getDataSource()) == null) {} else {throw new TransientDataAccessResourceException("SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");}}} else {}return session;}public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {//其实下面就是判断session是否被Spring事务管理,如果管理就会得到holderSqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);if ((holder != null) && (holder.getSqlSession() == session)) {//这里释放的作用,不是关闭,只是减少一下引用数,因为后面可能会被复用holder.released();} else {//如果不是被spring管理,那么就不会被Spring去关闭回收,就需要自己closesession.close();}}
这样我们就可以通过Spring的依赖注入在Dao中直接使用SqlSessionTemplate来编程了,这个时候我们的Dao可能是这个样子:
package com.tiantian.mybatis.dao;import java.util.List;import javax.annotation.Resource;import org.mybatis.spring.SqlSessionTemplate;import org.springframework.stereotype.Repository;import com.tiantian.mybatis.model.Blog;@Repositorypublic class BlogDaoImpl implements BlogDao {private SqlSessionTemplate sqlSessionTemplate;public void deleteBlog(int id) {sqlSessionTemplate.delete("com.tiantian.mybatis.mapper.BlogMapper.deleteBlog", id);}public Blog find(int id) {return sqlSessionTemplate.selectOne("com.tiantian.mybatis.mapper.BlogMapper.selectBlog", id);}public List<Blog> find() {return this.sqlSessionTemplate.selectList("com.tiantian.mybatis.mapper.BlogMapper.selectAll");}public void insertBlog(Blog blog) {this.sqlSessionTemplate.insert("com.tiantian.mybatis.mapper.BlogMapper.insertBlog", blog);}public void updateBlog(Blog blog) {this.sqlSessionTemplate.update("com.tiantian.mybatis.mapper.BlogMapper.updateBlog", blog);}public SqlSessionTemplate getSqlSessionTemplate() {return sqlSessionTemplate;}@Resourcepublic void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {this.sqlSessionTemplate = sqlSessionTemplate;}}
