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

陶邦仁 發(fā)布于 2015/12/25 15:42

原文鏈接

系列目錄

單獨(dú)使用mybatis是有很多限制的(比如無法實(shí)現(xiàn)跨越多個session的事務(wù)),而且很多業(yè)務(wù)系統(tǒng)本來就是使用spring來管理的事務(wù),因此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:通過正則表達(dá)式,支持mybatis動態(tài)掃描添加mapper不用像ibatis,用一個還要蛋疼滴添加一個include
    -->
    <property name="mapperLocations" value="classpath*:/com/tx/demo/**/*SqlMap.xml" />
    <!-- 
    typeHandlersPackage: 由于mybatis默認(rèn)入?yún)⑷绻麨榭?,又沒有指定jdbcType時會拋出異常,在這里通過配置一些默認(rèn)的類型空值插入的handle,以便處理mybatis的默認(rèn)類型為空的情況。
    例如NullAbleStringTypeHandle通過實(shí)現(xiàn)當(dāng)String字符串中為null是調(diào)用ps.setString(i,null)其他常用類型雷同。
    -->  
    <property name="typeHandlersPackage" value="com.tx.core.mybatis.handler"></property>
    <!-- 
    failFast:開啟后將在啟動時檢查設(shè)定的parameterMap,resultMap是否存在,是否合法。個人建議設(shè)置為true,這樣可以盡快定位解決問題。不然在調(diào)用過程中發(fā)現(xiàn)錯誤,會影響問題定位。
    --> 
    <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的異常轉(zhuǎn)換,通過配置該translator可以將mybatis異常轉(zhuǎn)換為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事務(wù)配置

<!-- 自動掃描業(yè)務(wù)包 -->  
<context:component-scan base-package="com.xxx.service" />  

<!-- 數(shù)據(jù)源 -->  
<jee:jndi-lookup id="jndiDataSource" jndi-name="java:comp/env/jdbc/datasource" />

<!-- 配置事務(wù) -->  
<bean id="txManager"  
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    <property name="dataSource" ref="jndiDataSource" />  
</bean>  
<!-- 配置基于注解的事務(wù)aop -->  
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/>  

或:

<!-- 自動掃描業(yè)務(wù)包 -->  
<context:component-scan base-package="com.xxx.service" />  

<!-- 數(shù)據(jù)源 -->  
<jee:jndi-lookup id="jndiDataSource" jndi-name="java:comp/env/jdbc/datasource" />

<!-- 配置事務(wù)管理器,注意這里的dataSource和SqlSessionFactoryBean的dataSource要一致,不然事務(wù)就沒有作用了 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

<!-- 配置事務(wù)的傳播特性 -->
<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>

<!--把事務(wù)控制在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>  

<!--創(chuàng)建dao bean(只需提供接口不需提供實(shí)現(xiàn)類 )-->  
<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,它的作用就是解析配置(數(shù)據(jù)源、別名等)。

MapperFactoryBean是一個工廠bean,在spring容器里,工廠bean是有特殊用途的,當(dāng)spring將工廠bean注入到其他bean里時,它不是注入工廠bean本身而是調(diào)用bean的getObject方法。我們接下來就看看這個getObjec方法干了些什么:

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

看到這里大家應(yīng)該就很明白了,這個方法和我們之前單獨(dú)使用Mybatis的方式是一樣的,都是先獲取一個Sqlsession對象,然后再從Sqlsession里獲取Mapper對象(再次強(qiáng)調(diào)Mapper是一個代理對象,它代理的是mapperInterface接口,而這個接口是用戶提供的dao接口)。自然,最終注入到業(yè)務(wù)層就是這個Mapper對象。

實(shí)際的項(xiàng)目一般來說不止一個Dao,如果你有多個Dao那就按照上面的配置依次配置即可。

4 如何使用批量更新

前一節(jié)講了如何注入一個mapper對象到業(yè)務(wù)層, mapper的行為依賴于配置,mybatis默認(rèn)使用單個更新(即ExecutorType默認(rèn)為SIMPLE而不是BATCH),當(dāng)然我們可以通過修改mybatis配置文件來修改默認(rèn)行為,但如果我們只想讓某個或某幾個mapper使用批量更新就不得行了。這個時候我們就需要使用模板技術(shù)

<!--通過模板定制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> 

跟上一節(jié)的mapper配置不同的是,這里不需要配置sqlSessionFactory屬性,只需要配置sqlSessionTemplate(sqlSessionFactory屬性在模板里已經(jīng)配置好了)。

由于在3.1.1升級后,可直接通過BatchExcutor實(shí)現(xiàn)具體的批量執(zhí)行。在該excutor中會重用上一次相同的PreparedStatement。

5 通過自動掃描簡化mapper的配置

前面的章節(jié)可以看到,我們的dao需要一個一個的配置在配置文件中,如果有很多個dao的話配置文件就會非常大,這樣管理起來就會比較痛苦。幸好mybatis團(tuán)隊(duì)也意識到了這點(diǎn),他們利用spring提供的自動掃描功能封裝了一個自動掃描dao的工具類,這樣我們就可以使用這個功能簡化配置:

<!-- 采用自動掃描方式創(chuàng)建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>  

<!-- 采用自動掃描方式創(chuàng)建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的技術(shù)我就不多講了,感興趣且對spring原理比較了解的可以去看下它的源碼。我們重點(diǎn)看一下它的三個屬性:

basePackage:掃描器開始掃描的基礎(chǔ)包名,支持嵌套掃描;

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

markerInterface:基于接口的過濾器,實(shí)現(xiàn)了該接口的dao才會被掃描器掃描,與basePackage是與的作用。

除了使用接口過濾外,還可使用注解過濾

<!-- 采用自動掃描方式創(chuàng)建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是用于指定一個接口的,當(dāng)指定了markerInterface之后,MapperScannerConfigurer將只注冊繼承自markerInterface的接口。

6 與Spring集成源碼分析

6.1 SqlSessionFactory

我們知道在Mybatis的所有操作都是基于一個SqlSession的,而SqlSession是由SqlSessionFactory來產(chǎn)生的,SqlSessionFactory又是由SqlSessionFactoryBuilder來生成的。但是Mybatis-Spring是基于SqlSessionFactoryBean的。在使用Mybatis-Spring的時候,我們也需要SqlSession,而且這個SqlSession是內(nèi)嵌在程序中的,一般不需要我們直接訪問。SqlSession也是由SqlSessionFactory來產(chǎn)生的,但是Mybatis-Spring給我們封裝了一個SqlSessionFactoryBean,在這個bean里面還是通過SqlSessionFactoryBuilder來建立對應(yīng)的SqlSessionFactory,進(jìn)而獲取到對應(yīng)的SqlSession。通過SqlSessionFactoryBean我們可以通過對其指定一些屬性來提供Mybatis的一些配置信息。所以接下來我們需要在Spring的applicationContext配置文件中定義一個SqlSessionFactoryBean。

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
       <!-- dataSource屬性是必須指定的,它表示用于連接數(shù)據(jù)庫的數(shù)據(jù)源 -->  
       <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文件存放的位置,當(dāng)我們的Mapper文件跟對應(yīng)的Mapper接口處于同一位置的時候可以不用指定該屬性的值。

  • configLocation:用于指定Mybatis的配置文件位置。如果指定了該屬性,那么會以該配置文件的內(nèi)容作為配置信息構(gòu)建對應(yīng)的SqlSessionFactoryBuilder,但是后續(xù)屬性指定的內(nèi)容會覆蓋該配置文件里面指定的對應(yīng)內(nèi)容。

  • typeAliasesPackage:它一般對應(yīng)我們的實(shí)體類所在的包,這個時候會自動取對應(yīng)包中不包括包名的簡單類名作為包括包名的別名。多個package之間可以用逗號或者分號等來進(jìn)行分隔。

  • typeAliases:數(shù)組類型,用來指定別名的。指定了這個屬性后,Mybatis會把這個類型的短名稱作為這個類型的別名,前提是該類上沒有標(biāo)注@Alias注解,否則將使用該注解對應(yīng)的值作為此種類型的別名。

