文档章节

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

十月阳光
 十月阳光
发布于 2017/03/27 18:07
字数 2438
阅读 6213
收藏 15

    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<T extends DataSource> extends AbstractRoutingDataSource
                                                                                                implements
                                                                                                ApplicationContextAware {

    /** 日志 */
    protected Logger logger = LoggerFactory.getLogger(getClass());
    /** 默认的数据源KEY,和spring配置文件中的id=druidDynamicDataSource的bean中配置的默认数据源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数据源
 * <p>摘抄自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<DruidDataSource> {

    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配置。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="
   http://www.springframework.org/schema/beans 
   http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
   http://www.springframework.org/schema/tx 
   http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
   ">
	<bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource"
		init-method="init" destroy-method="close">
		<!-- 基本属性driverClassName、 url、user、password -->
		<property name="driverClassName" value="${pro.driver}" />
		<property name="url" value="${pro.url}" />
		<property name="username" value="${pro.username}" />
		<property name="password" value="${pro.password}" />

		<!-- 配置初始化大小、最小、最大 -->
		<property name="initialSize" value="${pro.initialSize}" />
		<property name="minIdle" value="${pro.minIdle}" />
		<property name="maxActive" value="${pro.maxActive}" />

		<!-- 配置获取连接等待超时的时间 -->
		<property name="maxWait" value="${pro.maxWait}" />

		<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
		<property name="timeBetweenEvictionRunsMillis" value="${pro.timeBetweenEvictionRunsMillis}" />

		<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
		<property name="minEvictableIdleTimeMillis" value="${pro.minEvictableIdleTimeMillis}" />

		<property name="validationQuery" value="SELECT 'x'" />
		<property name="testWhileIdle" value="true" />
		<property name="testOnBorrow" value="false" />
		<property name="testOnReturn" value="false" />

		<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
		<property name="poolPreparedStatements" value="true" />
		<property name="maxPoolPreparedStatementPerConnectionSize"
			value="20" />

		<!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 -->
		<property name="filters" value="stat" />
	</bean>

	<!-- 管理动态数据源的数据源(这句话的理解可以看下AbstractRoutingDataSource类的内容) -->
	<bean id="druidDynamicDataSource" class="com.eya.pubservice.datasource.DruidDynamicDataSource">
		<property name="defaultTargetDataSource" ref="defaultDataSource" />
        <property name="targetDataSources">
            <map>
                <entry key="defaultDataSource" value-ref="defaultDataSource"/>
                <!-- 这里还可以加多个dataSource -->
            </map>
        </property>
    </bean>    

	<!-- 注解事务 -->
	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="druidDynamicDataSource" />
	</bean>

	<tx:annotation-driven transaction-manager="txManager" />

	<!-- 定义SqlSessionFactory -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="configLocation">
			<value>classpath:config/sqlMapConfig.xml</value>
		</property>
		<property name="dataSource" ref="druidDynamicDataSource" />
		<property name="typeAliasesPackage" value="com.eya.model.domain" />
		<property name="mapperLocations" value="classpath:com/eya/dao/**/*.xml" />
		<!-- define config location -->
		<!-- <property name="configLocation" value="sqlMapConfig.xml"/> -->
	</bean>
	<!-- 扫描mybatis的接口类 -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.eya.dao,com.eya.pubmapper" />
	</bean>
	<!-- spring 线程池的配置 -->
    <bean id ="taskExecutor"  class ="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" >
        <!-- 线程池维护线程的最少数量 -->
        <property name ="corePoolSize" value ="5" />
        <!-- 线程池维护线程所允许的空闲时间 -->
        <property name ="keepAliveSeconds" value ="30000" />
        <!-- 线程池维护线程的最大数量 -->
        <property name ="maxPoolSize" value ="1000" />
        <!-- 线程池所使用的缓冲队列 -->
        <property name ="queueCapacity" value ="200" />
    </bean>

    <!-- 配置线程池 -->
    <bean id ="dataImportTaskExecutor"  parent="taskExecutor" >
        <!-- 线程池维护线程的最少数量 -->
        <property name ="corePoolSize" value ="1" />
        <!-- 线程池维护线程的最大数量 -->
        <property name ="maxPoolSize" value ="1" />
    </bean>
</beans>

5、编写测试类。实际业务中应该使用AOP实现数据源的切换,这里只写了一个测试,AOP相关很简单,就不在这里单独写了。当调用该方法时,可以从日志信息中看到,首先初始化了datasource-2,并且切换到了datasource-2。图片效果不行,勉强看看

/**
 * 分页查询
 * @return {@link Pagination}
 * @Author : ll. create at 2016年04月05日 下午01:43:19
 */
@RequestMapping(value = "/page.do", method = RequestMethod.POST)
public Pagination<CoreRoleView> page(HttpServletRequest request) {
	logger.info("【分页查询】");

	Map<String, Object> map = new HashMap<String, Object>();
	map.put(DBContextHolder.DATASOURCE_KEY, "localhost");
	map.put(DBContextHolder.DATASOURCE_DRIVER, "com.mysql.jdbc.Driver");
	map.put(DBContextHolder.DATASOURCE_URL,
		"jdbc:mysql://127.0.0.1:3306/test_20170217?useUnicode=true&characterEncoding=UTF-8");
	map.put(DBContextHolder.DATASOURCE_USERNAME, "root");
	map.put(DBContextHolder.DATASOURCE_PASSWORD, "");
	DBContextHolder.setDBType(map);

	return super.page(request, false);
}

© 著作权归作者所有

十月阳光

十月阳光

粉丝 36
博文 94
码字总数 54658
作品 0
成都
程序员
私信 提问
Spring动态创建,加载,使用多数据源

项目中我们经常会遇到多数据源的问题,尤其是数据同步或定时任务等项目更是如此。多数据源让人最头痛的,不是配置多个数据源,而是如何能灵活动态的切换数据源。例如在一个spring和hibernate...

Java编程思想
2014/02/26
9.8K
3
轻量级的关系型数据库中间件 - Sharding-JDBC

Sharding-JDBC是一个开源的适用于微服务的分布式数据访问基础类库,它始终以云原生的基础开发套件为目标。 Sharding-JDBC定位为轻量级java框架,使用客户端直连数据库,以jar包形式提供服务,...

亮_dangdang
2016/01/27
50.1K
41
JAVA中使用代码创建多数据源,并实现动态切换,这个功能我要怎么调用实现

@十月阳光 你好,想跟你请教个问题:请教一下,你这篇JAVA中使用代码创建多数据源,并实现动态切换,这个功能我要怎么调用,入口在哪里,用DBContextHolder.setDBType(map);这个方法就能执行...

牛奶棒冰
2017/05/12
344
1
Java通用数据访问层 Fastser-DAL 1.0.2 发布

Fastser-DAL是Java通用数据访问组件,基于mybatis、spring jdbc、hibernate等ORM框架开发,同时支持基于多数据源的读写分离、主备切换、故障转移,自动恢复、负载均衡、缓存等。 Fastser-DA...

冶卫军
2014/12/24
6.7K
31
MyBatis源码窥探:MyBatis整体架构解析

Mybatis的使用这里就不介绍了,不知道怎么使用的朋友可以点击 http://www.mybatis.org/mybatis-3/zh/index.html 这里面的教程很详细,包括xml的配置、映射、动态sql都有介绍,可以学习和使用...

java邵先生
01/15
0
0

没有更多内容

加载失败,请刷新页面

加载更多

自定义ApiBoot Logging链路以及单元ID生成策略

ApiBoot Logging会为每一个请求都对应创建链路编号(TraceID)以及单元编号(SpanID),用于归类每一次请求日志,通过一个链路下日志单元的Parent SpanID可以进行上下级关系的梳理。 前文回顾...

恒宇少年
30分钟前
15
0
浅谈 Application 和 activity

对于 在 Application初始化一些变量,为什么不可以放在activity 或者其他的组件里呢? 这里就根据个人的理解来讲述一下,欢迎补充指正。 首先 activity 是以栈的形式出现,一个app应用会有多...

MrLins
30分钟前
13
0
Allegro的脚本文件内容里都有哪些

小伙伴们在使用Allegro的时候是否经常用到脚本文件夹呢?scr的用法其实可真不简单。。。 首先脚本文件的运行模式就存在很多种,比如不提示错误信息,不弹出确认对画框(这样很有利于我们执行...

demyar
32分钟前
21
0
微信升级外链管理规范,「砍一刀帮我加速」要被禁止了

原创: 蒋鸿昌 首发:「知晓程序」公众号 - 最好的微信新商业媒体 几天前,知名互联网评论人阑夕模仿皮尤研究中心(Pew Research Center)在美国做的互联网通识调查问卷,做了一份中文版问卷...

知晓云
32分钟前
20
0
CentOS 7接投影仪

我将一台安装着CentOS 7图形界面的惠普笔记本电脑当桌面使用。最近,想要连接投影仪时却遇到了问题。笔记本有一个HDMI接口。我买了一个HDMI---->VGA的转接线,连上笔记本电脑后,屏幕一直在闪...

大别阿郎
36分钟前
11
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部