纸上得来终觉浅,绝知此事要躬行
最终效果就是,通过在service层类或方法上,注解声明需要的数据源名称,从而实现切换数据源的目的
理论知识就不说了,直接上代码(不支持分布式事务)*
1.准备多数据源properties文件
<!-- 数据库1 -->
customer.jdbc.driver = com.mysql.jdbc.Driver
customer.jdbc.url = jdbc:mysql://localhost:3306/demo1?useUnicode=true&characterEncoding=utf-8
customer.jdbc.username = root
customer.jdbc.password = root
<!-- 数据库2 -->
...
2.spring注入数据源
<!-- 数据库1 -->
<bean id="customer" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close" >
<property name="driverClass" value="#{database['customer.jdbc.driver']}" />
<property name="jdbcUrl" value="#{database['customer.jdbc.url']}" />
<property name="user" value="#{database['customer.jdbc.username']}" />
<property name="password" value="#{database['customer.jdbc.password']}"/>
</bean>
<!-- 数据库2 -->
...
3.自定义DataSourceContextHolder类
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/**
* @description 提供给AOP去设置当前的线程的数据源的信息
* @param dbType
*/
public static void setDbType(String dbType) {
contextHolder.set(dbType);
}
/**
* @description 提供给AbstractRoutingDataSource的实现类,通过key选择数据源
* @return
*/
public static String getDbType() {
return (contextHolder.get());
}
/**
* @description 使用默认的数据源
*/
public static void clearDbType() {
contextHolder.remove();
}
}
4.自定义DynamicDataSource类继承自AbstractRoutingDataSource抽象类并实现determineCurrentLookupKey方法
public class DynamicDataSource extends AbstractRoutingDataSource{
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
5.spring中注入多数据源
<bean id="dynamicDataSource" class="com.core.DynamicDataSource" >
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry value-ref="customer" key="customer"></entry>
...
</map>
</property>
<property name="defaultTargetDataSource" ref="customer">
</property>
</bean>
6.sqlSessionFactory
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dynamicDataSource" />
<property name="typeAliasesPackage" value="com.mapper1,com.mapper2" />
<property name="mapperLocations">
<list><!-- mapperLocations是数组类型 list array都可以 示例为多路径复杂情况 -->
<value>classpath:/mybatis/*Mapper.xml</value>
<value>classpath:/mybatis/base/*Mapper.xml</value>
</list>
</property>
</bean>
7.sqlSessionTemplate通过sqlSessionFactory初始化
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
8.扫描所有mybatis的接口
<!-- 扫描basePaclage下所有mapper接口 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 若使用sqlSessionFactoryBeanName,则忽略上一步
有小伙伴发现这个成员变量会导致数据库切换失败,我本人测试没问题
个人觉得由于事务和数据源的切换顺序导致的问题居多
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
-->
<property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate" />
<property name="basePackage" value="com.dao1, com.dao2"></property>
</bean>
9.自定义AOP动态切换数据源切面类DataSourceAspect
方式1:采用注解方式,自定义注解做为切点标记
自定义注解DataSource
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface DataSource {
String value();
}
//使用自定义切换数据库注解
@DataSource(value="customer")
@Service
public class CustomerService
/**
* AOP动态切换数据源切面类
* @author user
*
*/
@Component//<!-- 确认是否配置了该路径下注释配置bean的自动扫描 -->
@Order(0)//<!-- 设置切换数据源的优先级 -->
@Aspect
public class DataSourceAspect {
//@within在类上设置
//@annotation在方法上进行设置
//事务在service层 因此切换数据库的注解只能放在service层或之前
@Pointcut("@within(com.core.DataSource)||@annotation(com.core.DataSource)")
public void pointcut() {}
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint)
{
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
DataSource annotationClass = method.getAnnotation(DataSource.class);
if(annotationClass == null){
annotationClass = joinPoint.getTarget().getClass().getAnnotation(DataSource.class);
if(annotationClass == null) return;
}
String dataSourceKey = annotationClass.value();
if(dataSourceKey !=null){
DatabaseContextHolder.setCustomerType(dataSourceKey);
}
}
@After("pointcut()")
public void after(JoinPoint point) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
DatabaseContextHolder.clearCustomerType();
System.out.println("切换默认数据源成功!");
}
}
注意:这里有个坑,一定记得配置扫描器,确认已经存在的扫描路径可以扫描到这个切面类
<!-- 采用注释的方式配置bean -->
<context:component-scan base-package="com.core"></context:component-scan>
<!-- aop代理 true表示使用CGLIB代理 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
方式2:采用xml配置方式,切点表达式作为切点 首先还是定义切面类
public class DataSourceAspect {
public void doBefore(JoinPoint joinPoint)
{
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
DataSource annotationClass = method.getAnnotation(DataSource.class);
if(annotationClass == null){
annotationClass = joinPoint.getTarget().getClass().getAnnotation(DataSource.class);
if(annotationClass == null) return;
}
String dataSourceKey = annotationClass.value();
if(dataSourceKey !=null){
DatabaseContextHolder.setCustomerType(dataSourceKey);
}
}
public void after(JoinPoint point) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
DatabaseContextHolder.clearCustomerType();
System.out.println("切换默认数据源成功!");
}
}
配置切面-切点-设置优先级
<bean id="dataSourceAspect" class="com.core.DataSourceAspect"/>
<aop:config>
<!--数据源的切换应在事务之前,事务在service层,
因此数据源的切点至少在service层(或者controller层)-->
<aop:pointcut id="switchDataSource" expression="execution(* com.service..*.*(..)) "/>
<!--order的数值越小优先级越高 -->
<aop:aspect ref="dataSourceAspect" order="0">
<aop:before method="doBefore" pointcut-ref="switchDataSource"/>
<aop:after method="after" pointcut-ref="switchDataSource"/>
</aop:aspect>
</aop:config>
设置事务的优先级
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1" order="1"/>