1. 程式人生 > >Spring 多資料來源事務配置問題

Spring 多資料來源事務配置問題

在SpringSide 3 中,白衣提供的預先配置好的環境非常有利於使用者進行快速開發,但是同時也會為擴充套件帶來一些困難。最直接的例子就是關於在專案中使用多個數據源的問題,似乎 很難搞。在上一篇中,我探討了SpringSide 3 中的資料訪問層,在這一篇中,我立志要解決多資料來源配置的難題,我的思路是這樣的:

第一步、測試能否配置多個DataSource
第二步、測試能否配置多個SessionFactory
第三步、測試能否配置多個TransactionManager
第四步、測試能否使用多個TransactionManager,也就是看能否配置多個

基本上到第四步就應該走不通了,因為Spring中似乎不能配置多個,而且@transactional註解也無法讓使用者選擇具體使用哪個TransactionManager。也就是說,在SpringSide的應用中,不能讓不同的資料來源分別屬於不同的事務管理器,多資料來源只能使用分散式事務管理器,那麼測試思路繼續如下進行:

第五步、測試能否配置JTATransactionManager

如果到這一步,專案還能順利在Tomcat中執行的話,我們就算大功告成了。但我總認為事情不會那麼順利,我總覺得JTATransactionManager需要應用伺服器的支援,而且需要和JNDI配合使用,具體是不是這樣,那只有等測試後才知道。如果被我不幸言中,那麼進行下一步:

第六步、更換Tomcat為GlassFish,更換JDBC的DataSource為JNDI查詢的DataSource,然後配置JTATransactionManager

下面測試開始,先假設場景,還是繼續用上一篇中提到的簡單的文章釋出系統,假設該系統執行一段時間後非常火爆,單靠一臺伺服器已經無法支援巨大的使用者數, 這時候,站長想到了把資料進行水平劃分,於是,需要建立一個索引資料庫,該索引資料庫需儲存每一篇文章的Subject及其內容所在的Web伺服器,而每 一個Web伺服器上執行的專案,需要同時訪問索引資料庫和內容資料庫。所以,需要建立索引資料庫,如下:
create database puretext_index;

use puretext_index;

create table articles(
id int primary key auto_increment,
subject varchar(256),
webserver varchar(30)
);

第一步測試,配置多個DataSource,配置檔案如下:
application.properties:
jdbc.urlContent=jdbc:mysql://localhost:3306/PureText useUnicode=true&characterEncoding=utf8
jdbc.urlIndex=jdbc:mysql://localhost:3306/PureText_Index useUnicode=true&characterEncoding=utf8

applicationContext.xml:
< xml version="1.0" encoding="UTF-8" >
Spring公共配置檔案 classpath*:/application.propertiesclasspath*:/application.local.propertiesorg.hibernate.dialect.MySQL5InnoDBDialect${hibernate.show_sql}${hibernate.format_sql}org.hibernate.cache.EhCacheProvider
                ${hibernate.ehcache_config_file}!-->!--

這個時候執行上一篇文章中寫好的單元測試DaoTest.java,結果發現還是會出錯,錯誤原因如下:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cn.puretext.unit.service.DaoTest': Autowiring of methods failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests.setDataSource(javax.sql.DataSource); nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [javax.sql.DataSource] is defined: expected single matching bean but found 2: [dataSourceContent, dataSourceIndex]

經過分析,發現是測試類的基類需要注入DataSource,而現在配置了多個DataSource,所以Spring不知道哪個DataSource匹配了,所以需要改寫DaoTest.java,如下:
package cn.puretext.unit.service;

import java.util.List;

import javax.annotation.Resource;
import javax.sql.DataSource;

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springside.modules.orm.Page;
import org.springside.modules.test.junit4.SpringTxTestCase;

import cn.puretext.dao.ArticleDao;
import cn.puretext.entity.web.Article;

public class DaoTest extends SpringTxTestCase {
    @Autowired
    private ArticleDao articleDao;
    
    public ArticleDao getArticleDao() {
        return articleDao;
    }

    public void setArticleDao(ArticleDao articleDao) {
        this.articleDao = articleDao;
    }

    @Override
    @Resource(name = "dataSourceContent")
    public void setDataSource(DataSource dataSource) {
        // TODO Auto-generated method stub
        super.setDataSource(dataSource);
    }

    @Test
    public void addArticle() {
        Article article = new Article();
        article.setSubject("article test");
        article.setContent("article test");
        articleDao.save(article);
    }
    
    @Test
    public void pageQuery() {
        Page page = new Page();
        page.setPageSize(10);
        page.setPageNo(2);
        page = articleDao.getAll(page);
        List articles = page.getResult();
    }
}


