天天看點

springBoot+mybatis 動态切換資料源動态資料源實作原理

動态資料源實作原理

實作動态資料源的切換,核心抽象類AbstractRoutingDataSource,下面貼上部分核心源碼代碼:

詳解: spring容器連接配接哪個資料源,是由該方法決定而具體選擇哪個資料源又是由determineCurrentLookupKey()方法的傳回值決定的,該方法需要我們繼承AbstractRoutingDataSource來重寫。該方法傳回一個key,該key就是資料源bean的beanName,我們根據key去resolvedDataSources中取到DataSource,然後将DataSource傳回,進行後續的操作。後續連接配接資料庫的操作.

/**
    * Retrieve the current target DataSource. Determines the
    * {@link #determineCurrentLookupKey() current lookup key}, performs
    * a lookup in the {@link #setTargetDataSources targetDataSources} map,
    * falls back to the specified
    * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
    * @see #determineCurrentLookupKey()
    */
   protected DataSource determineTargetDataSource() {
   	Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
   	Object lookupKey = determineCurrentLookupKey();
   	DataSource dataSource = this.resolvedDataSources.get(lookupKey);
   	if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
   		dataSource = this.resolvedDefaultDataSource;
   	}
   	if (dataSource == null) {
   		throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
   	}
   	return dataSource;
   }

   /**
    * Determine the current lookup key. This will typically be
    * implemented to check a thread-bound transaction context.
    * <p>Allows for arbitrary keys. The returned key needs
    * to match the stored lookup key type, as resolved by the
    * {@link #resolveSpecifiedLookupKey} method.
    */
   @Nullable
   protected abstract Object determineCurrentLookupKey();
           

項目配置

springBoot添加 mybatis,mysql,Orcale,AOP等依賴此處不再展示,後續會附上源碼,供大家參考,

SpringBoot啟動類配置添加@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}),用以禁用預設的自動配置,

/**
 * Hawk webservice Application
 *
 * @author husg
 */
//禁用springboot自帶的資料源注入,初始化會讀取yaml檔案下的單資料源
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@MapperScan("com.lenovo.hawk.mapper")
@EntityScan(basePackages = {"com.lenovo.hawk"})
@EnableScheduling
@EnableAsync
@Slf4j
public class HawkDashboardApplication {
	//TODO
}
           

資料源配置類

建立一個資料源配置類,主要做以下幾件事情:

  1. 配置 dao,model,xml mapper檔案的掃描路徑。
  2. 注入資料源配置屬性,建立master、slave資料源。
  3. 建立一個動态資料源,并裝入master、slave資料源。
  4. 将動态資料源設定到SQL會話工廠和事務管理器。
動态資料源核心配置類,此處會将用到的資料源加載裝配到對應的上下文對象中,注意因為
不想使用過多的SqlSessionFactory,此處隻配置一個動态資料源切換使用一個即可,簡化代碼.
           
package com.lenovo.hawk.config;

import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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 org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

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

/**
 *  資料源配置資訊
 * @author Hsg
 */
@Configuration
@Slf4j
public class DruidConfig {

    /**
     * Default DataSource
     *
     * @return
     */
    @Bean(name = "druidMySQLDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.mysql")
    @ConditionalOnProperty(name = "master.type", havingValue = "mysql", matchIfMissing = true)
    public DataSource druidMySQLDataSource() {
        return new DruidDataSource();
    }

    @Bean(name = "druidOracleDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.druid.oracle")
    @ConditionalOnProperty(name = "slave.type", havingValue = "oracle", matchIfMissing = true)
    public DataSource druidOracleDataSource() {
        return new DruidDataSource();
    }

    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(){
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //配置預設資料源
        dynamicDataSource.setDefaultTargetDataSource(druidMySQLDataSource());
        //配置多資料源
        Map<Object,Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("masterDataSource",druidMySQLDataSource());
        dataSourceMap.put("slaveDataSource",druidOracleDataSource());
        dynamicDataSource.setDataSources(dataSourceMap);
        return dynamicDataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        // 配置資料源,此處配置為關鍵配置,如果沒有将 dynamicDataSource作為資料源則不能實作切換
        sessionFactory.setDataSource(dynamicDataSource());
        sessionFactory.setTypeAliasesPackage("com.lenovo.hawk.pojo");    // 掃描Model
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));    // 掃描映射檔案
        return sessionFactory;

    }

    /**
     * 事務配置  使用事務時在方法頭部添加@Transactional注解即可
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(){
        return new DataSourceTransactionManager(dynamicDataSource());
    }

}
           

三級标題

動态資料源擷取,開始講解到 extends AbstractRoutingDataSource 講需要的用到的資料源裝載
           
package com.lenovo.hawk.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;

/**
 * 配置動态資料源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSource();
    }

    /**
     * 設定資料源
     * @param dataSources
     */
    public void setDataSources(Map<Object, Object> dataSources) {
        super.setTargetDataSources(dataSources);
        // 将資料源的 key 放到資料源上下文的 key 集合中,用于切換時判斷資料源是否有效
        DataSourceHolder.addDataSourceKeys(dataSources.keySet());
    }

}

           

資料源上下文

動态資料源的切換主要是通過調用這個類的方法來完成的。在任何想要進行切換資料源的時候都可以通過調用這個類的方法實作切換。比如系統登入時,根據使用者資訊調用這個類的資料源切換方法切換到使用者對應的資料庫
           
package com.lenovo.hawk.config;

import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 *  動态操作資料源配置類
 *  @author Hsg
 */
@Slf4j
public class DataSourceHolder {

    /**
     * 單線程配置
     */
    public static final ThreadLocal<String> dataSources = new ThreadLocal<>();

    /**
     * 資料源配置
     */
    public static void setDataSource(String dataSourceType){
        log.info("set dataSource info {}",dataSourceType);
        dataSources.set(dataSourceType);
    }

    /**
     * 擷取資料源
     */
    public static String getDataSource(){
        log.info("get dataSource info {}",dataSources.get());
        return dataSources.get();
    }

    /**
     * 清除資料源
     */
    public static void clearDataSource(){
        dataSources.remove();
    }

    /**
     * 資料源的 key集合,用于切換時判斷資料源是否存在
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * 添加資料源keys
     * @param keys
     * @return
     */
    public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }

    /**
     * 判斷是否包含資料源
     * @param key 資料源key
     * @return
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }

}
           

注解式資料源

我們實作通過注解的方式來進行資料源的切換,原理就是添加注解(如@DataSource(value="master")),然後實作注解切面進行資料源切換。
建立一個動态資料源注解,擁有一個value值,用于辨別要切換的資料源的key。
           
package com.lenovo.hawk.constant;

import java.lang.annotation.*;

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

    String value();

}
           

資料源切換表示key

package com.lenovo.hawk.constant;

public class DataSourceType {

    public static final String MASTER = "masterDataSource";

    public static final String SLAVE = "slaveDataSource";
}
           

切面配置

建立一個AOP切面,攔截帶 @DataSource 注解的方法,在方法執行前切換至目标資料源,執行完成後恢複到預設資料源。;
           
package com.lenovo.hawk.aop;

import com.lenovo.hawk.config.DataSourceHolder;
import com.lenovo.hawk.constant.TargetDataSource;
import lombok.extern.slf4j.Slf4j;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 動态資料源切換處理器
 * @author Hsg
 * @date    2020/07/17
 */
@Aspect
@Order(-1)  // 該切面應當先于 @Transactional 執行
@Component
@Slf4j
public class DynamicDataSourceAspect {

    /**
     * 切換資料源
     * @param point
     * @param dataSource
     */
    @Before("execution(* com.lenovo.hawk.service..*.*(..)) && @annotation(com.lenovo.hawk.constant.TargetDataSource) && @annotation(dataSource))")
    public void switchDataSource(JoinPoint point, TargetDataSource dataSource) {
        if (!DataSourceHolder.containDataSourceKey(dataSource.value())) {
            log.info("DataSource [{}] doesn't exist, use default DataSource [{}] " ,  dataSource.value());
        } else {
            // 切換資料源
            DataSourceHolder.setDataSource(dataSource.value());
            log.info("Switch DataSource to [{}] in Method [{}]",DataSourceHolder.getDataSource(),point.getSignature());
        }
    }

    /**
     * 重置資料源
     * @param point
     * @param dataSource
     */
    @After("@annotation(dataSource))")
    public void restoreDataSource(JoinPoint point, TargetDataSource dataSource) {
        // 将資料源置為預設資料源
        DataSourceHolder.clearDataSource();
        log.info("Restore DataSource to [{}] in Method [{}]" , DataSourceHolder.getDataSource(),point.getSignature());
    }
}
           

service層實作

@Service
@Transactional(readOnly = true)
public class CompServiceImpl implements CompService {

    @Autowired
    CompMapper compMapper;

    @Override
    @TargetDataSource(DataSourceType.SLAVE)
    public Integer findAll() {
        return compMapper.findAll();
    }
}
           

至此,springBoot整合Mybatis動态資料源切換全部完成

說明: 項目要求需要使用orcale資料庫,相關的pom依賴無法下載下傳,此處未添加orcale依賴包,可自行更改資料源配置

如連結無法打開,請複制連結至位址欄自行直接通路
           

源碼下載下傳

碼雲: https://gitee.com/wangshisuifeng123/springbootDynamicMybatis.git