AbstractRoutingDataSource 實現動態數據源切換原理簡單分析


AbstractRoutingDataSource 實現動態數據源切換原理簡單分析

寫在前面,項目中用到了動態數據源切換,記錄一下其運行機制。

代碼展示

下面列出一些關鍵代碼,后續分析會用到(通過下面的代碼即可實現基本的動態數據源切換)

  1. 數據配置
@Configuration
@PropertySource({ "classpath:jdbc.yml" })
@EnableTransactionManagement(proxyTargetClass = true)
public class DataConfig {


    @Autowired
    private Environment env ;


    /**
     *  將jdbc相關的異常轉換為spring的異常類型
     */
    @Bean
    public BeanPostProcessor persistenceTransLation(){
        return new PersistenceExceptionTranslationPostProcessor() ;
    }

    /**
     * 多數據源
     * @return
     */
    @Bean
    public DynamicDataSource dynamicDataSource(){
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object,Object> sourceMap = new HashMap<>();
        //取得所有的datasource,DataSourceEnum里存放數據源的唯一標識
        EnumSet<DataSourceEnum> enums = EnumSet.allOf(DataSourceEnum.class);
        for(DataSourceEnum dataSource:enums){
            // map存放數據源的key和數據源
            sourceMap.put(dataSource.getKey(),generateDataSource(dataSource.getKey()));
        }
        // 👇 重點
        dynamicDataSource.setTargetDataSources(sourceMap);
        dynamicDataSource.setDefaultTargetDataSource(sourceMap.get(DataSourceEnum.TEST.getKey()));
        return dynamicDataSource;
    }

    // 讀取配置文件,創建數據源對象
    private EncryptDataSource generateDataSource(String key){
        EncryptDataSource dataSource
                = new EncryptDataSource();
        key = key.toLowerCase() ;
        String url = "jdbc.url."+key;
        String username = "jdbc.username."+key;
        String password = "jdbc.password."+key;
        dataSource.setDriverClassName("com.sybase.jdbc4.jdbc.SybDataSource");//SybDriver
        dataSource.setUrl(env.getProperty(url));
        dataSource.setUsername(env.getProperty(username));
        dataSource.setPassword(env.getProperty(password));

        //配置連接池
        dataSource.setInitialSize(Integer.parseInt(env.getProperty("jdbc.initialSize")));
        dataSource.setMaxIdle(Integer.parseInt(env.getProperty("jdbc.maxIdle")));
        dataSource.setMinIdle(Integer.parseInt(env.getProperty("jdbc.minIdle")));
        return dataSource;
    }
}

  1. 自定義數據源類
public class DynamicDataSource extends AbstractRoutingDataSource {
    // 存放數據源的id(唯一標識)
    private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<>() ;

    // 👇 重點
    @Override
    protected Object determineCurrentLookupKey() {
        return dataSourceHolder.get();
    }

    // 切換數據源
    public static void router(String sourceKey){
        if(StrUtil.isEmpty(sourceKey)){
            return;
        }
        if(DataSourceEnum.getSourceByKey(sourceKey)!=null){
            //根據目標代碼切換
            dataSourceHolder.set(DataSourceEnum.getSourceByKey(sourceKey));
        }
    }
    
    ……
        
}

  1. 數據源配置(jdbc.yml)
#測試庫
jdbc.url.test: jdbc:sybase:Tds:xxx.xxx.xxx.xxx:xx/JUDGE?charset=cp936
jdbc.username.test: xx
jdbc.password.test: xx

原理分析

第一部分已將關鍵代碼列出,該部分通過修改后即可實現數據源的切換功能。下面來分析一下流程。

AbstractRoutingDataSource 類解析

只列出了部分方法,需要詳細代碼請自行移步源碼

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources;// 目標數據源map
    @Nullable
    private Object defaultTargetDataSource;// 默認數據源
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;

    public AbstractRoutingDataSource() {
    }

    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    // 初始化 Bean 時執行
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            // 將targetDataSources屬性的值賦值給resolvedDataSources,后續需要用到resolvedDataSources
            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                // 存放數據源唯一標識和數據源對象
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }
    
    // 重寫了 getConnection 方法,ORM 框架執行語句前會調用該處
    @Override
    public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }
    
    // 同上
    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return this.determineTargetDataSource().getConnection(username, password);
    }

    // 👇 重點
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        // 調用我們重寫的determineCurrentLookupKey方法,返回的是數據源的唯一標識
        Object lookupKey = this.determineCurrentLookupKey();
        // 從map中查詢該標識對應的數據源,然后方法返回該數據源,調用 getConnection 打開對應連接
        DataSource 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 + "]");
        } else {
            return dataSource;
        }
    }

    // 鈎子方法,供我們重寫
    @Nullable
    protected abstract Object determineCurrentLookupKey();
}

總結與閑談

綜上,可以列出以下幾點描述整個流程:

  1. 自定義類繼承 AbstractRoutingDataSource,重寫 determineCurrentLookupKey(),返回數據源的唯一標識;
  2. 將數據源名稱和數據源封裝為 map,調用 AbstractRoutingDataSource 類的 setTargetDataSources() 設置目標數據源。AbstractRoutingDataSource 類實現了 InitializingBean 接口,重寫了 afterPropertySet()(對該方法不熟悉的話請回顧一下 Bean 的生命周期,該方法在 Bean 的屬性注入后執行),該方法內部對 resolvedDataSources 屬性賦值(將 targetDataSources 的值放進去),后續會用到 resolvedDataSources ;
  3. AbstractRoutingDataSource 實現了 DataSource 接口,重寫了 getConnection(),當 ORM 框架執行 sql 語句前總是執行 getConnection(),然后就調用到了重寫后的 getConnection(),該方法內部調用了 AbstractRoutingDataSource 類的 determineTargetDataSource()
  4. determineTargetDataSource() 內部調用了自定義類重寫的 determineCurrentLookupKey(),返回數據源的映射,然后從 resolvedDataSources(map) 屬性獲取到數據源,進行后續的操作。

(題外話)想要實現數據源切換可以有兩種實現:

  1. 手動切換數據源,每次執行相應操作前調用 router 方法切換;
  2. 還有一種思路就是利用 AOP,設計一個注解,注解內添加數據源唯一標識的屬性,然后對方法添加注解,AOP 代碼進行攔截,然后將唯一標識賦值給 ThreadLocal 變量即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM