利用AOP和AbstractRoutingDataSource实现动态切换数据源

背景
公司有多套环境,每个环境执行数据库脚本的时机也不一样,久而久之,不同环境相同表的结构就有了差异,需要做一个工具进行对比。

分析
同一套环境下有很多数据库,不同环境的数据库连接肯定也是不一样的,那么如何做到查询指定环境下的某一个数据库,需要动态的去切换数据源,根据当前的查询条件路由对应的数据库。可以使用AOP在执行SQL前切换数据源。

实现逻辑

  1. 用 AbstractRoutingDataSource 实现动态切换数据源,向容器中注册自定义的 AbstractRoutingDataSource 实现类(AbstractRoutingDataSource的相关介绍,我这篇文章有介绍,点我)。
  2. 用 ThreadLocal 线程级别变量存放当前数据源key。
  3. 利用 AOP 在执行SQL时切换数据源。

我这里因为公司使用的是阿里的DRDS,数据库是按照用户名进行隔离的,一个用户名对应一个数据库,所以多套环境下数据库的url连接都是一样的,只是连接的账号密码不一样,所以只需要把账号和密码配置在 application.xml 中。

spring:
  datasource:
    default:
      driverClassName: com.mysql.jdbc.Driver
      url: jdbc:mysql://xxxxxxx/xxx?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
      username: xxx
      password: xxx
    databaselist:
      - database: dev_xx1
        username: dev_xx1
        password: dev_123
      - database: dev_xx2
        username: dev_xx2
        password: dev_123
      - database: test_xx1
        username: test_xx1
        password: test_123
      - database: test_xx2
        username: test_xx2
        password: test_123

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

DataSourceContextHolder


/**
 * 数据源上下文
 */
@Slf4j
@UtilityClass
public class DataSourceContextHolder {

    /**
     * 线程级别的私有变量
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public String getDataSourceName() {
        return CONTEXT_HOLDER.get();
    }

    public void setDataSourceName(String name) {
        log.info("切换到:{}数据源", name);
        CONTEXT_HOLDER.set(name);
    }

    public void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

RoutingDataSource


/**
 * 数据源动态切换路由
 */
public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceName();
    }
}

RoutingDataSource 实现 AbstractRoutingDataSource,重写 determineCurrentLookupKey() 方法,从 DataSourceContextHolder 获取当前数据源 key,实现切换数据源。

DataSourceRegister


/**
 * 数据源注册实现类
 * ImportBeanDefinitionRegistrar 用来注册bean实例。
 * EnvironmentAware 用来读取配置。
 */
@Slf4j
public class DataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    private static final Map<String, DataSource> DATA_SOURCE_MAP = new HashMap<>();

    /**
     * 数据库连接url
     */
    private static final String URL = "jdbc:mysql://xxx/%s?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true";

    /**
     * 数据库驱动
     */
    private static final String DRIVER_CLASS_NAME = "com.mysql.jdbc.Driver";

    /**
     * 参数绑定工具
     */
    private Binder binder;

    @Override
    public void setEnvironment(Environment environment) {
        this.binder = Binder.get(environment);
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        // 注册默认的DataSource(这里是取我们yml里面配置的默认数据源相关配置,注意读取的key)
        Map<String, String> defaultDataSourceProperties = binder.bind("spring.datasource.default", Map.class).get();
        DataSource defaultDataSource = buildDataSource(defaultDataSourceProperties.get("DRIVER_CLASS_NAME"), defaultDataSourceProperties.get("URL"), defaultDataSourceProperties.get("username"), defaultDataSourceProperties.get("PASSWORD"));

        // 自定义的DataSource
        initDataSource();

        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(RoutingDataSource.class);

        // 为 RoutingDataSource 添加属性
        MutablePropertyValues values = beanDefinition.getPropertyValues();
        values.add("defaultTargetDataSource", defaultDataSource);
        values.add("targetDataSources", DATA_SOURCE_MAP);
		// 注册到 IOC 容器
        beanDefinitionRegistry.registerBeanDefinition("datasource", beanDefinition);
    }

    /**
     * 初始化自定义DataSource
     */
    private void initDataSource() {
        // 获取配置的数据库连接名
        List<Map> databaseMapList = binder.bind("spring.datasource.databaselist", Bindable.listOf(Map.class)).get();
        for (Map databaseMap : databaseMapList) {
            log.info("开始注册数据源:{}", databaseMap.get("database"));
			// 因为我的数据库连接URL都是一样,不同的是连接的数据库名,所以这里取到数据库名直接format
            String jdbcUrl = String.format(URL, databaseMap.get("database"));
            String username = databaseMap.get("username").toString();
            String password = databaseMap.get("password").toString();
            DATA_SOURCE_MAP.put(username, buildDataSource(DRIVER_CLASS_NAME, jdbcUrl, username, password));
        }
    }

    /**
     * 构建DataSource
     *
     * @param driverClassName 数据库驱动名称
     * @param url URL
     * @param username 用户名
     * @param password 密码
     * @return  DataSource对象
     */
    private DataSource buildDataSource(String driverClassName, String url, String username, String password) {
        DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
                .username(username).password(password).type(HikariDataSource.class);

        return factory.build();
    }
}

