原创

Spring 动态数据源管理与切换

Spring基于Bean对所有的组件进行管理,通过Bean解决各个模块之间的依赖关系,因此需要深入理解BeanFactory的机制。例如在本例中,如果要实现在查询的过程中,针对不同用户或者模块配置的不同的数据表,进行动态切换数据源(更改数据库连接、用户名和密码)进行查询的功能,需要理解Bean之间的依赖和调用关系。首先,配置数据库的链接,可以使用连接池或Spring自带的链接管理类,本例中使用DHCP的连接池实现链接的管理,通过读取配置文件中的链接,从而建立数据库的链接,配置文件如下所示:

<!--加载properties文件,供其他bean使用,用${...}来调用 -->

<context:property-placeholder location="classpath:inspur.properties" />

 

<bean id="sys_dataSource" class="org.apache.commons.dbcp.BasicDataSource"

destroy-method="close" autowire="byName">

<property name="driverClassName">

<value>${jdbc.driver}</value>

</property>

<property name="url">

<value>${jdbc.url}</value>

</property>

<property name="username">

<value>${jdbc.username}</value>

</property>

<property name="password">

<value>${jdbc.password}</value>

</property>

<property name="maxActive">

<value>${jdbc.maxActive}</value>

</property>

<property name="maxIdle">

<value>${jdbc.maxIdle}</value>

</property>

<property name="testWhileIdle">

<value>true</value>

</property> <!-- 打开检查,用异步线程evict进行检查 -->

<property name="testOnBorrow">

<value>false</value>

</property>

<property name="testOnReturn">

<value>false</value>

</property>

<property name="validationQuery">

<value>select 1 from dual</value>

</property>

<property name="timeBetweenEvictionRunsMillis">

<value>30000</value>

</property>

<property name="numTestsPerEvictionRun">

<value>${jdbc.maxActive}</value>

</property>

</bean>


${jdbc.driver}、${jdbc.url}、${jdbc.username}、${jdbc.password}即为从配置文件中读取的数值,本链接使用的class为org.apache.commons.dbcp.BasicDataSource即DHCP连接池,id为sys_dataSource,可以供其他Bean使用ref=sys_dataSource进行调用,具体可见Spring中Bean配置MybatisSpring整合之后,不需要自己声明SQLSessionFactory,是因为使用了依赖注入的方式,实现了SQLSessionFactory Bean,配置文件如下:

<!-- MyBatis配置 -->

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="multipleDataSource" />

<property name="configLocation" value="mybatis_core.xml" />

<!-- 显式指定Mapper文件位置 -->

<property name="mapperLocations"

value="classpath:com/inspur/plugins/metaconf/conf/mybatis_*.xml" />

</bean>

 

<!-- 扫描basePackage下所有以@MyBatisRepository标识的 接口 -->

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

<property name="basePackage" value="com.inspur.plugins.metaconf.dao" />

<property name="annotationClass"

value="com.inspur.system.repository.mybatis.MyBatisRepository" />

</bean>


其中的ref便是指向数据源连接的Beanid,如果不需要动态数据源切换,应指向上文中配置的sys_dataSource。如果要进行动态数据源的切换,则需要根据Spring执行数据源链接的过程,重写其部分方法,并交于Spring管理。SpringAbstractRoutingDataSource中进行数据库的链接,本类实现了InitializingBean接口,即Spring启动时会加载该类中的方法,如果要实现数据源动态切换,需要继承该类,并重写其中的方法,其中determineTargetDataSource()方法会在选择数据源时执行,determineCurrentLookupKey()决定选择数据源的名称,具体可以阅读Spring的源码,使用afterPropertiesSet()方法保存数据源,使用的方式为key-valueMap形式,Map的变量名为targetDataSource,即所有的数据源链接皆存于targetDataSourceMap中,通过key-value的方式进行选择,实现动态数据源切换,可以重写determineTargetDataSource()方法,在该方法中,根据每一个查询线程创建一个新的数据源,并保存到targetDataSource中,从而实现数据源的动态切换。具体方法可见代码如下:

