JAVA中使用代码创建多数据源,并实现动态切换(一)

摘要: 案例基于Spring+mybatis。 设计的目的:解决不确定(个数和种类)数据源的切换问题

2017-06-06 11:31:57补充:近日,在本文的基础之上,扩展了下,使用atomikos来管理事务,保证多数据源操作时,事务一致性。(https://my.oschina.net/simpleton/blog/916108)

另外,感谢朋友对本文的关注和对博主的支持,最近有很多朋友联系我希望深入探讨下本文涉及内容,不过由于近日太忙,没有及时回复大家,请见谅。

近日,博主有个业务需求,就是根据数据库存储的不同数据源信息,动态创建数据源并实现业务不同而转到不同的数据源上处理。

数据库存储起来的数据源信息是不确定的,可以删除和添加,这些是业务前提。

在网上找了下相关资料,对于使用Spring配置,直接配置多套数据源,使用AOP动态切换的方式居多,这种方式博主以前也使用过,很强大。不过有个前提就是多个数据源的信息是预先就确定的。那么对于不确定数据源信息的业务需求,就只有使用代码动态实现数据源初始化和销毁操作了。

好了,有了这些思路,可以开始准备写代码了。

1、创建一个线程上下文对象(使用ThreadLocal,保证线程安全)。上下文对象中主要维护了数据源的KEY和数据源的地址等信息,当KEY对应的数据源找不到时,根据数据源地址、驱动和用户名等创建 一个数据源,这里也是业务中需要解决的一个核心问题(JAVA动态创建数据源)。

/**
* Copyright (c) 2015 - 2016 eay Inc.
* All rights reserved.
*/
package com.eya.pubservice.datasource;

import java.util.HashMap;
import java.util.Map;

/**
* 当前正在使用的数据源信息的线程上线文
* @create ll
* @createDate 2017年3月27日 下午2:37:07
* @update
* @updateDate
*/
public class DBContextHolder {
/* 数据源的KEY /
public static final String DATASOURCE_KEY = “DATASOURCE_KEY”;
/* 数据源的URL /
public static final String DATASOURCE_URL = “DATASOURCE_URL”;
/* 数据源的驱动 /
public static final String DATASOURCE_DRIVER = “DATASOURCE_DRIVER”;
/* 数据源的用户名 /
public static final String DATASOURCE_USERNAME = “DATASOURCE_USERNAME”;
/* 数据源的密码 /
public static final String DATASOURCE_PASSWORD = “DATASOURCE_PASSWORD”;

private static final ThreadLocal<Map<String, Object>> contextHolder = new ThreadLocal<Map<String, Object>>();

public static void setDBType(Map<String, Object> dataSourceConfigMap) {
    contextHolder.set(dataSourceConfigMap);
}

public static Map<String, Object> getDBType() {
    Map<String, Object> dataSourceConfigMap = contextHolder.get();
    if (dataSourceConfigMap == null) {
        dataSourceConfigMap = new HashMap<String, Object>();
    }
    return dataSourceConfigMap;
}

public static void clearDBType() {
    contextHolder.remove();
}

}

2、创建一个AbstractRoutingDataSource的子类,实现其determineCurrentLookupKey方法,用于决定使用哪一个数据源。说明一下,这里实现了ApplicationContextAware接口,用于在Spring加载完成后,注入Spring上下文对象,用于获取Bean。

/**
* Copyright (c) 2015 - 2016 eya Inc.
* All rights reserved.
*/
package com.eya.pubservice.datasource;

import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
* 动态数据源父类
* @create ll
* @createDate 2017年3月27日 下午2:38:05
* @update
* @updateDate
*/
public abstract class AbstractDynamicDataSource extends AbstractRoutingDataSource
implements
ApplicationContextAware {

/** 日志 */
protected Logger logger = LoggerFactory.getLogger(getClass());
/** 默认的数据源KEY */
protected static final String DEFAULT_DATASOURCE_KEY = "defaultDataSource";

/** 数据源KEY-VALUE键值对 */
public Map<Object, Object> targetDataSources;

/** spring容器上下文 */
private static ApplicationContext ctx;

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    ctx = applicationContext;
}

public static ApplicationContext getApplicationContext() {
    return ctx;
}

public static Object getBean(String name) {
    return ctx.getBean(name);
}

/**
 * @param targetDataSources the targetDataSources to set
 */
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
    this.targetDataSources = targetDataSources;
    super.setTargetDataSources(targetDataSources);
    // afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
    super.afterPropertiesSet();
}

/**
 * 创建数据源
 * @param driverClassName 数据库驱动名称
 * @param url 连接地址
 * @param username 用户名
 * @param password 密码
 * @return 数据源{@link T}
 * @Author : ll. create at 2017年3月27日 下午2:44:34
 */
public abstract T createDataSource(String driverClassName, String url, String username,
                                   String password);

/**
 * 设置系统当前使用的数据源
 * <p>数据源为空或者为0时,自动切换至默认数据源,即在配置文件中定义的默认数据源
 * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
 */
@Override
protected Object determineCurrentLookupKey() {
    logger.info("【设置系统当前使用的数据源】");
    Map<String, Object> configMap = DBContextHolder.getDBType();
    logger.info("【当前数据源配置为:{}】", configMap);
    if (MapUtils.isEmpty(configMap)) {
        // 使用默认数据源
        return DEFAULT_DATASOURCE_KEY;
    }
    // 判断数据源是否需要初始化
    this.verifyAndInitDataSource();
    logger.info("【切换至数据源:{}】", configMap);
    return configMap.get(DBContextHolder.DATASOURCE_KEY);
}

/**
 * 判断数据源是否需要初始化
 * @Author : ll. create at 2017年3月27日 下午3:57:43
 */
private void verifyAndInitDataSource() {
    Map<String, Object> configMap = DBContextHolder.getDBType();
    Object obj = this.targetDataSources.get(configMap.get(DBContextHolder.DATASOURCE_KEY));
    if (obj != null) {
        return;
    }
    logger.info("【初始化数据源】");
    T datasource = this.createDataSource(configMap.get(DBContextHolder.DATASOURCE_DRIVER)
        .toString(), configMap.get(DBContextHolder.DATASOURCE_URL).toString(),
        configMap.get(DBContextHolder.DATASOURCE_USERNAME).toString(),
        configMap.get(DBContextHolder.DATASOURCE_PASSWORD).toString());
    this.addTargetDataSource(configMap.get(DBContextHolder.DATASOURCE_KEY).toString(),
        datasource);
}

/**
 * 往数据源key-value键值对集合添加新的数据源
 * @param key 新的数据源键
 * @param dataSource 新的数据源
 * @Author : ll. create at 2017年3月27日 下午2:56:49
 */
private void addTargetDataSource(String key, T dataSource) {
    this.targetDataSources.put(key, dataSource);
    super.setTargetDataSources(this.targetDataSources);
    // afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
    super.afterPropertiesSet();
}

}

