与Spring集成

单独使用mybatis是有很多限制的(比如无法实现跨越多个session的事务),而且很多业务系统本来就是使用spring来管理的事务,因此mybatis最好与spring集成起来使用。

1 Spring集成配置#

<bean id="sqlSessionFactory"   
    class="org.mybatis.spring.SqlSessionFactoryBean">  
    <property name="dataSource" ref="datasource"></property>  
    <property name="configLocation" value="classpath:context/mybatis-config.xml"></property>  
    <!-- 
    mapperLocations:通过正则表达式,支持mybatis动态扫描添加mapper不用像ibatis,用一个还要蛋疼滴添加一个include
    -->
    <property name="mapperLocations" value="classpath*:/com/tx/demo/**/*SqlMap.xml" />
    <!-- 
    typeHandlersPackage: 由于mybatis默认入参如果为空,又没有指定jdbcType时会抛出异常,在这里通过配置一些默认的类型空值插入的handle,以便处理mybatis的默认类型为空的情况。
    例如NullAbleStringTypeHandle通过实现当String字符串中为null是调用ps.setString(i,null)其他常用类型雷同。
    -->  
    <property name="typeHandlersPackage" value="com.tx.core.mybatis.handler"></property>
    <!-- 
    failFast:开启后将在启动时检查设定的parameterMap,resultMap是否存在,是否合法。个人建议设置为true,这样可以尽快定位解决问题。不然在调用过程中发现错误,会影响问题定位。
    --> 
    <property name="failFast" value="true"></property>  
    <property name="plugins">  
        <array>  
            <bean class="com.tx.core.mybatis.interceptor.PagedDiclectStatementHandlerInterceptor">  
                <property name="dialect">  
                    <bean class="org.hibernate.dialect.PostgreSQLDialect"></bean>  
                </property>  
            </bean>  
        </array>  
    </property>  
</bean>  
<!--
myBatisExceptionTranslator:用以支持spring的异常转换,通过配置该translator可以将mybatis异常转换为spring中定义的DataAccessException。
-->
<bean id="myBatisExceptionTranslator" class="org.mybatis.spring.MyBatisExceptionTranslator">  
    <property name="dataSource">  
        <ref bean="datasource"></ref>  
    </property>  
</bean>  
  
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">  
    <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"></constructor-arg>  
    <constructor-arg name="executorType" ref="SIMPLE"></constructor-arg>  
    <constructor-arg name="exceptionTranslator" ref="myBatisExceptionTranslator"></constructor-arg>  
</bean>  
  
<bean id="myBatisDaoSupport" class="com.tx.core.mybatis.support.MyBatisDaoSupport">  
    <property name="sqlSessionTemplate">  
        <ref bean="sqlSessionTemplate"/>  
    </property>  
</bean>

2 Spring事务配置#

<!-- 自动扫描业务包 -->  
<context:component-scan base-package="com.xxx.service" />  
  
<!-- 数据源 -->  
<jee:jndi-lookup id="jndiDataSource" jndi-name="java:comp/env/jdbc/datasource" />
  
<!-- 配置事务 -->  
<bean id="txManager"  
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    <property name="dataSource" ref="jndiDataSource" />  
</bean>  
<!-- 配置基于注解的事务aop -->  
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/>  

或:

<!-- 自动扫描业务包 -->  
<context:component-scan base-package="com.xxx.service" />  
  
<!-- 数据源 -->  
<jee:jndi-lookup id="jndiDataSource" jndi-name="java:comp/env/jdbc/datasource" />

<!-- 配置事务管理器,注意这里的dataSource和SqlSessionFactoryBean的dataSource要一致,不然事务就没有作用了 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>
 
<!-- 配置事务的传播特性 -->
<bean id="baseTransactionProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean" abstract="true">
    <property name="transactionManager" ref="transactionManager" />
    <property name="transactionAttributes">
        <props>
            <prop key="add*">PROPAGATION_REQUIRED</prop>
            <prop key="edit*">PROPAGATION_REQUIRED</prop>
            <prop key="remove*">PROPAGATION_REQUIRED</prop>
            <prop key="insert*">PROPAGATION_REQUIRED</prop>
            <prop key="update*">PROPAGATION_REQUIRED</prop>
            <prop key="del*">PROPAGATION_REQUIRED</prop>
            <prop key="*">readOnly</prop>
        </props>
    </property>
