【深刻淺出MyBatis系列十】與Spring集成

#0 系列目錄#java

單獨使用mybatis是有不少限制的(好比沒法實現跨越多個session的事務),並且不少業務系統原本就是使用spring來管理的事務,所以mybatis最好與spring集成起來使用。 #1 Spring集成配置#web

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

或:spring

<!-- 自動掃描業務包 -->  
<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 單個集成#sql

<!-- 集成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,它的做用就是解析配置(數據源、別名等)express

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實現具體的批量執行。在該excutor中會重用上一次相同的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的實現類究竟是什麼:

輸入圖片說明

咱們能夠看到,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方法:

輸入圖片說明

MapperRegistry的getMapper方法:

輸入圖片說明

MapperProxyFactory構造MapperProxy:

輸入圖片說明

**沒錯! MapperProxyFactory就是使用了jdk組帶的Proxy完成動態代理。**MapperProxy原本一開始已經提到。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>

輸入圖片說明

輸入圖片說明

##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方法當中使用了倆個工具方法 分別是:

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(); 
     //返回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;
    }
}
相關文章
相關標籤/搜索