前面的开发配置基本已经介绍完毕,下面就针对其中切换数据源进行介绍:
何为切换数据源?就是我们在开发过程中,可能用到不同连接的数据库,有的操作需要使用数据库A,有的数据库需要使用数据库B
来看一下切换数据源的原理:
1.切换数据源为方法级别的切换。即调用某些方法时动态切换不同数据源
2.确定在哪些方法切换可以使用自定义注解以及AOP切面来实现
3.将多个数据源添加到配置文件
下面就开始具体代码:
这样就得到两个dataSource了,然后定义一个dataSource的bean,用来作为转换数据源的bean
其中class为自定义的类,该类继承
AbstractRoutingDataSource
具体为:
public class MultipleDataSourceToChoose extends AbstractRoutingDataSource{
/**
* 根据Key获取数据源的信息,上层抽象函数的钩子
* */
@Override
protected Object determineCurrentLookupKey() {
return HandlerDataSource.getDataSource();
}
}
该类实现抽象类中方法,获取数据源信息并返回。
我们在进行数据库操作时,mybatis会通过SqlSessionFactory创建SqlSession从而进行数据库操作。该类就是在创建SqlSessionFactory之前进行dataSource选择,通过bean中具体的名称创建SqlSessionfactory。而默认情况下是使用配置的默认数据源创建。
下面这张图可能让你明白一些东西:
那什么时候需要修改数据源呢?
这里一般情况下是根据业务来的,也就是说业务中使用数据源不同所以需要进行切换。看实例:
我这里需要拿到数据库1中的数据,然后将该数据插入到数据库2。查询和插入都是调用Service层的方法进行操作,因此在方法上我选择不同数据源进行操作。如何监听这个方法呢?答案就是使用AOP切面。
1.首先定义一个注解类。他的作用就是AOP可以检测到带有该注解的方法,同时获取到注解中的参数(参数为数据源名)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Yangsaijun
* @date 2018/11/23 0023
* @time 15:44
* @desc 编译器将把注释记录在类文件中,在运行时 VM 将保留注释,因此可以反射性地读取
* @see
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface DataSource {
String value() default "";
}
2.设置切面,这里采用的是注解方式注册切面
切面的功能就是在达到你切面中设置的条件时,触发切面方法
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @author sundongyang
* @date 2017/11/23 0023
* @time 15:46
* @desc 定义一个数据源切面类,通过aop访问,获取方法上的自定义注解,然后根据注解内容尽情判断,动态设置数据源
* @see
*/
@Aspect
@Component
@Order(1)
public class DataSourceAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceAspect.class);
@Pointcut("@within(com.youotech.usbmonitor.datasource.DataSource)||@annotation(com.youotech.usbmonitor.datasource.DataSource)")
public void pointCut() {}
@Before("pointCut()")
public void before(JoinPoint point) {
LOGGER.info("切面捕获到修改数据源信息");
MethodSignature signa = (MethodSignature) point.getSignature();
Method method = signa.getMethod();
DataSource annotationClass = method.getAnnotation(DataSource.class);//获取方法上的注解
if(annotationClass == null){
annotationClass = point.getTarget().getClass().getAnnotation(DataSource.class);//获取类上面的注解
if(annotationClass == null) return;
}
//获取注解上的数据源的值的信息
String dataSourceKey = annotationClass.value();
if(dataSourceKey !=null){
//给当前的执行SQL的操作设置特殊的数据源的信息
HandleDataSource.putDataSource(dataSourceKey);
}
LOGGER.info("AOP动态切换数据源,className"+point.getTarget().getClass().getName()+"methodName"+method.getName()+";dataSourceKey:"+dataSourceKey==""?"默认数据源":dataSourceKey);
}
/**
* 清理掉当前设置的数据源,让默认的数据源不受影响
* */
@After("pointCut()")
public void after(JoinPoint point){
HandleDataSource.clear();
}
}
这里需要注意的是:
a.需要使用@Aspect注解标识该类为切面类
b.切面也是需要Spring容器管理的,因此需要被扫描到IOC容器,这里使用@Component
c.@Order(1)注解是启用顺序的注解,该处注解的设置是因为有时因为事务的原因导致切面不起作用,因此让切面在事务之前使用。
d.@Pointcut里面就是我们刚才新增的注解,意思是有这个注解的地方就会触发这个切面中方法。
e.@Before的含义是在检测到某些方法上有我们定义的注解时,在执行目标方法前先执行这个被@Before修饰的方法,也叫前置方法
f.@After故名思意是后置通知,意思是目标方法执行后的方法。这里的目的是执行方法后,将数据源信息清除,即使用默认数据源。当然还有环绕通知。。。这里不在赘述,有兴趣可以自己网上查。
我们看到里面有调用数据源处理信息类这里贴出来
/**
* @author Yangsaijun
* @date 2017/11/23 0023
* @time 15:46
* @desc 利用ThreadLocal解决线程安全问题,当前线程获取设置相应数据源
* @see
*/
public class HandleDataSource {
private static ThreadLocal handleThreadLocal = new ThreadLocal();
//提供给AOP去设置当前的线程的数据源的信息
public static void putDataSource(String dataSource){
handleThreadLocal.set(dataSource);
}
//提供给AbstractRoutingDataSource的实现类,通过key选择数据源
public static String getDataSource(){
return handleThreadLocal.get();
}
//使用默认的数据源
public static void clear(){
handleThreadLocal.remove();
}
}
也许上面看着还是头晕,那下面就完整演示一下,演示内容为开发过程和类调用过程:
开发过程:
演示过程:
1.首先Service层中实现类有一个方法要切换数据源,则在该方法上添加注解,并把想要切换的数据源写入注解中:
@DataSource(value="dataSource2")
public int saveMessage(List listPhone,String message) {
List listOutBox = new ArrayList();
int resultNum = 0;
int listSize = listPhone.size();
if(listSize > 0){
OutBox outbox = null;
for (int i = 0; i < listSize; i++) {
outbox = new OutBox();
outbox.setMbno(listPhone.get(i));
outbox.setMsg(message);
outbox.setSendTime(new Timestamp(System.currentTimeMillis()));
listOutBox.add(outbox);
}
//进行警告信息插入远程短信表
resultNum = outBoxMapper.insertList(listOutBox);
}else{
LOGGER.info("[OutBoxServiceImpl.saveMessage]获取电话号码为空,无法进行短信插入");
}
return resultNum;
}
注意看到了 @DataSource(value="dataSource2")了吧
2.当我们调用这个方法的时候,因为AOP是拦截DataSource注解的嘛,所以马上就调用了
DataSourceAspect类中的前置通知方法:
@Before("pointCut()")
public void before(JoinPoint point) {
LOGGER.info("切面捕获到修改数据源信息");
MethodSignature signa = (MethodSignature) point.getSignature();
Method method = signa.getMethod();
DataSource annotationClass = method.getAnnotation(DataSource.class);//获取方法上的注解
if(annotationClass == null){
annotationClass = point.getTarget().getClass().getAnnotation(DataSource.class);//获取类上面的注解
if(annotationClass == null) return;
}
//获取注解上的数据源的值的信息
String dataSourceKey = annotationClass.value();
if(dataSourceKey !=null){
//给当前的执行SQL的操作设置特殊的数据源的信息
HandleDataSource.putDataSource(dataSourceKey);
}
LOGGER.info("AOP动态切换数据源,className"+point.getTarget().getClass().getName()+"methodName"+method.getName()+";dataSourceKey:"+dataSourceKey==""?"默认数据源":dataSourceKey);
}
前置方法都干了什么呢?没错拿到注解中的值:dataSource2,然后调用
HandleDataSource类中的putDataSource方法
该方法就是将当前线程中添加一个字符串,这里就是这个数据源名称
3.上面完事后,进行我们业务SQL执行了,Sql执行需要创建SqlSession,SqlSession需要SqlsessionFactory,而SqlsessionFactory的创建需要我们调用
AbstractRoutingDataSource抽象类中的determineCurrentLookupKey()方法
说白了就是确定数据源名,然后我们使用了
ChooseDataSource继承了AbstractRoutingDataSource,里面的方法返回的是
return HandleDataSource.getDataSource();
就是我们存放在线程中的那个值被取出来,没错是dataSource2,这是就创建了SqlsessionFactory,调用结束然后就是AOP中的后置通知了,将当前线程存放的数据源字符串去除。
@After("pointCut()")
public void after(JoinPoint point){
HandleDataSource.clear();
}
结构图为:
总结:
总的来说切换数据源还是比较简单的,主要是要明白Mybatis创建SqlsessionFactory的步骤以及实现。
是否切换成功就需要你自己看看对切换库表操作是否成功。同时其中导致失败原因可能为没有扫描或者被事务屏蔽等。
如有问题请留言。