运维开发网

Java+Spring+MyBatis实现多数据源的动态切换

运维开发网 https://www.qedev.com 2020-02-23 13:19 出处:51CTO 作者:buyinuan
通过Java+Spring+Mybatis实现多数据源的动态切换

    在实际的项目开发过程中我们经常会遇到一个项目需要使用多个数据源的情况,而多数据源又可分为固定多数据源和动态多数据源两种情况。

    固定多数据源是指在项目中需要使用多个数据源,但数据源的个数是确定的,不会改变,如我们的项目需要使用订单库和商品库这两个数据源,项目中所有的业务逻辑都只需要操作这两个库。动态多数据源是指在项目需要使用多数据源,但是数据源的个数不确定,可能会随着项目的需要动态的新增或删除数据源。下面我将会对这两中情况分别说明如何通过Java + Spring实现多数据源的动态切换。

    多数据源的动态切换都是通过重载Spring中的AbstractRoutingDataSource类来实现的。

固定多数据源切换

    固定多数据源的动态切换,通过自定义注解实现切换,这样在切换数据源时比较灵活,具体的实现方式如下:

    1、配置多数据源

    <!--定义数据源1-->
    <bean id="oracledataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
        <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1522:neworcl" />
        <property name="username" value="emspdadev" />
        <property name="password" value="emspdadev" />
        <!-- 初始化连接大小 -->
        <property name="initialSize" value="0"></property>
        <!-- 连接池最大数量 -->
        <property name="maxActive" value="20"></property>
        <!-- 连接池最大空闲 -->
        <property name="maxIdle" value="20"></property>
        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="1"></property>
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="60000"></property>
    </bean>

    <!--定义数据源2-->
    <bean id="mysqldataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/jbpmdb" />
        <property name="username" value="root" />
        <property name="password" value="123456" />
        <!-- 初始化连接大小 -->
        <property name="initialSize" value="0"></property>
        <!-- 连接池最大数量 -->
        <property name="maxActive" value="20"></property>
        <!-- 连接池最大空闲 -->
        <property name="maxIdle" value="20"></property>
        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="1"></property>
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="60000"></property>
    </bean>

    <!--动态数据源配置-->
    <bean id="dataSource" class="com.ssm.datasource.DynamicDataSource">
	<!--引入定义好的数据源-->
        <property  name="targetDataSources">
            <map  key-type="java.lang.String">
              <entry key="oracle" value-ref="oracledataSource" />
              <entry key="mysql" value-ref="mysqldataSource" />
            </map>
        </property>
	<!--定义默认数据源-->
        <property name="defaultTargetDataSource" ref="oracledataSource" />
    </bean>

    <!--spring和mybatis整合-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="mapperLocations" value="classpath:mapping/*.xml" />
    </bean>
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.ssm.dao" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    </bean>

    2、定义注解(注解名为DataSource),用于切换数据源,注解的值只能为上述配置中定义的key(对应于上面配置中定义的oracle、mysql)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
    String value();
}

    3、根据Sping切面编程,当调用指定的切面类时,解释注解,并根据注解的定义使用对应的数据库

public class DataSourceAspect {

    /**
    * 定义切面,当调用com.ssm.service下的所有类的所有方法前都会执行beforeInvoke方法
    */
    @Pointcut("execution(* com.ssm.service.*.*(..))")
    public void pointCut(){};

    @Before(value = "pointCut()")
    public void beforeInvoke(JoinPoint joinpoint) {
        try {
            String clazzName = joinpoint.getTarget().getClass().getName();
            String methodName = joinpoint.getSignature().getName();
            Class targetClazz = Class.forName(clazzName);
            Method[] methods = targetClazz.getMethods();
            for(Method method : methods) {
                if(method.getName().equals(methodName)) {
                    // 首先查看方法是否使用注解
                    // 如果使用注解,则获取注解定义的值,并根据注解的值设置访问数据库的key
                    if(method.isAnnotationPresent(DataSource.class)) {
                        DataSource dataSource = method.getAnnotation(DataSource.class);
                        DatasourceHolder.setDataType(dataSource.value());
                    }
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

    4、定义动态切换数据源(继承Spring的AbstractRoutingDataSource)

public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
    * 根据DatasourceHolder中DataType的值获取具体的数据源
    */
    @Override
    protected Object determineCurrentLookupKey() {
        return DatasourceHolder.getDataType();
    }
}

    5、数据源切换的使用

@Service
public class IdxServiceImpl implements IIdxSevice {

    @Autowired
    private IdxMapper idxMapper;

    @Override
    public List<Idx> listIdxInfo() {
        return null;
    }

    /**
    * 根据注解的配置,会访问oracle对应的数据源
    */
    @Override
    @DataSource("oracle")
    public Map<String,Object> getIdxById(int idxId) {
        return idxMapper.getIdxById(idxId);
    }

    /**
    * 根据注解的配置,会访问mysql对应的数据源
    */
    @Override
    @DataSource("mysql")
    public Map<String, Object> getJobInfo(int dbId) {
        return idxMapper.getJobInfo(dbId);
    }
}

通过以上的步骤即实现了数据源的动态切换

动态多数据源切换

    对于动态的多数据源,数据源的配置一般不放在配置文件中,因为如果放在配置文件中,每次新增或删除数据源,都需要重启项目,这样的实现方式非常不友好;通常情况向数据源的配置放在数据库中。实现方式如下:

    1、配置数据源,这里配置的数据源用于保存其他数据源的配置信息,今后数据的新增、删除、修改均在该数据库中操作,配置如下:

    <!--定义数据源-->
    <bean id="oracledataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
        <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1522:neworcl" />
        <property name="username" value="cfgmanage" />
        <property name="password" value="cfgmanage" />
        <!-- 初始化连接大小 -->
        <property name="initialSize" value="0"></property>
        <!-- 连接池最大数量 -->
        <property name="maxActive" value="20"></property>
        <!-- 连接池最大空闲 -->
        <property name="maxIdle" value="20"></property>
        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="1"></property>
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="60000"></property>
    </bean>

    <!--查询动态配置的数据库连接信息-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="oracledataSource" />
    </bean>
    <bean id="dbConfigService" class="com.teamsun.datasource.DBConfigService">
        <property name="jdbcTemplate" ref="jdbcTemplate" />
    </bean>

    <!--定义动态数据源-->
    <bean id="dataSource" class="com.teamsun.datasource.DynamicDataSource">
        <property name="masterDataSource" ref="oracledataSource" />
        <property name="dbConfigService" ref="dbConfigService" />
    </bean>

    <!--spring和mybatis整合-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="mapperLocations" value="classpath:mapper/*.xml" />
        <!--<property name="mapperLocations" value="classpath:mapping/*.xml" />-->
    </bean>
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.teamsun.mapper" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    </bean>

2、实现查询数据源配置信息的类

public class DBConfigService {

    private JdbcTemplate jdbcTemplate;

    /**
     * 查询数据库配置信息
     * @param dbName  数据库名称
     * @return 数据库配置信息
     */
    public DBCfg getDBCfg(String dbName) throws Exception {
        String querySql = "select\n" +
                "          t.db_type as \"dbType\",\n" +
                "           t.db_name as \"dbName\",\n" +
                "           t.db_comment as \"dbCommment\",\n" +
                "           t.db_driver as \"driverClass\",\n" +
                "           t.db_username as \"userName\",\n" +
                "           t.db_password as \"passworld\",\n" +
                "           t.db_url as \"jdbcURL\"" +
                "          from TB_RPT_DBCFG t\n" +
                "          where t.db_name = '" + dbName + "'";

        RowMapper<DBCfg> rowMapper = ParameterizedBeanPropertyRowMapper.newInstance(DBCfg.class);
        DBCfg dbCfg = (DBCfg) jdbcTemplate.queryForObject(querySql, rowMapper);
        return dbCfg;
    }

    public JdbcTemplate getJdbcTemplate() {
        return jdbcTemplate;
    }

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
}

3、实现动态切换数据源

/**
 * <p>动态创建及访问多数据源</p>
 */
public class DynamicDataSource extends AbstractRoutingDataSource{

    private DBConfigService dbConfigService;

    private DataSource masterDataSource;

    private Map<Object, Object> targetDataSource = new HashMap<Object, Object>();

    private static final String DEFAULT_DB_NAME = "dataSource";  // 默认数据库名

    private static final Logger LOGGER = Logger.getLogger(DynamicDataSource.class);

    /**
     * 创建并获取数据源
     * @return
     */
    @Override
    protected DataSource determineTargetDataSource() {
        // 获取数据源名称
        String dbName = (String) determineCurrentLookupKey();

        // 获取默认数据源
        if(DEFAULT_DB_NAME.equals(dbName)) {
            return masterDataSource;
        }

        // 创建数据源
        DataSource dataSource = (DataSource) targetDataSource.get(dbName);
        try {
            if (dataSource == null) {
                dataSource = getDataSourceByName(dbName);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    /**
     * 获取数据库名称,可根据获取的数据库名称查询数据库配置信息,
     * 通过配置信息动态创建数据源
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        String dbName = DatasourceHolder.getDBName();
        if(StringUtils.isEmpty(dbName)) {
            dbName = DEFAULT_DB_NAME;
        }

        DatasourceHolder.remove();
        return dbName;
    }

    @Override
    public void afterPropertiesSet() {

    }

    /**
     * 通过数据库的配置信息获取数据源
     * @param dbName 数据库名称
     * @return
     */
    public synchronized DataSource getDataSourceByName(String dbName) throws Exception {
        
        // 创建数据源
        BasicDataSource dataSource = createDataSource(dbName);
        
        // 如果创建数据源成功则缓存数据源,避免重复创建相同的数据源
        if(dataSource != null) {
            targetDataSource.put(dbName, dataSource);
        }
        return  dataSource;
    }

    /**
     * 通过数据库的配置创建数据源
     * @param dbName 数据库名称
     * @return
     */
    public BasicDataSource createDataSource(String dbName) throws Exception {
        
        // 查询动态数据源配置信息
        String oriDBName = DatasourceHolder.getDBName();

        if(dbConfigService == null) {
            System.out.println("创建数据源失败[dbCfgService is null......]");
            LOGGER.debug("创建数据源失败[dbCfgService is null......]");
        }

        // 通过数据库名称查询相关的数据库配置信息
        DatasourceHolder.setDBName(DEFAULT_DB_NAME);
        DBCfg dbCfg = dbConfigService.getDBCfg(dbName);
        DatasourceHolder.setDBName(oriDBName);

        String driver = dbCfg.getDriverClass();  // 数据库驱动
        String url = dbCfg.getJdbcURL();  // 数据库连接地址
        String username = dbCfg.getUserName();  // 数据库用户名
        String password = dbCfg.getPassworld();  // 数据库密码

        LOGGER.debug("动态连接的数据库为[" + url + "|" + username + "]");

        // 创建数据源
        BasicDataSource basicDataSource = new BasicDataSource();
        basicDataSource.setDriverClassName(driver);
        basicDataSource.setUrl(url);
        basicDataSource.setUsername(username);
        basicDataSource.setPassword(password);
        basicDataSource.setTestWhileIdle(true);

        return basicDataSource;
    }

    /**
     * 如果修改或删除数据源的配置,则需要同步删除缓存的数据源
     * @param dbName
     */
    public void removeDataSource(String dbName) {
        this.targetDataSource.remove(dbName);
    }

    public DataSource getMasterDataSource() {
        return masterDataSource;
    }

    public void setMasterDataSource(DataSource masterDataSource) {
        this.masterDataSource = masterDataSource;
    }

    public DBConfigService getDbConfigService() {
        return dbConfigService;
    }

    public void setDbConfigService(DBConfigService dbConfigService) {
        this.dbConfigService = dbConfigService;
    }
}

4、使用动态切换数据源

public class ShowRptServiceImpl implements IShowRptService {

    private static final Logger LOGGER = Logger.getLogger(ShowRptServiceImpl.class);

    @Autowired
    private DBCfgMapper dbCfgMapper;

    @Autowired
    private ShowRptInfoMapper showRptInfoMapper;

    @Override
    public RptResult queryRptInfo(BaseRpt baseRpt, Map<String, String> params) {
        // 在调用Mybatis执行数据库之前先选择数据源
        DatasourceHolder.setDBName(dbCfg.getDbName());
        // 查询报表数据
        List<Map<String,Object>> resultList = showRptInfoMapper.queryRptData(querySQL);


        // 选择数据源
        DatasourceHolder.setDBName(dbCfg.getDbName());
        // 查询数据数据量
        int totalCount = showRptInfoMapper.queryTotalCount(countSQL);

        RptResult rptResult = new RptResult();
        return rptResult;
    }
 }

通过以上步骤即可实现动态多数据源的动态切换

由于篇幅有限完整的源码信息可关注微信公众号 布衣暖 回复java获取

Java+Spring+MyBatis实现多数据源的动态切换

0

精彩评论

暂无评论...
验证码 换一张
取 消