利用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);
    }
}

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