package com.inspur.plugins.metaconf.comm;

 

import java.util.HashMap;

import java.util.Map;

 

import javax.sql.DataSource;

import javax.transaction.SystemException;

 

import org.apache.commons.dbcp.BasicDataSource;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.apache.log4j.Logger;

import org.springframework.beans.factory.annotation.Autowired;

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

import org.springframework.stereotype.Component;

 

import com.inspur.plugins.metaconf.model.DataSourceVO;

import com.inspur.plugins.metaconf.model.Dbs;

import com.inspur.plugins.metaconf.model.MetaConf;

import com.inspur.plugins.metaconf.model.MetaModelConf;

 

/**

 * 数据源动态切换类

 * <功能详细描述>

 *

 * @author  Jeff.Wang

 * @version  [版本号, 2018年7月13日]

 * @see  [相关类/方法]

 * @since  [产品/模块版本]

 */

@Component

public class DynamicDataSource extends AbstractRoutingDataSource

{

 

    //@Autowired

    //private CenterDatabaseManager centerDatabaseManager;

   

    private Logger log = Logger.getLogger(this.getClass());

    // 默认数据源,也就是主库

    protected DataSource masterDataSource;

    // 保存动态创建的数据源

    private static final Map targetDataSource = new HashMap();

   

    @Override

    public DataSource determineTargetDataSource() {

        // 根据数据库选择方案,拿到要访问的数据库

        log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>根据数据库选择方案,拿到要访问的数据库");

        String dataSourceName = determineCurrentLookupKey();

        if("dataSource".equals(dataSourceName)) {

            // 访问默认主库

            return masterDataSource;

        }

       

        // 根据数据库名字,从已创建的数据库中获取要访问的数据库

        DataSource dataSource = (DataSource) targetDataSource.get(dataSourceName);

        if(null == dataSource) {

            // 从已创建的数据库中获取要访问的数据库,如果没有则创建一个

            try

            {

                dataSource = this.selectDataSource(dataSourceName);

            }

            catch (SystemException e)

            {

                // TODO Auto-generated catch block

                e.printStackTrace();

            }

        }

        return dataSource;

    }

   

    @Override

    protected String determineCurrentLookupKey() {

        // TODO Auto-generated method stub

        log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>determineCurrentLookupKey()");

        String dataSourceName = Dbs.getDbType();

        if (dataSourceName == null || dataSourceName == "dataSource") {

            // 默认的数据源名字

            dataSourceName = "dataSource";

        }

        log.debug("use datasource : " + dataSourceName);

        return dataSourceName;

    }

 

    public void addTargetDataSource(String key, BasicDataSource dataSource) {

        log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>addTargetDataSource()");

        this.targetDataSource.put(key, dataSource);

        //setTargetDataSources(this.targetDataSource);

    }

 

   

    /**

     * 该方法为同步方法,防止并发创建两个相同的数据库

     * 使用双检锁的方式,防止并发

     * @param dbType

     * @return

     * @throws SystemException

     */

    private synchronized DataSource selectDataSource(String dbType) throws SystemException {

        log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>selectDataSource()");

        // 再次从数据库中获取,双检锁

        DataSource obj = (DataSource)this.targetDataSource.get(dbType);

        if (null != obj) {

            return obj;

        }

        // 为空则创建数据库

        BasicDataSource dataSource = this.getDataSource(dbType);

        if (null != dataSource) {

            // 将新创建的数据库保存到map中

            this.setDataSource(dbType, dataSource);

            return dataSource;

        }else {

            throw new SystemException("创建数据源失败!");

        }

    }

   

    /**

     * 查询对应数据库的信息

     * @param dbtype

     * @return

     */

    private BasicDataSource getDataSource(String dbType) {

        log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>getDataSource()");

        String oriType = Dbs.getDbType();

        // 先切换回主库

        Dbs.setDbType("dataSource");

        // 查询所需信息

        MetaModelConf model = MetaConf.getInstance().getModelById(dbType);

        DataSourceVO database = model.getDataSource();

        // 切换回目标库

        Dbs.setDbType(oriType);

       

       /* String url = "jdbc:sqlserver://" + datebase.getIp() + ":1433"

                + ";DatabaseName=" + datebase.getDatabaseName();*/

       

        BasicDataSource dataSource = createDataSource(database.getDriver(),database.getUrl(),database.getUsername(),database.getPassword());

        return dataSource;

    }

   

    //创建数据源

    private BasicDataSource createDataSource(String driverClassName, String url,

            String username, String password) {

        BasicDataSource dataSource = new BasicDataSource();

        dataSource.setDriverClassName(driverClassName);

        dataSource.setUrl(url);

        dataSource.setUsername(username);

        dataSource.setPassword(password);

        dataSource.setMaxActive(5);

        dataSource.setMaxIdle(2);

        dataSource.setTestWhileIdle(true);

        dataSource.setValidationQuery("select sysdate from dual");

       

        return dataSource;

    }

 

    public void setDataSource(String type, BasicDataSource dataSource) {

        this.addTargetDataSource(type, dataSource);

        Dbs.setDbType(type);

    }

 

    /**

     * 设置数据库 id 和 DataSource

     */

    public void setDataSource(String sourceId, DataSourceVO dataSourceVO){

        BasicDataSource dataSource =

                this.createDataSource(dataSourceVO.getDriver(), dataSourceVO.getUrl(),

                        dataSourceVO.getUsername(), dataSourceVO.getPassword());

        this.addTargetDataSource(sourceId, dataSource);

        Dbs.setDbType(sourceId);

    }

   

    /*@Override

    public void setTargetDataSources(Map targetDataSources) {

        // TODO Auto-generated method stub

        super.setTargetDataSources(targetDataSources);

        // 重点:通知container容器数据源发生了变化

        afterPropertiesSet();

    }*/

   

   

    /**

     * 该方法重写为空,因为AbstractRoutingDataSource类中会通过此方法将,targetDataSources变量中保存的数据源交给resolvedDefaultDataSource变量

     * 在本方案中动态创建的数据源保存在了本类的targetDataSource变量中。如果不重写该方法为空,会因为targetDataSources变量为空报错

     * 如果仍然想要使用AbstractRoutingDataSource类中的变量保存数据源,则需要在每次数据源变更时,调用此方法来为resolvedDefaultDataSource变量更新

     */

    @Override

    public void afterPropertiesSet() {

        log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>afterPropertiesSet()");

    }

 

    public DataSource getMasterDataSource() {

        return masterDataSource;

    }

 

    public void setMasterDataSource(DataSource masterDataSource) {

        this.masterDataSource = masterDataSource;

    }

   

   

}


 

 

Dbs的代码如下:

package com.inspur.plugins.metaconf.model;

 

/**

 * 负责切换数据源

 * <功能详细描述>

 *

 * @author  Jeff.Wang

 * @version  [版本号, 2018年7月12日]

 * @see  [相关类/方法]

 * @since  [产品/模块版本]

 */

public class Dbs

{

    private static final ThreadLocal<String> local = new ThreadLocal<String>();

   

    public static String getDbType(){

        return local.get();

    }

   

    public static void setDbType(String dbName){

        local.set(dbName);

    }

   

    public static void clear(){

        local.remove();

    }

}


如果要实现动态数据源切换,则需要以后所有的需要数据源连接的地方,ref的值指向该数据源动态管理类,例如Mybatis中的数据源管理:

<!-- 数据源动态管理的Bean -->

<bean id="multipleDataSource"  class="com.inspur.plugins.metaconf.comm.DynamicDataSource">

          <property name="masterDataSource"  ref="sys_dataSource"></property>

 </bean>

<!-- MyBatis配置 -->

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="multipleDataSource" />

…………………………………………………………………省略

</bean>

此后在进行查询时,只需要调用setDataSource()方法即可实现数据源的动态切换。

 

可参考: https://www.cnblogs.com/wsss/p/5475057.html 以及 http://blog.51cto.com/chrischen/1878517

正文到此结束
Loading...