</bean>

<!--把事务控制在Service层-->
<aop:config>
    <aop:pointcut id="pc" expression="execution(public * com.jeasy..service.*.*(..))" />
    <aop:advisor pointcut-ref="pc" advice-ref="baseTransactionProxy" />
</aop:config>

3 单个集成#

<!-- 集成mybatis -->  
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
    <property name="dataSource" ref="jndiDataSource" />  
    <property name="configLocation" value="classpath:/mybatis/mybatis-config.xml" />  
    <!-- 自动配置别名 -->  
    <property name="typeAliasesPackage" value="com.xxx.dto" />  
</bean>  
  
<!--创建dao bean(只需提供接口不需提供实现类 )-->  
<bean id="userDao" class="org.mybatis.spring.mapper.MapperFactoryBean">  
    <property name="mapperInterface" value="com.xxx.dao.UserDao" />  
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />  
</bean>

我们不但要明白如何使用,更要明白为什么要这么使用。

SqlSessionFactoryBean是一个工厂bean,它的作用就是解析配置(数据源、别名等)。

MapperFactoryBean是一个工厂bean,在spring容器里,工厂bean是有特殊用途的,当spring将工厂bean注入到其他bean里时,它不是注入工厂bean本身而是调用bean的getObject方法。我们接下来就看看这个getObjec方法干了些什么:

public T getObject() throws Exception {  
    return getSqlSession().getMapper(this.mapperInterface);  
}

看到这里大家应该就很明白了,这个方法和我们之前单独使用Mybatis的方式是一样的,都是先获取一个Sqlsession对象,然后再从Sqlsession里获取Mapper对象(再次强调Mapper是一个代理对象,它代理的是mapperInterface接口,而这个接口是用户提供的dao接口)。自然,最终注入到业务层就是这个Mapper对象。

实际的项目一般来说不止一个Dao,如果你有多个Dao那就按照上面的配置依次配置即可。

4 如何使用批量更新#

前一节讲了如何注入一个mapper对象到业务层,mapper的行为依赖于配置,mybatis默认使用单个更新(即ExecutorType默认为SIMPLE而不是BATCH),当然我们可以通过修改mybatis配置文件来修改默认行为,但如果我们只想让某个或某几个mapper使用批量更新就不得行了。这个时候我们就需要使用模板技术:

<!--通过模板定制mybatis的行为 -->  
<bean id="sqlSessionTemplateSimple" class="org.mybatis.spring.SqlSessionTemplate">     
    <constructor-arg index="0" ref="sqlSessionFactory" />  
    <!--更新采用单个模式 -->  
    <constructor-arg index="1" value="SIMPLE"/>  
</bean>  
      
<!--通过模板定制mybatis的行为 -->  
<bean id="sqlSessionTemplateBatch" class="org.mybatis.spring.SqlSessionTemplate">     
    <constructor-arg index="0" ref="sqlSessionFactory" />  
    <!--更新采用批量模式 -->  
    <constructor-arg index="1" value="BATCH"/>  
</bean> 

这里笔者定义了两个模板对象,一个使用单个更新,一个使用批量更新。有了模板之后我们就可以改变mapper的行为方式了:

<bean id="userDao" class="org.mybatis.spring.mapper.MapperFactoryBean">  
    <property name="mapperInterface" value="com.xxx.dao.UserDao" />  
    <property name="sqlSessionTemplate" ref="sqlSessionTemplateBatch" />  
</bean> 

跟上一节的mapper配置不同的是,这里不需要配置sqlSessionFactory属性,只需要配置sqlSessionTemplate(sqlSessionFactory属性在模板里已经配置好了)

由于在3.1.1升级后,可直接通过BatchExcutor实现具体的批量执行。在BatchExcutor中会重用上一次相同的PreparedStatement。

5 通过自动扫描简化mapper的配置#

前面的章节可以看到,我们的dao需要一个一个的配置在配置文件中,如果有很多个dao的话配置文件就会非常大,这样管理起来就会比较痛苦。幸好mybatis团队也意识到了这点,他们利用spring提供的自动扫描功能封装了一个自动扫描dao的工具类,这样我们就可以使用这个功能简化配置:

<!-- 采用自动扫描方式创建mapper bean(单个更新模式) -->  
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
    <property name="basePackage" value="com.xxx.dao" />  
    <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplateSimple" />  
    <property name="markerInterface" value="com.xxx.dao.SimpleDao" />  
</bean>  
       
<!-- 采用自动扫描方式创建mapper bean(批量更新模式) -->  
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
    <property name="basePackage" value="com.xxx.dao" />  
    <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplateBatch" />  
    <property name="markerInterface" value="com.xxx.dao.BatchDao" />  
</bean>

MapperScannerConfigurer本身涉及的spring的技术我就不多讲了,感兴趣且对spring原理比较了解的可以去看下它的源码。我们重点看一下它的三个属性:

basePackage:扫描器开始扫描的基础包名,支持嵌套扫描;

sqlSessionTemplateBeanName:前文提到的模板bean的名称;

markerInterface:基于接口的过滤器,实现了该接口的dao才会被扫描器扫描,与basePackage是与的作用

除了使用接口过滤外,还可使用注解过滤:

<!-- 采用自动扫描方式创建mapper bean(批量更新模式) -->  
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
    <property name="basePackage" value="com.xxx.dao" />  
    <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplateBatch" />  
    <property name="annotationClass" value="com.xxx.dao.BatchAnnotation" />  
</bean>

annotationClass:配置了该注解的dao才会被扫描器扫描,与basePackage是与的作用。需要注意的是,与上个接口过滤条件只能配一个。

markerInterface:markerInterface是用于指定一个接口的,当指定了markerInterface之后,MapperScannerConfigurer将只注册继承自markerInterface的接口。

6 与Spring集成源码分析#

6.1 SqlSessionFactory##

我们知道在Mybatis的所有操作都是基于一个SqlSession的,而SqlSession是由SqlSessionFactory来产生的,SqlSessionFactory又是由SqlSessionFactoryBuilder来生成的。但是Mybatis-Spring是基于SqlSessionFactoryBean的。在使用Mybatis-Spring的时候,我们也需要SqlSession,而且这个SqlSession是内嵌在程序中的,一般不需要我们直接访问。SqlSession也是由SqlSessionFactory来产生的,但是Mybatis-Spring给我们封装了一个SqlSessionFactoryBean,在这个bean里面还是通过SqlSessionFactoryBuilder来建立对应的SqlSessionFactory,进而获取到对应的SqlSession。通过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

接下来就是在Spring的applicationContext文件中定义我们想要的Mapper对象对应的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的整合。

如果想使用MapperScannerConfigurer,想要了解该类的作用,就得先了解MapperFactoryBean。

6.2 MapperFactoryBean##

MapperFactoryBean的出现为了代替手工使用SqlSessionDaoSupport或SqlSessionTemplate编写数据访问对象(DAO)的代码,使用动态代理实现

比如下面这个官方文档中的配置:

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

org.mybatis.spring.sample.mapper.UserMapper是一个接口,我们创建一个MapperFactoryBean实例,然后注入这个接口和sqlSessionFactory(mybatis中提供的SqlSessionFactory接口,MapperFactoryBean会使用SqlSessionFactory创建SqlSession)这两个属性。

之后想使用这个UserMapper接口的话,直接通过spring注入这个bean,然后就可以直接使用了,spring内部会创建一个这个接口的动态代理

当发现要使用多个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下的所有接口,然后创建各自接口的动态代理类。

6.3 MapperScannerConfigurer##

如果我们需要使用MapperScannerConfigurer来帮我们自动扫描和注册Mapper接口的话我们需要在Spring的applicationContext配置文件中定义一个MapperScannerConfigurer对应的bean。对于MapperScannerConfigurer而言有一个属性是我们必须指定的,那就是basePackage。basePackage是用来指定Mapper接口文件所在的基包的,在这个基包或其所有子包下面的Mapper接口都将被搜索到。多个基包之间可以使用逗号或者分号进行分隔。最简单的MapperScannerConfigurer定义就是只指定一个basePackage属性,如:

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的实现类到底是什么:

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方法。