改變的內容主要為重寫了基類中的setDataSource方法,並使用@Resource註解指定使用的DataSource為dataSourceContent。經過修改後,單元測試成功執行。

第二步,配置多個SessionFactory,配置檔案如下:
< xml version="1.0" encoding="UTF-8" >
Spring公共配置檔案 classpath*:/application.propertiesclasspath*:/application.local.propertiesorg.hibernate.dialect.MySQL5InnoDBDialect${hibernate.show_sql}${hibernate.format_sql}org.hibernate.cache.EhCacheProvider
                ${hibernate.ehcache_config_file}org.hibernate.dialect.MySQL5InnoDBDialect${hibernate.show_sql}${hibernate.format_sql}org.hibernate.cache.EhCacheProvider
                ${hibernate.ehcache_config_file}!-->!--

執行單元測試,報錯,錯誤程式碼如下:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cn.puretext.unit.service.DaoTest': Autowiring of fields failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private cn.puretext.dao.ArticleDao cn.puretext.unit.service.DaoTest.articleDao; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'articleDao': Autowiring of methods failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void org.springside.modules.orm.hibernate.SimpleHibernateDao.setSessionFactory(org.hibernate.SessionFactory); nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.hibernate.SessionFactory] is defined: expected single matching bean but found 2: [sessionFactoryContent, sessionFactoryIndex]

這和上面出現的錯誤是異曲同工的,只不過這次是ArticleDao類裡面不知道注入哪一個SessionFactory,因此,需要修改ArticleDao類,重寫setSessionFactory方法,並用@Resource註解指定,如下:
package cn.puretext.dao;


import javax.annotation.Resource;

import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springside.modules.orm.hibernate.HibernateDao;

import cn.puretext.entity.web.Article;

@Repository
public class ArticleDao extends HibernateDao {

    @Override
    @Resource(name = "sessionFactoryContent")
    public void setSessionFactory(SessionFactory sessionFactory) {
        // TODO Auto-generated method stub
        super.setSessionFactory(sessionFactory);
    }

}
,>

執行單元測試,成功。

第三步、配置多個TransactionManager,如下:
< xml version="1.0" encoding="UTF-8" >
Spring公共配置檔案 classpath*:/application.propertiesclasspath*:/application.local.propertiesorg.hibernate.dialect.MySQL5InnoDBDialect${hibernate.show_sql}${hibernate.format_sql}org.hibernate.cache.EhCacheProvider
                ${hibernate.ehcache_config_file}org.hibernate.dialect.MySQL5InnoDBDialect${hibernate.show_sql}${hibernate.format_sql}org.hibernate.cache.EhCacheProvider
                ${hibernate.ehcache_config_file}!-->!--

這個時候執行還是會出錯,出錯的原因為 org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'transactionManager' is defined,因為該出錯資訊很短,我也難以找出究竟是哪個地方需要名為“transactionManager”的事務管理器 ,改個名字都不行,看來Spring的自動注入有時候也錯綜複雜害人不淺。不過,如果把上面的其中一個名字改成“transactionManger”, 另外一個名字不改,執行是成功的,如下:

    

這個時候得出結論是,可以配置多個TransactionManager,但是必須有一個的名字是transactionManager。

第四步、配置多個,如下:

執行測試,天啦,竟然成功了。和我之前預料的完全不一樣,居然在一個配置檔案中配置多個一點 問題都沒有。那麼在使用@Transactional的地方,它真的能夠選擇正確的事務管理器嗎?我不得不寫更多的程式碼來進行測試。那就針對索引資料庫中 的表寫一個Entity,寫一個Dao測試一下吧。

程式碼如下:
package cn.puretext.entity.web;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import cn.puretext.entity.IdEntity;

@Entity
// 表名與類名不相同時重新定義表名.
@Table(name = "articles")
// 預設的快取策略.
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class ArticleIndex extends IdEntity {
    private String subject;
    private String webServer;

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }
    @Column(name = "webserver")
    public String getWebServer() {
        return webServer;
    }

    public void setWebServer(String webServer) {
        this.webServer = webServer;
    }
}

package cn.puretext.dao;

import javax.annotation.Resource;

import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springside.modules.orm.hibernate.HibernateDao;

import cn.puretext.entity.web.ArticleIndex;

@Repository
public class ArticleIndexDao extends HibernateDao {
    @Override
    @Resource(name = "sessionFactoryIndex")
    public void setSessionFactory(SessionFactory sessionFactory) {
        // TODO Auto-generated method stub
        super.setSessionFactory(sessionFactory);
    }
},>

