在使用Mybatis中,通常使用接口來表示一個Sql Mapper的接口以及相對應的xml實現,而在spring的配置文件中,通常會使用MapperScannerConfigurer來達到批量掃描以及簡化spring bean接口配置的目的,以直接讓mybatis的各個接口直接成為spring的bean組件。那么,一個通常的spring配置文件如下所示:
<bean id="datasource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource"/> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="datasource"/> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="datasource"/> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" p:basePackage="mapper"/>
上面的配置分別對應於一個基本的mybatis最需要的信息。值得需要注意的是,在配置MapperScannerConfigurer時,這里並沒有指定sqlSessionFactoryName以及sqlTemplateName。在沒有指定的情況下,spring就會指定默認的查找規則進行查詢,如分別查找到默認的sqlSessionFactory實現和sqlSessionTemplate實現,並注入到MapperFactoryBean中。
這種配置方式,在一個數據源時,沒有問題,但是在如果存在多個數據源時,上面的配置就存在問題了。在多個數據源時,如果配置不正確,或者配置的步驟不正確,將直接產生莫名奇妙的問題。而這個問題的產生,不在於開發人員,即不在於程序員本身,而在於spring,或來自於mybatis-spring,在其內部畫蛇添足的注解,將導致整個多數據源配置完全不能工作。
在mybatis-spring內部,即在配置MapperScannerConfigurer時,手冊告訴我們,可以不配置sqlSessionFactoryName和sqlTemplateName,因為不配置的情況下spring會自動進行注入。但是,手冊沒有告訴我們它是怎么來注入的,如果你仔細查看源代碼,你會發現,它使用了讓人悲劇的@Autowired悲劇來進行注解,那么這個注解是怎么工作的呢。以下是它的工作方式
- 先根據beanName進行注解,那么這個beanName是指什么呢,即參數的名稱。
- 如果未找到,則嘗試根據類型尋找所有這個類型的bean信息。如果查找到多於1個,則會報NotUnique異常;但如果沒有查找,則會根據Autowired中的required屬性進行處理,如果required為false,則不進行注入,否則報異常信息。
問題就在於它的工作方式,當出現多個同類型的bean時,它並不是總是報異常。因此,當第一個步驟滿足時,它總會優先拿到滿足條件的bean,而並不會因為有同類型的多個bean而報異常。那么,我們來看用於實現mybaits-spring中bean的代理工廠,即MapperFactoryBean的那2個屬性定義:
@Autowired(required = false) public final void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { if (!this.externalSqlSession) { this.sqlSession = new SqlSessionTemplate(sqlSessionFactory); } } @Autowired(required = false) public final void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) { this.sqlSession = sqlSessionTemplate; this.externalSqlSession = true; }
上面的定義在MapperFactoryBean的父類SqlSessionDaoSupport中。仔細看這兩個參數名,sqlSessionFactory和sqlSessionTemplate,是不是很熟悉。對,它就是我們在applicationContext.xml中定義的bean的默認id值。那么,當出現多個數據源時,它是怎么工作的呢。我們以另一個配置為例。如下所示:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" p:annotationClass="com.m_ylf.study.java.mybatis.Mybatis" p:basePackage="mapper2" p:sqlSessionFactoryBeanName="sessionFactory2" />
根據mybatis-spring的配置建議,我們不能即配置sqlSessionFactoryBeanName和sqlSessionTemplate,只能配置其中一個即可。好吧,我們配置了sqlSessionFactoryBeanName。如果你認為,它能夠像你期望地那樣工作,你肯定會失望了。因為最終的結果會是:
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'test.tc' doesn't exist
如果,你運行在oracle上,則是類似某用戶下不存在指定的表的錯誤,為什么會產生這個錯誤。而且,這個錯誤發生在運行期,而不是在spring的加載過程中。你絕對想不到,因為經過Autowired的處理,MapperFactoryBean即會運行setSqlSessionFactory方法,也會運行setSqlSessionTemplate方法。而更讓人郁悶的是,你設置的sqlSessionFactoryBeanName根本沒有用。這來自於內部,自以為是的externalSqlSession變量。當此變量為true時,setSqlSessionFactory方法會直接返回。因為,setSqlSessionTemplate會比屬性注入的applyPropertyValues更先運行,這一切是不是很讓人郁悶。
那么,你會想,那么我們使用sqlSessionTemplateName這個變量吧,好吧。這個變量是正常工作的,這也來自於內部的externalSqlSession變量。你不要以為setSqlSessionFactory不會運行,而是因為這個變量讓setSqlSessionFactory不能改變值而已。
那么,再對應於使用sqlSessionFactoryName所產生的問題,問題就在於在注入第二個數據源的信息時,並沒有使用對應第二個數據源的信息,而是根據默認的查詢策略直接找到了默認的第一個數據源信息。本來應該文號第二個數據源的sql信息,改去訪問第一個數據源,肯定找不到任何信息,因為想要訪問的數據表都不存在。
在這個情況中,不要認為,我們沒有把默認的第一個數據源的信息命名為sqlSessionFactoryName1和sqlSessionTemplate1。在大多數的項目中,不會從一開始就知道會有多個數據源的,在默認只有一個數據源的情況下,不會有人將bean id命名為那樣的奇怪名稱。
最后,我們再來看下,讓mybatis-spring報警告的同時配置sqlSessionFactoryName和sqlSessionTemplateName的情況。在這個情況下,mybaits-MapperScannerConfigurer會很好心的給你報一個警告,如下:
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
然后,它不知道,這個並不是警告,而是根本spring容器就不能啟動成功,即這樣的配置會讓整個spring容器啟動失敗。為什么,這要規結於內部奇怪的代碼,如下所示:
//第一部分,探測到sqlSessionFactoryName if (StringUtils.hasLength(MapperScannerConfigurer.this.sqlSessionFactoryBeanName)) { definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(MapperScannerConfigurer.this.sqlSessionFactoryBeanName)); } //第二部分,探測到sqlSessionTemplateName if (StringUtils.hasLength(MapperScannerConfigurer.this.sqlSessionTemplateBeanName)) { } definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(MapperScannerConfigurer.this.sqlSessionTemplateBeanName)); definition.getPropertyValues().add("sqlSessionFactory", null); }
問題,在於后面的definition.getPropertyValues().add("sqlSessionFactory", null);代碼,這句話是說,行,我們會調用setSqlSessionFactory(null)方法。看起來沒有錯誤,因為我們在調用了setSqlSessionTemplate之后,再調用setSqlSessionFactory(null)不會出錯,因為后面的調用無效嘛。但是,問題關鍵在於:setSqlSessionFactory(null)會比setSqlSessionTemplate更先調用,在先調用的情況下,會產生NullPointerException異常。這個異常在整個spring的錯誤堆棧中找不到,你根本查不到怎么會產生這個異常,而且spring報的異常如下所示:
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tcMapper' defined in file xxxxxxxx: Error setting property values; nested exception is org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are: PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'sqlSessionFactory' threw exception; nested exception is java.lang.NullPointerException
錯誤原因在於在調用setSqlSessionFactory方法時,會將sqlSessionFactory傳遞給sqlSessionTemplate以用於創建新的sqlSessionTemplate,而在sqlSessionTemplate的構造方法中,會調用sqlSessionFactory.getConfiguration().getDefaultExecutorType()方法,這就是NPE的來源。
綜全所述,如果想要成功運行,一是修改bean命名,二是修改MapperScannerConfigurer配置中的屬性一定為sqlSessionTemplateBeanName而不是sqlSessionFactoryBeanName。
本文使用的mybatis-spring為1.1.1版本,mybatis版本為3.1.1.
本文地址:http://www.iflym.com/index.php/code/201211010001.html