3、编写AbstractDynamicDataSource的实现类,使用com.alibaba.druid.pool.DruidDataSource数据源。主要实现创建数据源的方法(createDataSource)

/**
* Copyright (c) 2015 - 2016 eya Inc.
* All rights reserved.
*/
package com.eya.pubservice.datasource;

import java.sql.SQLException;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.pool.DruidDataSource;

/**
* Druid数据源
*

摘抄自http://www.68idc.cn/help/buildlang/java/20160606618505.html
* @create ll
* @createDate 2017年3月27日 下午2:40:17
* @update
* @updateDate
*/
public class DruidDynamicDataSource extends AbstractDynamicDataSource {

private boolean testWhileIdle = true;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;

// 是否打开连接泄露自动检测
private boolean removeAbandoned = false;
// 连接长时间没有使用,被认为发生泄露时长
private long removeAbandonedTimeoutMillis = 300 * 1000;
// 发生泄露时是否需要输出 log,建议在开启连接泄露检测时开启,方便排错
private boolean logAbandoned = false;

// 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就会被自动设定为true,使用oracle时可以设定此值。
//    private int maxPoolPreparedStatementPerConnectionSize = -1;

// 配置监控统计拦截的filters
private String filters; // 监控统计:"stat" 防SQL注入:"wall" 组合使用: "stat,wall"
private List<Filter> filterList;

/*
 * 创建数据源
 * @see com.cdelabcare.pubservice.datasource.IDynamicDataSource#createDataSource(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
 */
@Override
public DruidDataSource createDataSource(String driverClassName, String url, String username,
                                        String password) {
    DruidDataSource parent = (DruidDataSource) super.getApplicationContext().getBean(
        DEFAULT_DATASOURCE_KEY);
    DruidDataSource ds = new DruidDataSource();
    ds.setUrl(url);
    ds.setUsername(username);
    ds.setPassword(password);
    ds.setDriverClassName(driverClassName);
    ds.setInitialSize(parent.getInitialSize());
    ds.setMinIdle(parent.getMinIdle());
    ds.setMaxActive(parent.getMaxActive());
    ds.setMaxWait(parent.getMaxWait());
    ds.setTimeBetweenConnectErrorMillis(parent.getTimeBetweenConnectErrorMillis());
    ds.setTimeBetweenEvictionRunsMillis(parent.getTimeBetweenEvictionRunsMillis());
    ds.setMinEvictableIdleTimeMillis(parent.getMinEvictableIdleTimeMillis());

    ds.setValidationQuery(parent.getValidationQuery());
    ds.setTestWhileIdle(testWhileIdle);
    ds.setTestOnBorrow(testOnBorrow);
    ds.setTestOnReturn(testOnReturn);

    ds.setRemoveAbandoned(removeAbandoned);
    ds.setRemoveAbandonedTimeoutMillis(removeAbandonedTimeoutMillis);
    ds.setLogAbandoned(logAbandoned);

    // 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就会被自动设定为true,参照druid的源码
    ds.setMaxPoolPreparedStatementPerConnectionSize(parent
        .getMaxPoolPreparedStatementPerConnectionSize());

    if (StringUtils.isNotBlank(filters))
        try {
            ds.setFilters(filters);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

    addFilterList(ds);
    return ds;
}

private void addFilterList(DruidDataSource ds) {
    if (filterList != null) {
        List<Filter> targetList = ds.getProxyFilters();
        for (Filter add : filterList) {
            boolean found = false;
            for (Filter target : targetList) {
                if (add.getClass().equals(target.getClass())) {
                    found = true;
                    break;
                }
            }
            if (!found)
                targetList.add(add);
        }
    }
}

}

4、使用Spring配置默认数据源。系统运行肯定有一套默认的数据源(否则动态创建的数据源信息从哪里来呢?上面提到的,动态创建的数据源信息是存放在数据库中的)。这里我贴出完整的Spring配置。

猜你喜欢

转载自blog.csdn.net/yueaini10000/article/details/79930848
今日推荐