postProcessBeanDefinitionRegistry()实现

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

ClassPathMapperScanner扫描对应package下的接口

其实MapperScannerConfigurer的作用也就是将对应的接口的类型改造为MapperFactoryBean,而这个MapperFactoryBean的属性mapperInterface是原类型。MapperFactoryBean本文开头已分析过。

所以最终我们还是要分析MapperFactoryBean的实现原理!

MapperFactoryBean继承了SqlSessionDaoSupport类,SqlSessionDaoSupport类继承DaoSupport抽象类,DaoSupport抽象类实现了InitializingBean接口,因此实例个MapperFactoryBean的时候,都会调用InitializingBean接口的afterPropertiesSet方法。

DaoSupport的afterPropertiesSet方法:

DaoSupport的afterPropertiesSet方法

MapperFactoryBean重写了checkDaoConfig方法:

MapperFactoryBean重写了checkDaoConfig方法

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

通过spring工厂拿对应的bean

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

SqlSessionTemplate的getMapper方法

Configuration的getMapper方法,会使用MapperRegistry的getMapper方法:

MapperRegistry的getMapper方法

MapperRegistry的getMapper方法:

MapperRegistry的getMapper方法

MapperProxyFactory构造MapperProxy:

MapperProxyFactory构造MapperProxy

没错! MapperProxyFactory就是使用了jdk组带的Proxy完成动态代理。MapperProxy本来一开始已经提到。MapperProxy内部使用了MapperMethod类完成方法的调用:

MapperProxy内部使用了MapperMethod类完成方法的调用

下面,我们以UserDao的getById方法来debug看看MapperMethod的execute方法是如何走的:

@Test
public 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>
debug看看MapperMethod的execute方法
debug看看MapperMethod的execute方法
debug看看MapperMethod的execute方法
debug看看MapperMethod的execute方法

6.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完成对数据库的操作,但是DefailtSqlSession这个类不是线程安全的,所以这个类不可以被设置成单例模式的。

如果是常规开发模式,我们每次在使用DefailtSqlSession的时候都从SqlSessionFactory当中获取一个就可以了。但是与Spring集成以后,Spring提供了一个全局唯一的SqlSessionTemplate示例 来完成DefailtSqlSession的功能

问题就是:无论是多个dao使用一个SqlSessionTemplate,还是一个dao使用一个SqlSessionTemplate,SqlSessionTemplate都是对应一个sqlSession,当多个web线程调用同一个dao时,它们使用的是同一个SqlSessionTemplate,也就是同一个SqlSession,那么它是如何确保线程安全的呢?让我们一起来分析一下。

  1. 首先,通过如下代码创建代理类,表示创建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());
}
  1. 核心代码就在 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托管则自动commit
           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) {
               Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
               if (translated != null) {
                   unwrapped = translated;
               }
           }
           throw unwrapped;
       } finally {
           //关闭sqlSession
           //它会根据当前的sqlSession是否在Spring的事务上下文当中来执行具体的关闭动作
           //如果sqlSession被Spring管理 则调用holder.released(); 使计数器-1
           //否则才真正的关闭sqlSession
           closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
       }
    }
}
  1. 在上面的invoke方法当中使用了俩个工具方法分别是:
  1. SqlSessionUtils.getSqlSession(...)
  1. SqlSessionUtils.closeSqlSession(...)

那么这个俩个方法又是如何与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(); 
          //返回sqlSession 
          return holder.getSqlSession(); 
      } 
      //如果找不到,则根据执行类型构造一个新的sqlSession 
      SqlSession session = sessionFactory.openSession(executorType); 
      //判断同步是否激活,只要SpringTX被激活,就是true 
      if (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>> synchronizations 
              registerSynchronization(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事务管理,如果管理就会得到holder  
     SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory); 
     if ((holder != null) && (holder.getSqlSession() == session)) { 
         //这里释放的作用,不是关闭,只是减少一下引用数,因为后面可能会被复用 
         holder.released(); 
     } else { 
         //如果不是被spring管理,那么就不会被Spring去关闭回收,就需要自己close 
         session.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;
 
@Repository
public 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;
    }
   
    @Resource
    public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
       this.sqlSessionTemplate = sqlSessionTemplate;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容