在Mybatis-spring中由於默認Autowired導致不能配置多個數據源的問題分析及解決


在使用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悲劇來進行注解,那么這個注解是怎么工作的呢。以下是它的工作方式

 

  1. 先根據beanName進行注解,那么這個beanName是指什么呢,即參數的名稱。
  2. 如果未找到,則嘗試根據類型尋找所有這個類型的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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM