Spring+Mybatis实现动态数据源配置
第一次接触电子商城的项目,所以也是第一次碰到读写要分离的需求,搞了半天时间终于搞定了,跟大家分享下,如果有不完整的地方,望大家见谅并提出宝贵的建议,谢谢!
好的,我们开始正题吧!
完成这一需求的思路图,如下图:
好处在于,当我们添加多个数据源时,不需要添加多个sessionFactory。
使用到的技术有:annotation,Spring AOP ,反射。
---------------------------------------------------------------
1、配置数据源,这个举例两个,如下代码:
<!-- 配置第一个数据源 --> <!-- 数据源 : DriverManagerDataSource --> <bean id="dataSourceOne" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${driverClass}" /> <property name="url" value="${url}" /> <property name="username" value="${username}" /> <property name="password" value="${password}" /> <!-- 初始化连接大小 --> <property name="initialSize" value="0"></property> <!-- 连接池最大数量 --> <property name="maxActive" value="20"></property> <!-- 连接池最大空闲 --> <property name="maxIdle" value="20"></property> <!-- 连接池最小空闲 --> <property name="minIdle" value="1"></property> <!-- 获取连接最大等待时间 --> <property name="maxWait" value="60000"></property> </bean> <!-- 配置第二个数据源 --> <!-- 数据源 : DriverManagerDataSource --> <bean id="dataSourceTwo" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${driverClass1}" /> <property name="url" value="${url1}" /> <property name="username" value="${username1}" /> <property name="password" value="${password1}" /> <!-- 初始化连接大小 --> <property name="initialSize" value="0"></property> <!-- 连接池最大数量 --> <property name="maxActive" value="20"></property> <!-- 连接池最大空闲 --> <property name="maxIdle" value="20"></property> <!-- 连接池最小空闲 --> <property name="minIdle" value="1"></property> <!-- 获取连接最大等待时间 --> <property name="maxWait" value="60000"></property> </bean>
2、接下来我们就要配置SqlSession 工厂(SqlSessionFactoryBean)
但是这边要注意的是,因为我们要有多个数据源,所以这边有所不同的是我们不能直接引用上面定义好的两个数据源,而是要引用我们自定义的动态数据源,看代码如下:
<bean id="dynamicDataSource" class="com.zgn.common.DynamicDataSource"> <property name="defaultTargetDataSource" ref="dataSourceOne" /> <property name="targetDataSources"> <map> <entry key="dataSourceOne" value-ref="dataSourceOne" /> <entry key="dataSourceTwo" value-ref="dataSourceTwo" /> </map> </property> </bean> <!-- mybatis 的 SqlSession 的工厂: SqlSessionFactoryBean --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dynamicDataSource" /> <!-- 配置mybatis的映射文件 --> <property name="mapperLocations"> <array> <value>classpath:com/zgn/*/mappers/*.xml</value> </array> </property> <property name="typeAliasesPackage" value="com.zgn" /> </bean>
3、那好啦,问题来了,com.zgn.common.DynamicDataSource这个类到底是什么呢??
这边就涉及到了一个AbstractRoutingDataSource类,我们来看下它的结构是怎么样的?
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { }
它继承了AbstractDataSource,而AbstractDataSource又是javax.sql.DataSource的子类,我们在往里面看看这个getConnection方法:
public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); }
当你看到determineTargetDataSource时,我想您也猜到怎么回事了吧,哈哈!!!!
那我们就看一眼,就一眼,看它里面是什么?好奇吧
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = (DataSource) this.resolvedDataSources.get(lookupKey); if (dataSource == null) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; }
有没有?有没有?看到determineCurrentLookupKey(),该方法返回需要使用的DataSource的key值,然后根据这个key从resolvedDataSources这个map里取出对应的DataSource,如果找不到,则用默认的resolvedDefaultDataSource,我们再结合看下上面贴出来的部分代码回顾下
<bean id="dynamicDataSource" class="com.zgn.common.DynamicDataSource"> <property name="defaultTargetDataSource" ref="dataSourceOne" /> <property name="targetDataSources"> <map> <entry key="dataSourceOne" value-ref="dataSourceOne" /> <entry key="dataSourceTwo" value-ref="dataSourceTwo" /> </map> </property> </bean>
4、所以说了那么多,我们到底怎么写这个动态数据源类呢?com.zgn.common.DynamicDataSource
继承AbstractRoutingDataSource重写determineCurrentLookupKey方法,还是看如下代码吧:
package com.zgn.common; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource{ @Override protected Object determineCurrentLookupKey() { //从自定义的位置获取数据源标识 return DynamicDataSourceHolder.getDataSource(); } }
5、好了问题又来了,这方法里面返回的是DynamicDataSourceHolder.getDataSource();方法,在哪里?在哪里?在这里,哈哈
package com.zgn.common; public class DynamicDataSourceHolder { /** 注意:数据源标识保存在线程变量中,避免多线程操作数据源时互相干扰 */ private static final ThreadLocal<String> dataSources = new ThreadLocal<String>(); // 设置数据源 public static void setDataSource(String dataSource) { dataSources.set(dataSource); } // 获取数据源 public static String getDataSource() { return dataSources.get(); } // 清除数据源 public static void clearDataSource() { dataSources.remove(); } }
6、那我们在什么时候调用setDataSource方法来动态切换数据源呢?(手动切换直接调用方法就好了,这里不多说了哈),接下来才是真正的高潮部分,这里我们用到了自定义注解(只需要在类或者方法上面写上注解就好了。),先定义一个注解类
package com.zgn.common; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String name() default DataSource.master; public static String master = "dataSourceOne"; public static String dataSourceTwo = "dataSourceTwo"; }
7、DataSource 接口类创建完还不够哈,这样使用是不会有效果的,接下来我们就要用到AOP了,创建一个DataSourceAspect切面类,获取由@DataSource指定的数据源标识,进行自动切换数据源(其实就是调用DynamicDataSourceHolder.setDataSource方法)
public class DataSourceAspect { /** * 拦截目标方法,获取由@DataSource指定的数据源标识,设置到线程存储中以便切换数据源 * * @param point * @throws Exception */ public void intercept(JoinPoint point) throws Exception { Class<?> target = point.getTarget().getClass(); MethodSignature signature = (MethodSignature) point.getSignature(); // 默认使用目标类型的注解,如果没有则使用其实现接口的注解 for (Class<?> clazz : target.getInterfaces()) { resolveDataSource(clazz, signature.getMethod()); } resolveDataSource(target, signature.getMethod()); } /** * 提取目标对象方法注解和类型注解中的数据源标识 * * @param clazz * @param method */ private void resolveDataSource(Class<?> clazz, Method method) { try { Class<?>[] types = method.getParameterTypes(); // 默认使用类型注解 if (clazz.isAnnotationPresent(DataSource.class)) { DataSource source = clazz.getAnnotation(DataSource.class); DynamicDataSourceHolder.setDataSource(source.name()); } // 方法注解可以覆盖类型注解 Method m = clazz.getMethod(method.getName(), types); if (m != null && m.isAnnotationPresent(DataSource.class)) { DataSource source = m.getAnnotation(DataSource.class); DynamicDataSourceHolder.setDataSource(source.name()); } } catch (Exception e) { System.out.println(clazz + ":" + e.getMessage()); } } }
8、好了,切面类写好后,我们还有一个很重要的步骤就是要在spring配置文件中进行配置才会有效果,
在所有的接口中进行拦截使用
<bean id="dataSourceAspect" class="com.zgn.common.DataSourceAspect" /> <aop:config> <aop:aspect ref="dataSourceAspect"> <!-- 拦截所有dao方法 --> <aop:pointcut id="dataSourcePointcut" expression="execution(* com.zgn.*.dao.*.*(..))" /> <aop:before pointcut-ref="dataSourcePointcut" method="intercept" /> </aop:aspect> </aop:config>
9、以上步骤做完就大功告成了,那怎么使用呢?
@DataSource(name=DataSource.master) public interface UserDao {
只要在dao层的接口类上写上自定义的注解就好了,当然了,也可以写在方法上。
搞定收工!!!!
对了,这边有使用到一个关键类aspectjweaver.jar,附上了供亲们下载哈,不用谢!!!