spring企业开发-动态数据源切换-第三篇

版权声明:Wizard原创博客,多多支持 https://blog.csdn.net/ysj4428/article/details/82111024

前面的开发配置基本已经介绍完毕,下面就针对其中切换数据源进行介绍:

何为切换数据源?就是我们在开发过程中,可能用到不同连接的数据库,有的操作需要使用数据库A,有的数据库需要使用数据库B

来看一下切换数据源的原理:

1.切换数据源为方法级别的切换。即调用某些方法时动态切换不同数据源

2.确定在哪些方法切换可以使用自定义注解以及AOP切面来实现

3.将多个数据源添加到配置文件

下面就开始具体代码:

步骤一:配置文件中添加多个数据源配置

    <!--MySQL数据源1配置-->
    <bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${druid.initialSize}"/>
        <property name="minIdle" value="${druid.minIdle}"/>
        <property name="maxActive" value="${druid.maxActive}"/>

        <!-- 配置获取连接等待超时时间 -->
        <property name="maxWait" value="${druid.maxWait}"/>

        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}"/>

        <!-- 配置一个连接池中最小生存的时间,单位毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}"/>

        <property name="validationQuery" value="${druid.validationQuery}"/>
        <property name="testWhileIdle" value="${druid.testWhileIdle}"/>

        <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="${druid.poolPreparedStatements}"/>
        <property name="maxPoolPreparedStatementPerConnectionSize"
                  value="${druid.maxPoolPreparedStatementPerConnectionSize}"/>

        <!-- 配置监控系统拦截的filters,去掉后监控界面sql无法统计 -->
        <property name="filters" value="${druid.filters}"/>
    </bean>
    <!--MySQL数据源2配置-->
    <bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${smsJdbc.url}"/>
        <property name="driverClassName" value="${smsJdbc.driverClassName}"/>
        <property name="username" value="${smsJdbc.username}"/>
        <property name="password" value="${smsJdbc.password}"/>
        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${druid.initialSize}"/>
        <property name="minIdle" value="${druid.minIdle}"/>
        <property name="maxActive" value="${druid.maxActive}"/>

        <!-- 配置获取连接等待超时时间 -->
        <property name="maxWait" value="${druid.maxWait}"/>

        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}"/>

        <!-- 配置一个连接池中最小生存的时间,单位毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}"/>

        <property name="validationQuery" value="${druid.validationQuery}"/>
        <property name="testWhileIdle" value="${druid.testWhileIdle}"/>

        <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="${druid.poolPreparedStatements}"/>
        <property name="maxPoolPreparedStatementPerConnectionSize"
                  value="${druid.maxPoolPreparedStatementPerConnectionSize}"/>

        <!-- 配置监控系统拦截的filters,去掉后监控界面sql无法统计 -->
        <property name="filters" value="${druid.filters}"/>
    </bean>

这样就得到两个dataSource了,然后定义一个dataSource的bean,用来作为转换数据源的bean

<bean id="dataSource" class="com.youo.mybatis.datasource.ChooseDataSource">
    <property name="defaultTargetDataSource" ref="dataSource1" />
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry key="dataSource1" value-ref="dataSource1" />
            <entry key="dataSource2" value-ref="dataSource2" />
            <!-- 这里还可以加多个dataSource -->
        </map>
    </property>
</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切面。

步骤二:设置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<String> handleThreadLocal = new ThreadLocal<String>();

    //提供给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<String> listPhone,String message) {
        List<OutBox> listOutBox = new ArrayList<OutBox>();
        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的步骤以及实现。

是否切换成功就需要你自己看看对切换库表操作是否成功。同时其中导致失败原因可能为没有扫描或者被事务屏蔽等。

如有问题请留言。

 
 

猜你喜欢

转载自blog.csdn.net/ysj4428/article/details/82111024
今日推荐