DataSourceRegister 主要是将我们自定义的 RoutingDataSource 注册到 IOC 容器中(这步注册的操作有多种方式)。

  1. DataSourceRegister 实现了 ImportBeanDefinitionRegistrar 接口,重写 registerBeanDefinitions() 向容器中注册bean。
  2. DataSourceRegister 实现了 EnvironmentAware 接口,重写 setEnvironment() 获取参数绑定对象,读取配置。
  3. 注册 RoutingDataSource 需要设置 defaultTargetDataSource 和 targetDataSources 两个属性。

ChangeDataSourceAspect


/**
 * 数据源动态切换切面类
 *
 */
@Aspect
@Component
public class ChangeDataSourceAspect {
	
	/**
	 * 切点,dao 层下 comparetable 包下所有的接口
	 */	
    @Pointcut("execution(public * com.xxx.dao.comparetable.*.*(..))")
    public void point(){}

	/**
	 * 在执行SQL前执行
	 */	
    @Before("point()")
    public void changeDataSource(JoinPoint point) {
        Object[] args = point.getArgs();
        // 从切点中获取参数
        BaseQueryCriteria criteria = (BaseQueryCriteria) args[0];
        // 从参数中获取数据源key
        DataSourceContextHolder.setDataSourceName(criteria.getSchemaName());
    }

 	/**
	 * 在执行SQL后,清除上一次的数据源key
	 */	
    @After("point()")
    public void clear() {
        DataSourceContextHolder.clearDataSource();
    }
}

ChangeDataSourceAspect 切面类会对dao层的请求做一个预处理,在执行SQL之前,执行 DataSourceContextHolder 的 setDataSourceName() 接口,根据参数将当前的数据源key保存下来。RoutingDataSource 会从 DataSourceContextHolder 获取到这个key。
在这里插入图片描述RoutingDataSource 的父类 AbstractRoutingDataSource 调用基类已实现的 determineCurrentLookupKey() 获取数据源key,从 resolvedDataSources 中获取这个key对应的数据源对象。
在这里插入图片描述
然后调用这个数据源的 getConnection()方法获取数据库连接。
在这里插入图片描述这样就完成了动态切换数据源,实现对指定的数据源进行写读操作,不用重启服务。

经过以上配置,在启动类上需要加上 @Import(DataSourceRegister.class) 才能开始使用。


@SpringBootApplication
@Import(DataSourceRegister.class)
@MapperScan("com.xxx.dao")
public class BigBangApplication {

    public static void main(String[] args) {
        SpringApplication.run(BigBangApplication.class, args);
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章