package cn.puretext.unit.service;

import java.util.List;

import javax.annotation.Resource;
import javax.sql.DataSource;

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springside.modules.orm.Page;
import org.springside.modules.test.junit4.SpringTxTestCase;

import cn.puretext.dao.ArticleDao;
import cn.puretext.dao.ArticleIndexDao;
import cn.puretext.entity.web.Article;
import cn.puretext.entity.web.ArticleIndex;
import cn.puretext.service.ServiceException;

public class DaoTest extends SpringTxTestCase {
    @Autowired
    private ArticleDao articleDao;
    @Autowired
    private ArticleIndexDao articleIndexDao;
    
    public void setArticleIndexDao(ArticleIndexDao articleIndexDao) {
        this.articleIndexDao = articleIndexDao;
    }

    public void setArticleDao(ArticleDao articleDao) {
        this.articleDao = articleDao;
    }

    @Override
    @Resource(name = "dataSourceContent")
    public void setDataSource(DataSource dataSource) {
        // TODO Auto-generated method stub
        super.setDataSource(dataSource);
    }

    @Test
    @Transactional
    public void addArticle() {
        Article article = new Article();
        article.setSubject("article test");
        article.setContent("article test");
        articleDao.save(article);
    }

    @Test
    @Transactional
    public void pageQuery() {
        Page page = new Page();
        page.setPageSize(10);
        page.setPageNo(2);
        page = articleDao.getAll(page);
        List articles = page.getResult();
    }
    
    @Test
    @Transactional
    public void addIndex() {
        ArticleIndex articleIndex = new ArticleIndex();
        articleIndex.setSubject("test");
        articleIndex.setWebServer("www001");
        articleIndexDao.save(articleIndex);
    }
    
    @Test
    @Transactional
    public void addArticleAndAddIndex() {
        addArticle();
        addIndex();
        throw new ServiceException("測試事務回滾");
    }
}

執行測試,結果還是成功的。到目前,發現在一個專案中使用多個TransactionManager可以正常執行,但是有兩個問題需要考慮:
1、為什麼必須得有一個TransactionManager名字為transactionManager?
2、這兩個TransactionManager真的能正常工作嗎?
3、OpenSessionInView的問題怎麼解決?

