單獨使用mybatis是有很多限制的(比如無法實現(xiàn)跨越多個session的事務),而且很多業(yè)務系統(tǒng)本來就是使用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動態(tài)掃描添加mapper不用像ibatis,用一個還要蛋疼滴添加一個include
-->
<property name="mapperLocations" value="classpath*:/com/tx/demo/**/*SqlMap.xml" />
<!--
typeHandlersPackage: 由于mybatis默認入?yún)⑷绻麨榭障湍罚譀]有指定jdbcType時會拋出異常,在這里通過配置一些默認的類型空值插入的handle,以便處理mybatis的默認類型為空的情況雹姊。
例如NullAbleStringTypeHandle通過實現(xiàn)當String字符串中為null是調(diào)用ps.setString(i,null)其他常用類型雷同。
-->
<property name="typeHandlersPackage" value="com.tx.core.mybatis.handler"></property>
<!--
failFast:開啟后將在啟動時檢查設定的parameterMap,resultMap是否存在衡楞,是否合法吱雏。個人建議設置為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的異常轉換,通過配置該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事務配置#
<!-- 自動掃描業(yè)務包 -->
<context:component-scan base-package="com.xxx.service" />
<!-- 數(shù)據(jù)源 -->
<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"/>
或:
<!-- 自動掃描業(yè)務包 -->
<context:component-scan base-package="com.xxx.service" />
<!-- 數(shù)據(jù)源 -->
<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>
<!--創(chuàng)建dao bean(只需提供接口不需提供實現(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是有特殊用途的祈惶,當spring將工廠bean注入到其他bean里時雕旨,它不是注入工廠bean本身而是調(diào)用bean的getObject方法。我們接下來就看看這個getObjec方法干了些什么:
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
看到這里大家應該就很明白了捧请,這個方法和我們之前單獨使用Mybatis的方式是一樣的奸腺,都是先獲取一個Sqlsession對象,然后再從Sqlsession里獲取Mapper對象(再次強調(diào)Mapper是一個代理對象血久,它代理的是mapperInterface接口突照,而這個接口是用戶提供的dao接口)。自然氧吐,最終注入到業(yè)務層就是這個Mapper對象讹蘑。
實際的項目一般來說不止一個Dao,如果你有多個Dao那就按照上面的配置依次配置即可筑舅。
4 如何使用批量更新#
前一節(jié)講了如何注入一個mapper對象到業(yè)務層座慰,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>
跟上一節(jié)的mapper配置不同的是谜慌,這里不需要配置sqlSessionFactory屬性然想,只需要配置sqlSessionTemplate(sqlSessionFactory屬性在模板里已經(jīng)配置好了)。
由于在3.1.1升級后欣范,可直接通過BatchExcutor實現(xiàn)具體的批量執(zhí)行变泄。在BatchExcutor中會重用上一次相同的PreparedStatement。
5 通過自動掃描簡化mapper的配置#
前面的章節(jié)可以看到恼琼,我們的dao需要一個一個的配置在配置文件中妨蛹,如果有很多個dao的話配置文件就會非常大,這樣管理起來就會比較痛苦晴竞。幸好mybatis團隊也意識到了這點蛙卤,他們利用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的技術我就不多講了颓鲜,感興趣且對spring原理比較了解的可以去看下它的源碼表窘。我們重點看一下它的三個屬性:
basePackage:掃描器開始掃描的基礎包名,支持嵌套掃描甜滨;
sqlSessionTemplateBeanName:前文提到的模板bean的名稱乐严;
markerInterface:基于接口的過濾器,實現(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是用于指定一個接口的甫恩,當指定了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來建立對應的SqlSessionFactory,進而獲取到對應的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文件存放的位置撞蜂,當我們的Mapper文件跟對應的Mapper接口處于同一位置的時候可以不用指定該屬性的值。
configLocation:用于指定Mybatis的配置文件位置侥袜。如果指定了該屬性蝌诡,那么會以該配置文件的內(nèi)容作為配置信息構建對應的SqlSessionFactoryBuilder,但是后續(xù)屬性指定的內(nèi)容會覆蓋該配置文件里面指定的對應內(nèi)容枫吧。
typeAliasesPackage:它一般對應我們的實體類所在的包浦旱,這個時候會自動取對應包中不包括包名的簡單類名作為包括包名的別名。多個package之間可以用逗號或者分號等來進行分隔九杂。
typeAliases:數(shù)組類型颁湖,用來指定別名的宣蠕。指定了這個屬性后,Mybatis會把這個類型的短名稱作為這個類型的別名甥捺,前提是該類上沒有標注@Alias注解抢蚀,否則將使用該注解對應的值作為此種類型的別名。
<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會自動把該包下面的類注冊為對應的TypeHandler屋休。多個package之間可以用逗號或者分號等來進行分隔。
typeHandlers:數(shù)組類型备韧,表示TypeHandler劫樟。
接下來就是在Spring的applicationContext文件中定義我們想要的Mapper對象對應的MapperFactoryBean了。通過MapperFactoryBean可以獲取到我們想要的Mapper對象织堂。MapperFactoryBean實現(xiàn)了Spring的FactoryBean接口毅哗,所以MapperFactoryBean是通過FactoryBean接口中定義的getObject方法來獲取對應的Mapper對象的。在定義一個MapperFactoryBean的時候有兩個屬性需要我們注入捧挺,一個是Mybatis-Spring用來生成實現(xiàn)了SqlSession接口的SqlSessionTemplate對象的sqlSessionFactory虑绵;另一個就是我們所要返回的對應的Mapper接口了。
定義好相應Mapper接口對應的MapperFactoryBean之后闽烙,我們就可以把我們對應的Mapper接口注入到由Spring管理的bean對象中了翅睛,比如Service bean對象。這樣當我們需要使用到相應的Mapper接口時黑竞,MapperFactoryBean會從它的getObject方法中獲取對應的Mapper接口捕发,而getObject內(nèi)部還是通過我們注入的屬性調(diào)用SqlSession接口的getMapper(Mapper接口)方法來返回對應的Mapper接口的。這樣就通過把SqlSessionFactory和相應的Mapper接口交給Spring管理實現(xiàn)了Mybatis跟Spring的整合很魂。
如果想使用MapperScannerConfigurer扎酷,想要了解該類的作用,就得先了解MapperFactoryBean遏匆。
6.2 MapperFactoryBean##
MapperFactoryBean的出現(xiàn)為了代替手工使用SqlSessionDaoSupport或SqlSessionTemplate編寫數(shù)據(jù)訪問對象(DAO)的代碼法挨,使用動態(tài)代理實現(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實例凡纳,然后注入這個接口和sqlSessionFactory(mybatis中提供的SqlSessionFactory接口,MapperFactoryBean會使用SqlSessionFactory創(chuàng)建SqlSession)這兩個屬性帝蒿。
之后想使用這個UserMapper接口的話荐糜,直接通過spring注入這個bean,然后就可以直接使用了,spring內(nèi)部會創(chuàng)建一個這個接口的動態(tài)代理暴氏。
當發(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對應的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:
這個屬性已經(jīng)廢棄
赂蠢。當我們使用了多個數(shù)據(jù)源的時候我們就需要通過sqlSessionFactory來指定在注冊MapperFactoryBean的時候需要使用的SqlSessionFactory,因為在沒有指定sqlSessionFactory的時候辨泳,會以Autowired的方式自動注入一個虱岂。換言之當我們只使用一個數(shù)據(jù)源的時候,即只定義了一個SqlSessionFactory的時候我們就可以不給MapperScannerConfigurer指定SqlSessionFactory菠红。sqlSessionFactoryBeanName:它的功能跟sqlSessionFactory是一樣的量瓜,只是它指定的是定義好的SqlSessionFactory對應的bean名稱。
sqlSessionTemplate:
這個屬性已經(jīng)廢棄
途乃。它的功能也是相當于sqlSessionFactory的绍傲,因為就像前面說的那樣,MapperFactoryBean最終還是使用的SqlSession的getMapper方法取的對應的Mapper對象。當定義有多個SqlSessionTemplate的時候才需要指定它烫饼。對于一個MapperFactoryBean來說SqlSessionFactory和SqlSessionTemplate只需要其中一個就可以了猎塞,當兩者都指定了的時候,SqlSessionFactory會被忽略杠纵。sqlSessionTemplateBeanName:指定需要使用的sqlSessionTemplate對應的bean名稱荠耽。
注意:由于使用sqlSessionFactory和sqlSessionTemplate屬性時會使一些內(nèi)容在PropertyPlaceholderConfigurer之前加載,導致在配置文件中使用到的外部屬性信息無法被及時替換而出錯比藻,因此官方現(xiàn)在新的Mybatis-Spring中已經(jīng)把sqlSessionFactory和sqlSessionTemplate屬性廢棄了铝量,推薦大家使用sqlSessionFactoryBeanName屬性和sqlSessionTemplateBeanName屬性。
我們先通過測試用例debug查看userDao的實現(xiàn)類到底是什么:
我們可以看到银亲,userDao是1個MapperProxy類的實例慢叨。看下MapperProxy的源碼务蝠,沒錯拍谐,實現(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實現(xiàn)了BeanDefinitionRegistryPostProcessor接口馏段,BeanDefinitionRegistryPostProcessor接口是一個可以修改spring工廠中已定義的bean的接口轩拨,該接口有個postProcessBeanDefinitionRegistry方法。
然后我們看下ClassPathMapperScanner中的關鍵是如何掃描對應package下的接口的院喜。
其實MapperScannerConfigurer的作用也就是將對應的接口的類型改造為MapperFactoryBean亡蓉,而這個MapperFactoryBean的屬性mapperInterface是原類型。MapperFactoryBean本文開頭已分析過喷舀。
所以最終我們還是要分析MapperFactoryBean的實現(xiàn)原理砍濒!
MapperFactoryBean繼承了SqlSessionDaoSupport類,SqlSessionDaoSupport類繼承DaoSupport抽象類元咙,DaoSupport抽象類實現(xiàn)了InitializingBean接口梯影,因此實例個MapperFactoryBean的時候,都會調(diào)用InitializingBean接口的afterPropertiesSet方法庶香。
DaoSupport的afterPropertiesSet方法:
MapperFactoryBean重寫了checkDaoConfig方法:
然后通過spring工廠拿對應的bean的時候:
這里的SqlSession是SqlSessionTemplate甲棍,SqlSessionTemplate的getMapper方法:
Configuration的getMapper方法,會使用MapperRegistry的getMapper方法:
MapperRegistry的getMapper方法:
MapperProxyFactory構造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為我們提供了一個實現(xiàn)了SqlSession接口的SqlSessionTemplate類陪白,它是線程安全的,可以被多個Dao同時使用膳灶。同時它還跟Spring的事務進行了關聯(lián)咱士,確保當前被使用的SqlSession是一個已經(jīng)和Spring的事務進行綁定了的立由。而且它還可以自己管理Session的提交和關閉。當使用了Spring的事務管理機制后序厉,SqlSession還可以跟著Spring的事務一起提交和回滾锐膜。
使用SqlSessionTemplate時我們可以在Spring的applicationContext配置文件中如下定義:
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
通過源碼我們何以看到 SqlSessionTemplate 實現(xiàn)了SqlSession接口,也就是說我們可以使用SqlSessionTemplate來代理以往的DefailtSqlSession完成對數(shù)據(jù)庫的操作弛房,但是DefailtSqlSession這個類不是線程安全的道盏,所以這個類不可以被設置成單例模式的。
如果是常規(guī)開發(fā)模式文捶,我們每次在使用DefailtSqlSession的時候都從SqlSessionFactory當中獲取一個就可以了荷逞。但是與Spring集成以后,Spring提供了一個全局唯一的SqlSessionTemplate示例 來完成DefailtSqlSession的功能粹排。
問題就是:無論是多個dao使用一個SqlSessionTemplate种远,還是一個dao使用一個SqlSessionTemplate,SqlSessionTemplate都是對應一個sqlSession恨搓,當多個web線程調(diào)用同一個dao時院促,它們使用的是同一個SqlSessionTemplate筏养,也就是同一個SqlSession斧抱,那么它是如何確保線程安全的呢?讓我們一起來分析一下渐溶。
- 首先辉浦,通過如下代碼創(chuàng)建代理類,表示創(chuàng)建SqlSessionFactory的代理類的實例茎辐,該代理類實現(xiàn)SqlSession接口宪郊,定義了方法攔截器,如果調(diào)用代理類實例中實現(xiàn)SqlSession接口定義的方法拖陆,該調(dià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());
}
- 核心代碼就在 SqlSessionInterceptor的invoke方法當中:
private class SqlSessionInterceptor implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//獲取SqlSession(這個SqlSession才是真正使用的弛槐,它不是線程安全的)
//這個方法可以根據(jù)Spring的事務上下文來獲取事務范圍內(nèi)的sqlSession
//一會我們在分析這個方法
final SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
//調(diào)用真實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);
}
//返回執(zhí)行結果
return result;
} catch (Throwable t) {
//如果出現(xiàn)異常則根據(jù)情況轉換后拋出
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
//它會根據(jù)當前的sqlSession是否在Spring的事務上下文當中來執(zhí)行具體的關閉動作
//如果sqlSession被Spring管理 則調(diào)用holder.released(); 使計數(shù)器-1
//否則才真正的關閉sqlSession
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
- 在上面的invoke方法當中使用了倆個工具方法分別是:
- SqlSessionUtils.getSqlSession(...)
- SqlSessionUtils.closeSqlSession(...)
那么這個倆個方法又是如何與Spring的事務進行關聯(lián)的呢?
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
//根據(jù)sqlSessionFactory從當前線程對應的資源map中獲取 SqlSessionHolder依啰,當sqlSessionFactory創(chuàng)建了sqlSession乎串,就會在事務管理器中添加一對映射:key為sqlSessionFactory,value為SqlSessionHolder速警,該類保存sqlSession及執(zhí)行方式
SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);
//如果holder不為空叹誉,且和當前事務同步
if (holder != null && holder.isSynchronizedWithTransaction()) {
//hodler保存的執(zhí)行類型和獲取SqlSession的執(zhí)行類型不一致,就會拋出異常闷旧,也就是說在同一個事務中长豁,執(zhí)行類型不能變化,原因就是同一個事務中同一個sqlSessionFactory創(chuàng)建的sqlSession會被重用
if (holder.getExecutorType() != executorType) {
throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
}
//增加該holder,也就是同一事務中同一個sqlSessionFactory創(chuàng)建的唯一sqlSession忙灼,其引用數(shù)增加匠襟,被使用的次數(shù)增加
holder.requested();
//返回sqlSession
return holder.getSqlSession();
}
//如果找不到,則根據(jù)執(zhí)行類型構造一個新的sqlSession
SqlSession session = sessionFactory.openSession(executorType);
//判斷同步是否激活,只要SpringTX被激活酸舍,就是true
if (isSynchronizationActive()) {
//加載環(huán)境變量机错,判斷注冊的事務管理器是否是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);
//增加引用數(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) {
//其實下面就是判斷session是否被Spring事務管理璧亮,如果管理就會得到holder
SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);
if ((holder != null) && (holder.getSqlSession() == session)) {
//這里釋放的作用萧诫,不是關閉,只是減少一下引用數(shù)枝嘶,因為后面可能會被復用
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;
}
}