<property name="typeAliases">  
   <array>  
       <value>com.tiantian.mybatis.model.Blog</value>  
       <value>com.tiantian.mybatis.model.Comment</value>  
   </array>  
</property>

  • plugins:數(shù)組類型,用來指定Mybatis的Interceptor。

  • typeHandlersPackage:用來指定TypeHandler所在的包,如果指定了該屬性,SqlSessionFactoryBean會自動把該包下面的類注冊為對應(yīng)的TypeHandler。多個package之間可以用逗號或者分號等來進(jìn)行分隔。

  • typeHandlers:數(shù)組類型,表示TypeHandler。

接下來就是在Spring的applicationContext文件中定義我們想要的Mapper對象對應(yīng)的MapperFactoryBean了。通過MapperFactoryBean可以獲取到我們想要的Mapper對象。MapperFactoryBean實(shí)現(xiàn)了Spring的FactoryBean接口,所以MapperFactoryBean是通過FactoryBean接口中定義的getObject方法來獲取對應(yīng)的Mapper對象的。在定義一個MapperFactoryBean的時候有兩個屬性需要我們注入,一個是Mybatis-Spring用來生成實(shí)現(xiàn)了SqlSession接口的SqlSessionTemplate對象的sqlSessionFactory;`另一個就是我們所要返回的對應(yīng)的Mapper接口了。

定義好相應(yīng)Mapper接口對應(yīng)的MapperFactoryBean之后,我們就可以把我們對應(yīng)的Mapper接口注入到由Spring管理的bean對象中了,比如Service bean對象。這樣當(dāng)我們需要使用到相應(yīng)的Mapper接口時,MapperFactoryBean會從它的getObject方法中獲取對應(yīng)的Mapper接口,而getObject內(nèi)部還是通過我們注入的屬性調(diào)用SqlSession接口的getMapper(Mapper接口)方法來返回對應(yīng)的Mapper接口的。這樣就通過把SqlSessionFactory和相應(yīng)的Mapper接口交給Spring管理實(shí)現(xiàn)了Mybatis跟Spring的整合。

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

6.2 MapperFactoryBean

MapperFactoryBean的出現(xiàn)為了代替手工使用SqlSessionDaoSupport或SqlSessionTemplate編寫數(shù)據(jù)訪問對象(DAO)的代碼,使用動態(tài)代理實(shí)現(xiàn)

比如下面這個官方文檔中的配置:

<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是一個接口,我們創(chuàng)建一個MapperFactoryBean實(shí)例,然后注入這個接口和sqlSessionFactory(mybatis中提供的SqlSessionFactory接口,MapperFactoryBean會使用SqlSessionFactory創(chuàng)建SqlSession)這兩個屬性。

之后想使用這個UserMapper接口的話,直接通過spring注入這個bean,然后就可以直接使用了,spring內(nèi)部會創(chuàng)建一個這個接口的動態(tài)代理。

當(dāng)發(fā)現(xiàn)要使用多個MapperFactoryBean的時候,一個一個定義肯定非常麻煩,于是mybatis-spring提供了MapperScannerConfigurer這個類,它將會查找類路徑下的映射器并自動將它們創(chuàng)建成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下的所有接口,然后創(chuàng)建各自接口的動態(tài)代理類。

6.3 MapperScannerConfigurer

如果我們需要使用MapperScannerConfigurer來幫我們自動掃描和注冊Mapper接口的話我們需要在Spring的applicationContext配置文件中定義一個MapperScannerConfigurer對應(yīng)的bean。對于MapperScannerConfigurer而言有一個屬性是我們必須指定的,那就是basePackage。basePackage是用來指定Mapper接口文件所在的基包的,在這個基包或其所有子包下面的Mapper接口都將被搜索到。多個基包之間可以使用逗號或者分號進(jìn)行分隔。最簡單的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:當(dāng)指定了annotationClass的時候,MapperScannerConfigurer將只注冊使用了annotationClass注解標(biāo)記的接口。
  • markerInterface:markerInterface是用于指定一個接口的,當(dāng)指定了markerInterface之后,MapperScannerConfigurer將只注冊繼承自markerInterface的接口。

如果上述兩個屬性都指定了的話,那么MapperScannerConfigurer將取它們的并集,而不是交集。即使用了annotationClass進(jìn)行標(biāo)記或者繼承自markerInterface的接口都將被注冊為一個MapperFactoryBean。

  • sqlSessionFactory:這個屬性已經(jīng)廢棄。當(dāng)我們使用了多個數(shù)據(jù)源的時候我們就需要通過sqlSessionFactory來指定在注冊MapperFactoryBean的時候需要使用的SqlSessionFactory,因?yàn)樵跊]有指定sqlSessionFactory的時候,會以Autowired的方式自動注入一個。換言之當(dāng)我們只使用一個數(shù)據(jù)源的時候,即只定義了一個SqlSessionFactory的時候我們就可以不給MapperScannerConfigurer指定SqlSessionFactory。

  • sqlSessionFactoryBeanName:它的功能跟sqlSessionFactory是一樣的,只是它指定的是定義好的SqlSessionFactory對應(yīng)的bean名稱。

  • sqlSessionTemplate:這個屬性已經(jīng)廢棄。它的功能也是相當(dāng)于sqlSessionFactory的,因?yàn)榫拖袂懊嬲f的那樣,MapperFactoryBean最終還是使用的SqlSession的getMapper方法取的對應(yīng)的Mapper對象。當(dāng)定義有多個SqlSessionTemplate的時候才需要指定它。對于一個MapperFactoryBean來說SqlSessionFactory和SqlSessionTemplate只需要其中一個就可以了,當(dāng)兩者都指定了的時候,SqlSessionFactory會被忽略。

  • sqlSessionTemplateBeanName:指定需要使用的sqlSessionTemplate對應(yīng)的bean名稱。

注意:由于使用sqlSessionFactory和sqlSessionTemplate屬性時會使一些內(nèi)容在PropertyPlaceholderConfigurer之前加載,導(dǎo)致在配置文件中使用到的外部屬性信息無法被及時替換而出錯,因此官方現(xiàn)在新的Mybatis-Spring中已經(jīng)把sqlSessionFactory和sqlSessionTemplate屬性廢棄了,推薦大家使用sqlSessionFactoryBeanName屬性和sqlSessionTemplateBeanName屬性。

我們先通過測試用例debug查看userDao的實(shí)現(xiàn)類到底是什么:

輸入圖片說明

我們可以看到,userDao是1個MapperProxy類的實(shí)例。看下MapperProxy的源碼,沒錯,實(shí)現(xiàn)了InvocationHandler,說明使用了jdk自帶的動態(tài)代理:

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實(shí)現(xiàn)了BeanDefinitionRegistryPostProcessor接口,BeanDefinitionRegistryPostProcessor接口是一個可以修改spring工廠中已定義的bean的接口,該接口有個postProcessBeanDefinitionRegistry方法。

輸入圖片說明

然后我們看下ClassPathMapperScanner中的關(guān)鍵是如何掃描對應(yīng)package下的接口的。

輸入圖片說明

其實(shí)MapperScannerConfigurer的作用也就是將對應(yīng)的接口的類型改造為MapperFactoryBean,而這個MapperFactoryBean的屬性mapperInterface是原類型。MapperFactoryBean本文開頭已分析過。

所以最終我們還是要分析MapperFactoryBean的實(shí)現(xiàn)原理!

MapperFactoryBean繼承了SqlSessionDaoSupport類,SqlSessionDaoSupport類繼承DaoSupport抽象類,DaoSupport抽象類實(shí)現(xiàn)了InitializingBean接口,因此實(shí)例個MapperFactoryBean的時候,都會調(diào)用InitializingBean接口的afterPropertiesSet方法。

DaoSupport的afterPropertiesSet方法:

輸入圖片說明

MapperFactoryBean重寫了checkDaoConfig方法:

輸入圖片說明

然后通過spring工廠拿對應(yīng)的bean的時候:

輸入圖片說明

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

輸入圖片說明

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

輸入圖片說明

MapperRegistry的getMapper方法:

輸入圖片說明

MapperProxyFactory構(gòu)造MapperProxy:

輸入圖片說明

沒錯! MapperProxyFactory就是使用了jdk組帶的Proxy完成動態(tài)代理。MapperProxy本來一開始已經(jīng)提到。MapperProxy內(nèi)部使用了MapperMethod類完成方法的調(diào)用:

輸入圖片說明

下面,我們以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為我們提供了一個實(shí)現(xiàn)了SqlSession接口的SqlSessionTemplate類,它是線程安全的,可以被多個Dao同時使用。同時它還跟Spring的事務(wù)進(jìn)行了關(guān)聯(lián),確保當(dāng)前被使用的SqlSession是一個已經(jīng)和Spring的事務(wù)進(jìn)行綁定了的。而且它還可以自己管理Session的提交和關(guān)閉。當(dāng)使用了Spring的事務(wù)管理機(jī)制后,SqlSession還可以跟著Spring的事務(wù)一起提交和回滾。

使用SqlSessionTemplate時我們可以在Spring的applicationContext配置文件中如下定義:

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg index="0" ref="sqlSessionFactory" />
</bean>

通過源碼我們何以看到 SqlSessionTemplate 實(shí)現(xiàn)了SqlSession接口,也就是說我們可以使用SqlSessionTemplate 來代理以往的DefailtSqlSession完成對數(shù)據(jù)庫的操作,但是DefailtSqlSession這個類不是線程安全的,所以這個類不可以被設(shè)置成單例模式的。

如果是常規(guī)開發(fā)模式 我們每次在使用DefailtSqlSession的時候都從SqlSessionFactory當(dāng)中獲取一個就可以了。但是與Spring集成以后,Spring提供了一個全局唯一的SqlSessionTemplate示例 來完成DefailtSqlSession的功能,問題就是:無論是多個dao使用一個SqlSessionTemplate,還是一個dao使用一個SqlSessionTemplate,SqlSessionTemplate都是對應(yīng)一個sqlSession,當(dāng)多個web線程調(diào)用同一個dao時,它們使用的是同一個SqlSessionTemplate,也就是同一個SqlSession,那么它是如何確保線程安全的呢?讓我們一起來分析一下。

  1. 首先,通過如下代碼創(chuàng)建代理類,表示創(chuàng)建SqlSessionFactory的代理類的實(shí)例,該代理類實(shí)現(xiàn)SqlSession接口,定義了方法攔截器,如果調(diào)用代理類實(shí)例中實(shí)現(xiàn)SqlSession接口定義的方法,該調(diào)用則被導(dǎo)向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方法當(dāng)中:
private class SqlSessionInterceptor implements InvocationHandler {
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
     //獲取SqlSession(這個SqlSession才是真正使用的,它不是線程安全的)
     //這個方法可以根據(jù)Spring的事務(wù)上下文來獲取事務(wù)范圍內(nèi)的sqlSession
     //一會我們在分析這個方法
     final SqlSession sqlSession = getSqlSession(
         SqlSessionTemplate.this.sqlSessionFactory,
         SqlSessionTemplate.this.executorType,
         SqlSessionTemplate.this.exceptionTranslator);
     try {
       //調(diào)用真實(shí)SqlSession的方法
       Object result = method.invoke(sqlSession, args);
       //然后判斷一下當(dāng)前的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);
       }
       //返回執(zhí)行結(jié)果
       return result;
     } catch (Throwable t) {
       //如果出現(xiàn)異常則根據(jù)情況轉(zhuǎn)換后拋出
       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 {
       //關(guān)閉sqlSession
       //它會根據(jù)當(dāng)前的sqlSession是否在Spring的事務(wù)上下文當(dāng)中來執(zhí)行具體的關(guān)閉動作
       //如果sqlSession被Spring管理 則調(diào)用holder.released(); 使計(jì)數(shù)器-1
       //否則才真正的關(guān)閉sqlSession
       closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
     }
   }
}

  1. 在上面的invoke方法當(dāng)中使用了倆個工具方法 分別是:

SqlSessionUtils.getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator)

SqlSessionUtils.closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory)

那么這個倆個方法又是如何與Spring的事務(wù)進(jìn)行關(guān)聯(lián)的呢?

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {     
   //根據(jù)sqlSessionFactory從當(dāng)前線程對應(yīng)的資源map中獲取SqlSessionHolder,當(dāng)sqlSessionFactory創(chuàng)建了sqlSession,就會在事務(wù)管理器中添加一對映射:key為sqlSessionFactory,value為SqlSessionHolder,該類保存sqlSession及執(zhí)行方式 
   SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory); 
   //如果holder不為空,且和當(dāng)前事務(wù)同步 
   if (holder != null && holder.isSynchronizedWithTransaction()) { 
     //hodler保存的執(zhí)行類型和獲取SqlSession的執(zhí)行類型不一致,就會拋出異常,也就是說在同一個事務(wù)中,執(zhí)行類型不能變化,原因就是同一個事務(wù)中同一個sqlSessionFactory創(chuàng)建的sqlSession會被重用 
     if (holder.getExecutorType() != executorType) { 
       throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction"); 
     } 
     //增加該holder,也就是同一事務(wù)中同一個sqlSessionFactory創(chuàng)建的唯一sqlSession,其引用數(shù)增加,被使用的次數(shù)增加 
     holder.requested(); 
     //返回sqlSession 
     return holder.getSqlSession(); 
   } 
   //如果找不到,則根據(jù)執(zhí)行類型構(gòu)造一個新的sqlSession 
   SqlSession session = sessionFactory.openSession(executorType); 
   //判斷同步是否激活,只要SpringTX被激活,就是true 
   if (isSynchronizationActive()) { 
     //加載環(huán)境變量,判斷注冊的事務(wù)管理器是否是SpringManagedTransaction,也就是Spring管理事務(wù) 
     Environment environment = sessionFactory.getConfiguration().getEnvironment(); 
     if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) { 
       //如果是,則將sqlSession加載進(jìn)事務(wù)管理的本地線程緩存中 
       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)); 
       //設(shè)置當(dāng)前holder和當(dāng)前事務(wù)同步 
       holder.setSynchronizedWithTransaction(true); 
       //增加引用數(shù) 
       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) { 
   //其實(shí)下面就是判斷session是否被Spring事務(wù)管理,如果管理就會得到holder  
   SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory); 
   if ((holder != null) && (holder.getSqlSession() == session)) { 
     //這里釋放的作用,不是關(guān)閉,只是減少一下引用數(shù),因?yàn)楹竺婵赡軙粡?fù)用 
     holder.released(); 
   } else { 
     //如果不是被spring管理,那么就不會被Spring去關(guān)閉回收,就需要自己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;
    }
}

? 著作權(quán)歸作者所有

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容