以上的三個問題在單元測試中是不能找出答案的,我只好再去寫Action層的程式碼,期望能夠從中得到線索。經過一天艱苦的努力,終於真相大白:
1、並不是必須有一個TransactionManager的名字為transactionMananger,這只是單元測試在搞鬼,在真實的Web環境 中,無論兩個TransactionManager取什麼名字都可以,執行不會報錯。所以這個答案很明確,是因為單元測試的基類需要一個名為 transactionMananger的事務管理器。
2、在單元測試中,只能測試Dao類和Entity類能否正常工作,但是由於單元測試結束後事務會自動回滾,不會把資料寫入到資料庫中,所以沒有辦法確定 兩個TransactionManager能否正常工作。在真實的Web環境中,問題很快就浮出水面,只有一個數據庫中有資料,另外一個數據庫中沒有,經 過調整的位置並對比分析,發現只有放在前面的TransactionMananger的事務 能夠正常提交,放在後面的TransactionManager的事務不能提交,所以永遠只有一個數據庫裡面有資料。
3、如果早一點脫離單元測試而進入真實的Web環境,就會早一點發現OpenSessionInViewFilter的問題,因為只要配置多個 SessionFactory,執行的時候OpenSessionInViewFilter就會報錯。為了解決這個問題,我只能去閱讀 OpenSessionInViewFilter的原始碼,發現它在將Session繫結到執行緒的時候用的是Map,而且使用 SessionFactory作為Map的key,這就說明線上程中繫結多個Session不會衝突,也進一步說明可以在web.xml中配置多個 OpenSessionInViewFilter。而我也正是通過配置多個OpenSessionInViewFilter來解決問題的。我的 web.xml檔案如下:
< xml version="1.0" encoding="UTF-8" >
PureTextcontextConfigLocationclasspath*:/applicationContext*.xmlencodingFilterorg.springframework.web.filter.CharacterEncodingFilterencodingUTF-8forceEncodingtruehibernateOpenSessionInViewFilterContentorg.springside.modules.orm.hibernate.OpenSessionInViewFilterexcludeSuffixsjs,css,jpg,gifsessionFactoryBeanNamesessionFactoryContenthibernateOpenSessionInViewFilterIndexorg.springside.modules.orm.hibernate.OpenSessionInViewFilterexcludeSuffixsjs,css,jpg,gifsessionFactoryBeanNamesessionFactoryIndexspringSecurityFilterChainorg.springframework.web.filter.DelegatingFilterProxystruts2Filterorg.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilterencodingFilter/*springSecurityFilterChain/*hibernateOpenSessionInViewFilterContent/*hibernateOpenSessionInViewFilterIndex/*struts2Filter/*org.springframework.web.context.ContextLoaderListenerorg.springframework.web.util.IntrospectorCleanupListener20java.lang.Throwable/common/500.jsp500/common/500.jsp404/common/404.jsp403/common/403.jsp

經過上面的分析,發現使用多個TransactionManager是不可行的(這個時候我在想,也許不使用Annotation就可以使用多個 TransactionMananger吧,畢竟Spring的AOP應該是可以把不同的TransactionManager插入到不同的類和方法中, 但是誰願意走回頭路呢?畢竟都已經是@Transactional的年代了),雖然執行不會報錯,但是隻有一個TransactionManager的事 務能夠正常提交。所以測試進入下一步:

第五步、使用JTATransactionManager

簡單地修改配置檔案,使用JTATransactionManager做為事務管理器,配置檔案我就不列出來了,執行,結果抱錯,錯誤資訊如下:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name '_filterChainProxy': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '_filterChainList': Cannot create inner bean '(inner bean)' of type [org.springframework.security.config.OrderedFilterBeanDefinitionDecorator$OrderedFilterDecorator] while setting bean property 'filters' with key [10]; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)': Cannot resolve reference to bean 'filterSecurityInterceptor' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'filterSecurityInterceptor' defined in file [D:Temp1-PureTextWEB-INFclassesapplicationContext-security.xml]: Cannot resolve reference to bean 'databaseDefinitionSource' while setting bean property 'objectDefinitionSource'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'databaseDefinitionSource': FactoryBean threw exception on object creation; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.transaction.interceptor.TransactionInterceptor#0': Cannot resolve reference to bean 'transactionManager' while setting bean property 'transactionManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManager' defined in file [D:Temp1-PureTextWEB-INFclassesapplicationContext.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: No JTA UserTransaction available - specify either 'userTransaction' or 'userTransactionName' or 'transactionManager' or 'transactionManagerName'

通過分析,發現其中最關鍵的一句是No JTA UserTransaction available,看來,我們只能進入到第六步,使用GlassFish了。

第六步、將專案部署到GlassFish中

將專案簡單地部署到GlassFish中之後,專案可以成功執行,沒有報錯,說明JTA UserTransaction問題解決了,但是檢查資料庫卻發現依然沒有資料,看來JTATransactionManager不僅要和應用伺服器配合 使用,還要和JNDI資料來源一起使用。將資料來源的配置修改為JNDI後,問題解決。下面是我的配置檔案:
< xml version="1.0" encoding="UTF-8" >
Spring公共配置檔案 classpath*:/application.propertiesclasspath*:/application.local.propertiesorg.hibernate.dialect.MySQL5InnoDBDialect${hibernate.show_sql}${hibernate.format_sql}org.hibernate.cache.EhCacheProvider
                ${hibernate.ehcache_config_file}org.hibernate.dialect.MySQL5InnoDBDialect${hibernate.show_sql}${hibernate.format_sql}org.hibernate.cache.EhCacheProvider
                ${hibernate.ehcache_config_file}!-->

最後,我得出的結論是:要想使用多個數據庫,就必須使用JTATransactionMananger,必須使用GlassFish等應用伺服器而不是Tomcat,必須使用JNDI來管理dataSource。

如果一定要使用Tomcat呢?

這確實是一個難題,但是並不代表著沒有解決辦法。經過廣泛的Google一番之後,終於發現了一個好東東,那就是JOTM,它的全稱就是Java Open Transaction Mananger,它的作用就是可以單獨提供JTA事務管理的功能,不需要應用伺服器。JOTM的使用方法有兩種,一種就是把它配置到專案中,和 Spring結合起來使用,另外一種就是把它配置到Tomcat中,這時,Tomcat搖身一變就成了和GlassFish一樣的能夠提供JTA功能的服 務器了。

JOTM的官方網站為http://jotm.ow2.org,這是它的新網站,舊網站為http://jotm.objectweb.org。

我選擇了把JOTM 2.0.11整合到Tomcat中的方法進行了測試,結果發現還是不能夠正常執行,我使用的是JOTM2.0.11,Tomcat 6.0.20,JKD 6 Update10。看來還得繼續折騰下去了。

另外一個開源的JTA事務管理器是Atomikos,它供了事務管理和連線池,不需要應用伺服器支援,其官方網站為http://www.atomikos.com/。有興趣的朋友可以試試。