项目要实现多数据源动态切换,咋搞?

在作项目的时候,几乎都会用到数据库,不少时候就只连一个数据库,可是有时候咱们须要一个项目操做多个数据库,不一样的业务功能产生的数据存到不一样的数据库,那怎么来实现数据源的动态、灵活的切换呢?今天咱们就来实现这个功能。java

前期准备工做

咱们须要有一台联网的电脑(用于maven自动下载依赖),而且电脑安装JDK 八、IDEA、MySQL数据库、maven,首先建立一个springboot项目(SSM也行)。springboot版本和SSM版本的代码都已经放到码云托管。感兴趣的能够去下载https://gitee.com/itwalking/springboot-dynamic-datasource,https://gitee.com/itwalking/ssm-dynamic-datasourcemysql

实现思路

首先讲一下咱们的实现思路,平时咱们作项目,都会用到spring来集成咱们的数据源,链接mysqlOracle数据库,经过暴露出DataSource相关的接口,而后不一样的数据库均可以集成过来,咱们只须要配置数据源的四大参数便可,这是咱们往常的作法。而若是使用动态数据源的话,Spring也为咱们提供了相应的扩展点,那就是AbstractRoutingDataSource抽象类,它一样是jdbcDataSource接口的实现类。git

代码

废话很少说,咱们直接上代码。
建立咱们本身的数据源DynamicDataSource继承AbstractRoutingDataSource,实现它的抽象方法determineCurrentLookupKey,这个方法其实就是实现动态选择数据源的关键,经过这个方法返回的对象关联到咱们的数据源。web

package com.walking.db;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author walking
 * 公众号:编程大道
 */

public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * ThreadLocal 用于提供线程局部变量,在多线程环境能够保证各个线程里的变量独立于其它线程里的变量。
     * 也就是说 ThreadLocal 能够为每一个线程建立一个【单独的变量副本】,至关于线程的 private static 类型变量。
     */

    private static final ThreadLocal<DataSourceName> dataSourceName = new ThreadLocal<DataSourceName>();
    /**
     * 支持以包名的粒度选择数据源
     */

    private static final Map<String,DataSourceName> packageDataSource = new HashMap<>();

    public DynamicDataSource(DataSource firstDataSource, Map<Object, Object> targetDataSources) {
        setDefaultTargetDataSource(firstDataSource);
        setTargetDataSources(targetDataSources);
        afterPropertiesSet();
    }

    /**
     * 获取与线程上下文绑定的数据源名称(存储在ThreadLocal中)
     * @return 返回数据源名称
     */

    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceName dsName = dataSourceName.get();
        dataSourceName.remove();
        return dsName;
    }
    public static void setDataSourceName(DataSourceName dataSource){
        dataSourceName.set(dataSource);
    }
    public static void usePackageDatasourceKey(String pkName) {
        dataSourceName.set(packageDataSource.get(pkName));
    }
    public Map<String,DataSourceName> getPackageDatasource(){
        return packageDataSource;
    }
    public void setPackageDatasource(Map<String,DataSourceName> packageDatasource){
        this.packageDataSource.putAll(packageDatasource);
    }
}

DynamicDataSource中有一个ThreadLocal用来保存咱们当前选择的数据源名称,代码中的注释写的很清楚了。其中ThreadLocal的泛型是DataSourceNameDataSourceName是咱们本身定义的一个枚举类,用于定义咱们的数据源名称,我这里拿两个数据源作演示,并命名为FIRST, SECONDspring

package com.walking.db;
/**
 * @author walking
 * 公众号:编程大道
 */

public enum DataSourceName {
    FIRST, SECOND;
}

而后自定义一个注解,用于标注咱们操做数据库时选择哪一个数据源,很简单只有一个name属性,默认是 DataSourceName.FIRSTsql

@Target({ElementType.PACKAGE,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurDataSource {

    /**
     * name of DataSource
     * @return
     */

    DataSourceName value() default DataSourceName.FIRST;

}

有了这些还不够,咱们还须要根据咱们的注解里的name属性动态的去修改 DynamicDataSourceThreadLocal 中保存的数据库名称,每次执行SQL前都要修改数据源,这样才能达到修改数据源的目的。那很显然咱们就须要spring AOP来完成这个操做了。数据库

以下,DynamicDataSourceAspect 是咱们定义的一个切面类,同时也定了三个切点,分别去切方法上带@CurDataSource注解的方法,类上带@CurDataSource注解的类,以及按包名去切。这样,咱们的动态数据源就支持方法级别的、类级别的、包级别的动态配置了。编程

package com.walking.aaspect;

import com.walking.db.CurDataSource;
import com.walking.db.DataSourceName;
import com.walking.db.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Objects;

/**
 * 动态数据源切面类
 * 被切中的,则先判断方法上是否有CurDataSource注解
 * 而后判断方法所属类上是否有CurDataSource注解
 * 其次判断是否配置了包级别的数据源
 *
 * 优先级为方法、类、包
 * 若同时配置则优先按方法上的
 *
 * @author walking
 * 公众号:编程大道
 */

@Slf4j
@Aspect
@Component
public class DynamicDataSourceAspect {
    // pointCut
    @Pointcut("@annotation(com.walking.db.CurDataSource)")
    public void choseDatasourceByAnnotation() {
    }
    @Pointcut("@within(com.walking.db.CurDataSource)")
    public void choseDatasourceByClass() {
    }
    @Pointcut("execution(* com.walking.service3..*(..))")
    public void choseDatasourceByPackage() {
    }

    @Around("choseDatasourceByAnnotation() || choseDatasourceByClass() || choseDatasourceByPackage()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("进入AOP环绕通知");
        Signature signature = joinPoint.getSignature();
        DataSourceName datasourceName = getDatasourceKey(signature);
        if (!Objects.isNull(datasourceName)) {
            DynamicDataSource.setDataSourceName(datasourceName);
        }
        return joinPoint.proceed();
    }
    private DataSourceName getDatasourceKey(Signature signature) {
        if (signature == null) {
            return null;
        } else {
            if (signature instanceof MethodSignature) {
                MethodSignature methodSignature = (MethodSignature) signature;
                Method method = methodSignature.getMethod();
                if (method.isAnnotationPresent(CurDataSource.class)) {
                    return this.dsSettingInMethod(method);
                }
                Class<?> declaringClass = method.getDeclaringClass();
                if (declaringClass.isAnnotationPresent(CurDataSource.class)) {
                    return this.dsSettingInConstructor(declaringClass);
                }
                Package aPackage = declaringClass.getPackage();
                this.dsSettingInPackage(aPackage);
            }
            return null;
        }
    }
    private DataSourceName dsSettingInConstructor(Class<?> declaringClass) {
        CurDataSource dataSource = declaringClass.getAnnotation(CurDataSource.class);
        return dataSource.value();
    }
    private DataSourceName dsSettingInMethod(Method method) {
        CurDataSource dataSource = method.getAnnotation(CurDataSource.class);
        return dataSource.value();
    }
    private void dsSettingInPackage(Package pkg) {
        DynamicDataSource.usePackageDatasourceKey(pkg.getName());
    }
}

仔细看一下这个切面类的环绕通知这个方法的逻辑,能够发现,咱们首先看的是方法上的注解,而后再看类上的注解,最后看是否配置了包级别数据源。
基本上,该有的类咱们都写完了,剩下就是验证。springboot

验证以前咱们还须要进行一些配置。微信

配置多数据源

这里,咱们使用的是阿里的Druid数据源,用springboot自带的也行。咱们能够看到在Druid:配置下,本来直接就配置url、name这些参数,咱们新增了一级分别是first和second,用于配置多个数据源

server:
  port: 9966
  servlet:
    context-path: /walking
spring:
  mvc:
    log-request-details: false
  application:
    name: walking
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      first:
        url: jdbc:mysql://localhost:3306/walking_mybatis?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
        username: root
        password: 123456ppzy,
      second:
        url: jdbc:mysql://localhost:3306/walking_mybatis2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
        username: root
        password: 123456ppzy,

mybatis:
  mapper-locations: classpath:mapper/*.xml

而后javaconfig配置类。
配置了三个bean,前两个是数据源的bean,使用@ConfigurationProperties注解,让springboot帮咱们去配置文件读取指定前缀的配置,这样咱们刚才配的两个数据源参数就区分开了。
而后第三个bean是咱们配置的叫作dataSource的bean,用于覆盖spring默认的DataSource,在这个bean中,咱们把全部的数据源注入进去,这里咱们有两个,命名为FIRST和SECOND,以及咱们要配置的包级别的数据源,而后调用构造函数建立DynamicDataSource咱们的动态数据源。并指明了默认的数据源。

package com.walking.configuration;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.walking.db.DataSourceName;
import com.walking.db.DynamicDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author walking
 * 公众号:编程大道
 */

@Configuration
public class DynamicDataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource firstDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource secondDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>(2);
        targetDataSources.put(DataSourceName.FIRST, firstDataSource);
        targetDataSources.put(DataSourceName.SECOND, secondDataSource);

        //配置包级别的数据源
        Map<String, DataSourceName> packageDataSource = new HashMap<>();
        packageDataSource.put("com.walking.service3", DataSourceName.SECOND);

        DynamicDataSource dynamicDataSource = new DynamicDataSource(firstDataSource, targetDataSources);
        dynamicDataSource.setPackageDatasource(packageDataSource);
        dynamicDataSource.afterPropertiesSet();
        return dynamicDataSource;
    }
}

而后就是咱们的启动类了,咱们须要禁用掉spring的自动配置数据源,和Druid的自动配置数据源,使用咱们自定义的动态数据源。

@EnableAspectJAutoProxy
//关掉数据源自动配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
        DruidDataSourceAutoConfigure.class})
//导入咱们本身的数据源配置
@Import({DynamicDataSourceConfig.class})
@MapperScan(basePackages = "com.walking.dao")
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

操做Mybatis我就很少说了,这里我在两个数据库(walking_mybatis和walking_mybatis2)里建立了相同的user表,咱们测试的时候观察插入到哪一个表就OK了。

项目总体结构

测试

咱们在save_1上添加注解指明使用SECOND,在save_2则没有,UserService1类上也没用注解,一样的,在配置类里也没配置UserService1的包名,那么save_2将会使用默认的数据源那就是FIRST

controller

运行,访问http://localhost:9966/walking/test01
日志输出

查看数据库则第二个数据库新增一条数据。
完整代码我已上传gitee码云,详细的测试都在这三个service包下和test包下,感兴趣的能够去下载代码看看。

实现动态数据源切换就是这么简单。下次咱们看一下动态数据源的原理

总结一下

一、继承AbstractRoutingDataSource实现多数据源及默认数据源的配置
二、注解+AOP,实现动态修改数据源的逻辑
三、排除spring和Druid(若是引入了第三方数据库链接池)默认的自动配置数据源

动手操做下一下,SQL和项目都已上传。


本文分享自微信公众号 - 编程大道(learn_code)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索