关于Spring3 + Mybatis3整合时多数据源动态切换的问题
以前的项目经历中,基本上都是spring + hibernate + spring jdbc这种组合用的多。至于mybatis,也就这个项目才开始试用,闲话不多说,进入正题。
以前的这种框架组合中,动态数据源切换可谓已经非常成熟了,网上也有非常多的博客介绍,都是继承abstractroutingdatasource,重写determinecurrentlookupkey()方法。具体做法就不在此废话了。
所以当项目中碰到这个问题,我几乎想都没有想,就采用了这种做法,但是一测试,一点反应都没有。当时觉得不可能,于是断点,加log调试,发现determinecurrentlookupkey()根本没有调用。
为什么列? 这不可能啊。静下心来,仔细想想,才想到一个关键的问题: mybatis整合spring,而不是spring整合的mybatis! 直觉告诉我,问题就出在这里。
于是花时间去研究一下mybatis-spring.jar 这个包,发现有sqlsession这东西,很本能的就注意到了这一块,然后大致看一下他的一些实现类。于是就发现了他的实现类里面有一个内部类sqlsessioninterceptor(研究过程就不多说了,毕竟是个痛苦的过程)
好吧,这个类的作用列,就是产生sessionproxy。关键代码如下
final sqlsession sqlsession = getsqlsession( sqlsessiontemplate.this.sqlsessionfactory, sqlsessiontemplate.this.executortype, sqlsessiontemplate.this.exceptiontranslator);
这个sqlsessionfactory 我们就很眼熟啦,是我们在spring配置文件中配了的,是吧,也就是说这东西是直接从我们配置文件中读进来,但这东西,就关联了datasource。所以就想到,如果能把这东西,做到动态,那么数据源切换,也就动态了。
于是第一反应就是写了一个类,然后在里面定义一个map,用来存放多个sqlsessionfactory,并采用setter方法进行属性注入。
public class ejssqlsessiontemplate extends sqlsessiontemplate { private map<string, sqlsessionfactory> targetsqlsessionfactory = new hashmap<string, sqlsessionfactory>(); public void settargetsqlsessionfactory(map<string, sqlsessionfactory> targetsqlsessionfactory) { this.targetsqlsessionfactory = targetsqlsessionfactory; }
所以spring的配置文件就变成了这样:
<bean id="sqlsessiontemplate" class="com.ejushang.spider.datasource.ejssqlsessiontemplate"> <constructor-arg ref="sqlsessionfactory" /> <property name="targetsqlsessionfactory"> <map> <entry value-ref="sqlsessionfactory" key="spider"/> <entry value-ref="sqlsessionfactorytb" key="sysinfo"/> </map> </property> </bean> <bean id="mapperscannerconfigurer" class="org.mybatis.spring.mapper.mapperscannerconfigurer"> <property name="basepackage" value="com.foo.bar.**.mapper*" /> <property name="sqlsessiontemplatebeanname" value="sqlsessiontemplate"/> </bean>
那么这个思想是那里来的列? 当然就是借鉴了spring的动态数据源的思想啦,对比一下spring动态数据源的配置,看看是不是差不多?
然后重写了个关键的方法:
/** * 重写得到sqlsessionfactory的方法 * @return */ @override public sqlsessionfactory getsqlsessionfactory() { sqlsessionfactory targetsqlsessionfactory = this.targetsqlsessionfactory.get(sqlsessioncontextholder.getdatasourcekey()); if (targetsqlsessionfactory != null) { return targetsqlsessionfactory; } else if ( this.getsqlsessionfactory() != null) { return this.getsqlsessionfactory(); } throw new illegalargumentexception("sqlsessionfactory or targetsqlsessionfactory must set one at least"); }
而sqlsessioncontextholder就很简单,就是一个threadlocal的思想
public class sqlsessioncontextholder { private static final threadlocal<string> contextholder = new threadlocal<string>(); private static logger logger = loggerfactory.getlogger(sqlsessioncontextholder.class); public static void setsessionfactorykey(string datasourcekey) { contextholder.set(datasourcekey); } public static string getdatasourcekey() { string key = contextholder.get(); logger.info("当前线程thread:"+thread.currentthread().getname()+" 当前的数据源 key is "+ key); return key; } }
博主信心满满就开始测试了。。结果发现不行,切换不过来,始终都是绑定的是构造函数中的那个默认的sqlsessionfactory,当时因为看了一天源码,头也有点晕。其实为什么列?
看看我们产生sessionproxy的地方代码,他的sqlsessionfactory是直接从构造函数来拿的。而构造函数中的sqlsessionfactory在spring容器启动时,就已经初始化好了,这点也可以从我们spring配置文件中得到证实。
那这个问题,怎么解决列? 于是博主便想重写那个sqlsessioninterceptor。 擦,问题就来了,这个类是private的,没办法重写啊。于是博主又只能在自己的ejssqlsessiontemplate类中,也定义了一个内部类,把源码中的代码都copy过来,唯一不同的就是我不是读取构造函数中的sqlsessionfactory.而是每次都去调用 getsqlsessionfactory()方法。代码如下:
final sqlsession sqlsession = getsqlsession( ejssqlsessiontemplate.this.getsqlsessionfactory(), ejssqlsessiontemplate.this.getexecutortype(), ejssqlsessiontemplate.this.getpersistenceexceptiontranslator());
再试,发现还是不行,再找原因,又回归到了刚才那个问题。因为我没有重写sqlsessiontemplate的构造函数,而sqlsessionproxy是在构函数中初始化的,代码如下:
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()这东西都是private。 所以父类压根就不会加载我写的那个sqlsessioninterceptor()。所以问题就出在这,那好吧,博主又重写构函数
public ejssqlsessiontemplate(sqlsessionfactory sqlsessionfactory, executortype executortype, persistenceexceptiontranslator exceptiontranslator) { super(getsqlsessionfactory(), executortype, exceptiontranslator); }
很明显这段代码是编译不通过的,构造函数中,怎么可能调用类实例方法列? 那怎么办列? 又只有把父类的构造函数copy过来,那问题又有了,这些成员属性又没有。那又只得把他们也搬过来。。 后来,这个动态数据数据源的功能,终于完成了。
--------------------------------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------------------------整个完整的代码如下:
1、重写sqlsessiontemplate (重写的过程已经在上面分析过了)
public class ejssqlsessiontemplate extends sqlsessiontemplate { private final sqlsessionfactory sqlsessionfactory; private final executortype executortype; private final sqlsession sqlsessionproxy; private final persistenceexceptiontranslator exceptiontranslator; private map<object, sqlsessionfactory> targetsqlsessionfactory; public void settargetsqlsessionfactory(map<object, sqlsessionfactory> targetsqlsessionfactory) { this.targetsqlsessionfactory = targetsqlsessionfactory; } public ejssqlsessiontemplate(sqlsessionfactory sqlsessionfactory) { this(sqlsessionfactory, sqlsessionfactory.getconfiguration().getdefaultexecutortype()); } public ejssqlsessiontemplate(sqlsessionfactory sqlsessionfactory, executortype executortype) { this(sqlsessionfactory, executortype, new mybatisexceptiontranslator(sqlsessionfactory.getconfiguration() .getenvironment().getdatasource(), true)); } public ejssqlsessiontemplate(sqlsessionfactory sqlsessionfactory, executortype executortype, persistenceexceptiontranslator exceptiontranslator) { super(sqlsessionfactory, executortype, exceptiontranslator); this.sqlsessionfactory = sqlsessionfactory; this.executortype = executortype; this.exceptiontranslator = exceptiontranslator; this.sqlsessionproxy = (sqlsession) newproxyinstance( sqlsessionfactory.class.getclassloader(), new class[] { sqlsession.class }, new sqlsessioninterceptor()); } @override public sqlsessionfactory getsqlsessionfactory() { sqlsessionfactory targetsqlsessionfactory = this.targetsqlsessionfactory.get(sqlsessioncontextholder.getdatasourcekey()); if (targetsqlsessionfactory != null) { return targetsqlsessionfactory; } else if ( this.sqlsessionfactory != null) { return this.sqlsessionfactory; } throw new illegalargumentexception("sqlsessionfactory or targetsqlsessionfactory must set one at least"); } @override public configuration getconfiguration() { return this.getsqlsessionfactory().getconfiguration(); } public executortype getexecutortype() { return this.executortype; } public persistenceexceptiontranslator getpersistenceexceptiontranslator() { return this.exceptiontranslator; } /** * {@inheritdoc} */ public <t> t selectone(string statement) { return this.sqlsessionproxy.<t> selectone(statement); } /** * {@inheritdoc} */ public <t> t selectone(string statement, object parameter) { return this.sqlsessionproxy.<t> selectone(statement, parameter); } /** * {@inheritdoc} */ public <k, v> map<k, v> selectmap(string statement, string mapkey) { return this.sqlsessionproxy.<k, v> selectmap(statement, mapkey); } /** * {@inheritdoc} */ public <k, v> map<k, v> selectmap(string statement, object parameter, string mapkey) { return this.sqlsessionproxy.<k, v> selectmap(statement, parameter, mapkey); } /** * {@inheritdoc} */ public <k, v> map<k, v> selectmap(string statement, object parameter, string mapkey, rowbounds rowbounds) { return this.sqlsessionproxy.<k, v> selectmap(statement, parameter, mapkey, rowbounds); } /** * {@inheritdoc} */ public <e> list<e> selectlist(string statement) { return this.sqlsessionproxy.<e> selectlist(statement); } /** * {@inheritdoc} */ public <e> list<e> selectlist(string statement, object parameter) { return this.sqlsessionproxy.<e> selectlist(statement, parameter); } /** * {@inheritdoc} */ public <e> list<e> selectlist(string statement, object parameter, rowbounds rowbounds) { return this.sqlsessionproxy.<e> selectlist(statement, parameter, rowbounds); } /** * {@inheritdoc} */ public void select(string statement, resulthandler handler) { this.sqlsessionproxy.select(statement, handler); } /** * {@inheritdoc} */ public void select(string statement, object parameter, resulthandler handler) { this.sqlsessionproxy.select(statement, parameter, handler); } /** * {@inheritdoc} */ public void select(string statement, object parameter, rowbounds rowbounds, resulthandler handler) { this.sqlsessionproxy.select(statement, parameter, rowbounds, handler); } /** * {@inheritdoc} */ public int insert(string statement) { return this.sqlsessionproxy.insert(statement); } /** * {@inheritdoc} */ public int insert(string statement, object parameter) { return this.sqlsessionproxy.insert(statement, parameter); } /** * {@inheritdoc} */ public int update(string statement) { return this.sqlsessionproxy.update(statement); } /** * {@inheritdoc} */ public int update(string statement, object parameter) { return this.sqlsessionproxy.update(statement, parameter); } /** * {@inheritdoc} */ public int delete(string statement) { return this.sqlsessionproxy.delete(statement); } /** * {@inheritdoc} */ public int delete(string statement, object parameter) { return this.sqlsessionproxy.delete(statement, parameter); } /** * {@inheritdoc} */ public <t> t getmapper(class<t> type) { return getconfiguration().getmapper(type, this); } /** * {@inheritdoc} */ public void commit() { throw new unsupportedoperationexception("manual commit is not allowed over a spring managed sqlsession"); } /** * {@inheritdoc} */ public void commit(boolean force) { throw new unsupportedoperationexception("manual commit is not allowed over a spring managed sqlsession"); } /** * {@inheritdoc} */ public void rollback() { throw new unsupportedoperationexception("manual rollback is not allowed over a spring managed sqlsession"); } /** * {@inheritdoc} */ public void rollback(boolean force) { throw new unsupportedoperationexception("manual rollback is not allowed over a spring managed sqlsession"); } /** * {@inheritdoc} */ public void close() { throw new unsupportedoperationexception("manual close is not allowed over a spring managed sqlsession"); } /** * {@inheritdoc} */ public void clearcache() { this.sqlsessionproxy.clearcache(); } /** * {@inheritdoc} */ public connection getconnection() { return this.sqlsessionproxy.getconnection(); } /** * {@inheritdoc} * @since 1.0.2 */ public list<batchresult> flushstatements() { return this.sqlsessionproxy.flushstatements(); } /** * proxy needed to route mybatis method calls to the proper sqlsession got from spring's transaction manager it also * unwraps exceptions thrown by {@code method#invoke(object, object...)} to pass a {@code persistenceexception} to * the {@code persistenceexceptiontranslator}. */ private class sqlsessioninterceptor implements invocationhandler { public object invoke(object proxy, method method, object[] args) throws throwable { final sqlsession sqlsession = getsqlsession( ejssqlsessiontemplate.this.getsqlsessionfactory(), ejssqlsessiontemplate.this.executortype, ejssqlsessiontemplate.this.exceptiontranslator); try { object result = method.invoke(sqlsession, args); if (!issqlsessiontransactional(sqlsession, ejssqlsessiontemplate.this.getsqlsessionfactory())) { // force commit even on non-dirty sessions because some databases require // a commit/rollback before calling close() sqlsession.commit(true); } return result; } catch (throwable t) { throwable unwrapped = unwrapthrowable(t); if (ejssqlsessiontemplate.this.exceptiontranslator != null && unwrapped instanceof persistenceexception) { throwable translated = ejssqlsessiontemplate.this.exceptiontranslator .translateexceptionifpossible((persistenceexception) unwrapped); if (translated != null) { unwrapped = translated; } } throw unwrapped; } finally { closesqlsession(sqlsession, ejssqlsessiontemplate.this.getsqlsessionfactory()); } } } }
2。自定义了一个注解
/** * 注解式数据源,用来进行数据源切换 * user:amos.zhou * date: 14-2-27 * time: 下午2:34 */ @target({elementtype.method, elementtype.type}) @retention(retentionpolicy.runtime) @documented public @interface choosedatasource { string value() default ""; }
3.定义一个aspectj的切面(我习惯用aspectj,因为spring aop不支持cflow()这些语法),所以在编译,打包的时候一定要用aspectj的编译器,不能直接用原生的jdk。有些方法就是我基于以前hibernate,jdbc动态数据源的时候改动的。
/** * <li>类描述:完成数据源的切换,抽类切面,具体项目继承一下,不需要重写即可使用</li> * * @author: amos.zhou * 2013-8-1 上午11:51:40 * @since v1.0 */ @aspect public abstract class choosedatasourceaspect { protected static final threadlocal<string> predatasourceholder = new threadlocal<string>(); @pointcut("execution(public * *.*(..))") public void allmethodpoint() { } @pointcut("@within(com.ejushang.spider.annotation.choosedatasource) && allmethodpoint()") public void allservicemethod() { } /** * 对所有注解有choosedatasource的类进行拦截 */ @pointcut("cflow(allservicemethod()) && allservicemethod()") public void changedatasourcepoint() { } /** * 根据@choosedatasource的属性值设置不同的datasourcekey,以供dynamicdatasource */ @before("changedatasourcepoint()") public void changedatasourcebeforemethodexecution(joinpoint jp) { //拿到anotation中配置的数据源 string resultds = determinedatasource(jp); //没有配置实用默认数据源 if (resultds == null) { sqlsessioncontextholder.setsessionfactorykey(null); return; } predatasourceholder.set(sqlsessioncontextholder.getdatasourcekey()); //将数据源设置到数据源持有者 sqlsessioncontextholder.setsessionfactorykey(resultds); } /** * <p>创建时间: 2013-8-20 上午9:48:44</p> * 如果需要修改获取数据源的逻辑,请重写此方法 * * @param jp * @return */ @suppresswarnings("rawtypes") protected string determinedatasource(joinpoint jp) { string methodname = jp.getsignature().getname(); class targetclass = jp.getsignature().getdeclaringtype(); string datasourcefortargetclass = resolvedatasourcefromclass(targetclass); string datasourcefortargetmethod = resolvedatasourcefrommethod( targetclass, methodname); string resultds = determinatedatasource(datasourcefortargetclass, datasourcefortargetmethod); return resultds; } /** * 方法执行完毕以后,数据源切换回之前的数据源。 * 比如foo()方法里面调用bar(),但是bar()另外一个数据源, * bar()执行时,切换到自己数据源,执行完以后,要切换到foo()所需要的数据源,以供 * foo()继续执行。 * <p>创建时间: 2013-8-16 下午4:27:06</p> */ @after("changedatasourcepoint()") public void restoredatasourceaftermethodexecution() { sqlsessioncontextholder.setsessionfactorykey(predatasourceholder.get()); predatasourceholder.remove(); } /** * <li>创建时间: 2013-6-17 下午5:34:13</li> <li>创建人:amos.zhou</li> <li>方法描述 :</li> * * @param targetclass * @param methodname * @return */ @suppresswarnings("rawtypes") private string resolvedatasourcefrommethod(class targetclass, string methodname) { method m = reflectutil.finduniquemethod(targetclass, methodname); if (m != null) { choosedatasource chods = m.getannotation(choosedatasource.class); return resolvedatasourcename(chods); } return null; } /** * <li>创建时间: 2013-6-17 下午5:06:02</li> * <li>创建人:amos.zhou</li> * <li>方法描述 : 确定 * 最终数据源,如果方法上设置有数据源,则以方法上的为准,如果方法上没有设置,则以类上的为准,如果类上没有设置,则使用默认数据源</li> * * @param classds * @param methodds * @return */ private string determinatedatasource(string classds, string methodds) { // if (null == classds && null == methodds) { // return null; // } // 两者必有一个不为null,如果两者都为null,也会返回null return methodds == null ? classds : methodds; } /** * <li>创建时间: 2013-6-17 下午4:33:03</li> <li>创建人:amos.zhou</li> <li>方法描述 : 类级别的 @choosedatasource * 的解析</li> * * @param targetclass * @return */ @suppresswarnings({"unchecked", "rawtypes"}) private string resolvedatasourcefromclass(class targetclass) { choosedatasource classannotation = (choosedatasource) targetclass .getannotation(choosedatasource.class); // 直接为整个类进行设置 return null != classannotation ? resolvedatasourcename(classannotation) : null; } /** * <li>创建时间: 2013-6-17 下午4:31:42</li> <li>创建人:amos.zhou</li> <li>方法描述 : * 组装datasource的名字</li> * * @param ds * @return */ private string resolvedatasourcename(choosedatasource ds) { return ds == null ? null : ds.value(); } }
那么以上3个类,就可以作为一个公共的组件打个包了。
那么项目中具体 怎么用列?
4. 在项目中定义一个具体的aspectj切面
@aspect public class orderfetchaspect extends choosedatasourceaspect { }
如果你的根据你的需要重写方法,我这边是不需要重写的,所以空切面就行了。
5.配置spring,在上面的分析过程中已经贴出了,基本上就是每个数据库,一个datasource,每个datasource一个sqlsessionfactory。最后配一个sqlsessiontemplate,也就是我们自己重写的。再就是mapperscan了,大致如下(数据库连接信息已去除,包名为杜撰):
<bean id="datasource" class="com.mchange.v2.c3p0.combopooleddatasource" destroy-method="close"> </bean> <bean id="datasourcetb" class="com.mchange.v2.c3p0.combopooleddatasource" destroy-method="close"> </bean> <!-- 事务管理 --> <bean id="transactionmanager" class="org.springframework.jdbc.datasource.datasourcetransactionmanager"> <property name="datasource" ref="datasource" /> </bean> <!-- 注解控制事务 --> <tx:annotation-driven transaction-manager="transactionmanager"/> <bean id="sqlsessionfactory" class="org.mybatis.spring.sqlsessionfactorybean"> <property name="datasource" ref="datasource"/> <property name="configlocation" value="classpath:mybatis.xml" /> <property name="mapperlocations" value="classpath*:com/foo/bar/**/config/*mapper.xml" /> </bean> <bean id="sqlsessionfactorytb" class="org.mybatis.spring.sqlsessionfactorybean"> <property name="datasource" ref="datasourcetb"/> <property name="configlocation" value="classpath:mybatis.xml" /> <property name="mapperlocations" value="classpath*:<span style="font-family: arial, helvetica, sans-serif;">com/foo/bar</span><span style="font-family: arial, helvetica, sans-serif;">/**/configtb/*mapper.xml" /></span> </bean> <bean id="sqlsessiontemplate" class="com.foo.bar.template.ejssqlsessiontemplate"> <constructor-arg ref="sqlsessionfactory" /> <property name="targetsqlsessionfactory"> <map> <entry value-ref="sqlsessionfactory" key="spider"/> <entry value-ref="sqlsessionfactorytb" key="sysinfo"/> </map> </property> </bean> <bean id="mapperscannerconfigurer" class="org.mybatis.spring.mapper.mapperscannerconfigurer"> <property name="basepackage" value="com.foo.bar.**.mapper*" /> <property name="sqlsessiontemplatebeanname" value="sqlsessiontemplate"/> </bean>
6.具体应用
@choosedatasource("spider") public class shopservicetest extends erptest { private static final logger log = loggerfactory.getlogger(shopservicetest.class); @autowired private ishopservice shopservice; @autowired private ijdptbtradeservice jdptbtradeservice; @test @rollback(false) public void testfindallshop(){ list<shop> shoplist1 = shopservice.findallshop(); for(shop shop : shoplist1){ system.out.println(shop); } fromtestdb(); } @choosedatasource("sysinfo") private void fromtestdb(){ list<shop> shoplist = jdptbtradeservice.findallshop(); for(shop shop : shoplist){ system.out.println(shop); } } }
测试发现 shoplist1是从spider库查出来的数据,而fromdb则是从sysinfo中查出来的数据。 那么我们就大功告成。
要做到我以上功能,spring aop是做不到的,因为他不支持cflow(),这也就是我为什么要用aspectj的原因。
-----------------------------------------------------------------------------------------------再次分割线-------------------------------------------------------------------------------------------------------------------
好了,功能我们已经实现了,你有没有觉得很麻烦,这一点也不spring的风格,spring的各个地方扩展都是很方便的。那么我们看看,在sqlsessiontemplate中的什么地方改动一下,我们就可以很轻松的实现这个功能列?大家可以理解了,再回去看一下源码。
其实,只要将源码中的那个sqlsessioninterceptor的这句话:
final sqlsession sqlsession = getsqlsession( sqlsessiontemplate.this.sqlsessionfactory, sqlsessiontemplate.this.executortype, sqlsessiontemplate.this.exceptiontranslator);
改为:
final sqlsession sqlsession = getsqlsession( ejssqlsessiontemplate.this.getsqlsessionfactory(), ejssqlsessiontemplate.this.executortype, ejssqlsessiontemplate.this.exceptiontranslator);
保证 每次在产生session代理的时候,传进去的参数都是调用getsqlsessionfactory()获取,那么我们自定义的sqlsessiontemplate,只要重写getsqlsessionfactory(),加多一个以下2句话:
private map<object, sqlsessionfactory> targetsqlsessionfactory; public void settargetsqlsessionfactory(map<object, sqlsessionfactory> targetsqlsessionfactory) { this.targetsqlsessionfactory = targetsqlsessionfactory; }
那么就完全可以实现动态数据源切换。 那么mybatis-spring的项目团队会这样维护么? 我会以mail的方式与他们沟通。至于能否改进,我们不得而知了。
其实这也就引发一个关于面向对象设计时的思想,也是一直争论得喋喋不休的一个问题:
在类的方法中,如果要用到类的属性时,是直接用this.filedname 来操作,还是用 getfiledname() 来进行操作?
其实以前我也是偏向于直接用this.属性来进行操作的,但是经历过这次以后,我想我会偏向于后者。
以上所述是小编给大家介绍的关于spring3 + mybatis3整合时多数据源动态切换的问题,希望对大家有所帮助
上一篇: 详解Java 自动装箱与拆箱的实现原理