动态数据源

1.背景

动态数据源在实际的业务场景下需求很多,而且想要沟通多数据库确实需要封装这种工具,针对于bi工具可能涉及到从不同的业务库或者数据仓库中获取数据,动态数据源就更加有意义。

2.依赖

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.7.RELEASE</version>
</dependency> <dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<dependency>
<groupId>com.viewhigh.bi.common</groupId>
<artifactId>common</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>

3.多数据源原理解析

① 在应用程序启动的时候初始化默认数据源,并将默认数据源注册到spring上下文中,在这过程中需要实现EnvironmentAware接口中的setEnvironment方法,我们知道setEnvironment方法会在初始化上下文的时候调用,那么利用这个时机就可以根据配置文件初始化默认数据源了,当然可以初始化1个也可以多个。

/**
* Created by zzq on 2017/6/14.
* 负责初始化数据源配置
*/
public class DataSourceRegister<T> implements EnvironmentAware, ImportBeanDefinitionRegistrar {
private javax.sql.DataSource defaultTargetDataSource;
static final String MAINDATASOURCE = "mainDataSource"; public final void setEnvironment(Environment environment) {
DruidEntity druidEntity = FileUtil.readYmlByClassPath("db_info", DruidEntity.class); defaultTargetDataSource = DataSourceUtil.createMainDataSource(druidEntity);
} public final void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
// 0.将主数据源添加到数据源集合中
DataSourceSet.putTargetDataSourcesMap(MAINDATASOURCE, defaultTargetDataSource);
//1.创建DataSourceBean
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
//spring名称约定为defaultTargetDataSource和targetDataSources
mpv.addPropertyValue("defaultTargetDataSource", defaultTargetDataSource);
mpv.addPropertyValue("targetDataSources", DataSourceSet.getTargetDataSourcesMap());
beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
}
}

在上述代码中注册Data SourceBean时可以指定一个默认数据源,这个数据源就是默认使用的存储于defaultTargetDataSource,而其它的数据源则存在targetDataSources。

② 那么如果想要使用其它数据源就需要在targetDataSources中通过指定的key去切换就可以。在此之前需要重写Spring中AbstractRoutingDataSource类型的determineCurrentLookupKey方法,而返回值则是即将启动数据源所对应的key,这样就达到了多个数据源切换的目的。

/**
* Created by zzq on 2017/6/13.
*/
public class DataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String keyDataSource = DataSourceSet.getCurrDataSource();
LogUtil.info("***当前数据源为[{}]", keyDataSource == null ? "默认数据源" : keyDataSource);
return keyDataSource;
}
}

4.设计方案

  • 使用方式:

1) 通在应用程序启动时找到一个初始化时机,并使用import导入数据源注册类即可

@Import({DataSourceRegister.class})
@SpringBootApplication//(exclude={DataSourceAutoConfiguration.class,HibernateJpaAutoConfiguration.class})
@ComponentScan("com.XXX.bi")
//@EnableCaching
public class BiApplication {
public static void main(String[] args) {
LogUtil.setEnabled(true);//开启日志输出 SpringApplication sa = new SpringApplication(BiApplication.class);
sa.setBannerMode(Banner.Mode.LOG);
sa.run(args);
}
}

2) 通过注解方式使用

注解方式比较容易理解,但相对于代码而言处理不够灵活;示例如下:

@ActivateDataSource("001")
public List findAll() {
String sql = "select * from td_bi_datasourcetype where is_remove=0 and organization_id=? "; Map map = new HashMap();
map.put("id", String.class);
map.put("code", String.class);
map.put("remark", String.class);
map.put("name", String.class);
map.put("is_remove", Integer.class); DynamicBean dynamicBean = new DynamicBean(map);
String orgId = Identity.getOrganizationId();
return jdbcTemplateExtend.query(sql, new Object[]{orgId}, dynamicBean.getObject().getClass());
}

提供了在方法开始时标记注解,并指定数据源key,为注解参数,则在方法调用过程中即可使用当前key所对应的数据源。

内幕相信你已经猜到了,我们在数据源初始化的时候维护了一个DataSourceSet集合,该集合中存储了数据源key和对应实际的DataSource对象。而且在contextHolder中存储了当前已经设置的数据源key值,这样在触发查询方法时直接调用了系统determineCurrentLookupKey方法,则在这个方法中使用了contextHolder的key值;

/**
* Created by zzq on 2017/6/13.
*/
public class DataSourceSet {
private static final ThreadLocal<String> contextHolder = new ThreadLocal(); private static List<String> dataSourceKeyList = new CopyOnWriteArrayList<String>(); private static Map targetDataSourcesMap = new ConcurrentHashMap(); public static Object putTargetDataSourcesMap(Object key, Object dataSource) {
dataSourceKeyList.add(key.toString());
return targetDataSourcesMap.put(key, dataSource);
} public static Object removeTargetDataSourcesMap(Object key) {
try {
dataSourceKeyList.remove(key);
return targetDataSourcesMap.remove(key);
} catch (Exception e) {
e.printStackTrace();
throw new CustomException(00000, "移除DataSourceSet数据源信息时出现异常,可能由于dataSourceKeyList或targetDataSourcesMap没有该item项");
}
} public static Map getTargetDataSourcesMap() {
return targetDataSourcesMap;
} public static void setCurrDataSource(String ds) {
contextHolder.set(ds);
} public static String getCurrDataSource() {
return contextHolder.get();
} public static void clearCurrDataSource() {
contextHolder.remove();
} public static boolean containsDataSource(String dataSourceKey) {
return dataSourceKeyList.contains(dataSourceKey);
}
}

这样就可以在aspectJ的aop环绕方式中,方法开始时调用DataSourceSet的设置数据源key来达到切换数据源的目的,在方法调用结束后调用重置key的方法来切换回原来的数据源;

public class DataSourceAspect {
@Before("@annotation(ads)")
public void activateDataSource(JoinPoint point, ActivateDataSource ads) throws Throwable {
String keyDataSource = ads.value();
if (!process(keyDataSource, point))
return;
LogUtil.info("method:{} ", point.getSignature().getName());
DataSourceUtil.activateDataSource(keyDataSource, null);
} @After("@annotation(ads)")
public void resetDataSource(JoinPoint point, ActivateDataSource ads) {
String keyDataSource = ads.value();
if (!process(keyDataSource, point))
return;
LogUtil.info("method:{} ", point.getSignature().getName());
DataSourceUtil.resetDataSource(keyDataSource);
} private boolean process(String keyDataSource, JoinPoint point) {
if (keyDataSource == null) {
LogUtil.info("数据源注解已经标识,但value为null[{}]", point.getSignature().getName());
return false;
}
if (keyDataSource.equals(DataSourceRegister.MAINDATASOURCE)) return false;
return true;
}
}

而在DataSourceUtil中则封装了数据源创建时的一系列动作;那么这个时候你也很有可能会发问,应用程序在启动时会创建一次数据源,如果在程序运行期动态创建数据源怎么办呢,下面就可以揭开这个问题:

/**
* 从bean获取数据源
*
* @param keyDataSource
* @return
*/
private static DataSource loadDataSource(String keyDataSource) {
if (dataSourceGetStrategy == null) {
synchronized (DataSourceUtil.class) {
if (dataSourceGetStrategy == null) {
if (!App.getContext().containsBeanDefinition(DATASOURCEGETSTRATEGY))
throw new CustomException(ResType.OverrideGetDataSourceInfo);
dataSourceGetStrategy = (DataSourceGetStrategy) App.getContext().getBean(DATASOURCEGETSTRATEGY);
}
}
}
return dataSourceGetStrategy.getDataSource(keyDataSource);
}

那么在数据源帮助类中提供了一个抽象类:

/**
* 该抽象类必须由子类实现其抽象方法,用于负责动态数据源信息获取
* <p>
* Created by zzq on 2017/6/19.
*/
public abstract class DataSourceGetStrategy {
public abstract javax.sql.DataSource getDataSource(String keyDataSource); @Bean(name = DataSourceUtil.DATASOURCEGETSTRATEGY)
public DataSourceGetStrategy getDataSourceReadStrategy() {
return this;
}
}

如果想要使用动态数据源框架则必须实现其getDataSource方法,那么在这个方法中你可以获取之前传入的datasourcekey,就可以按照自己的方式创建数据源了,如下示例为创建了一个阿里的druid数据源:

/**
* Created by zzq on 2017/6/19.
*/
@Configuration
public class GetDataSource extends DataSourceGetStrategy {
@Autowired
private JdbcTemplateExtend jdbcTemplateExtend; @Override
public DataSource getDataSource(String keyDataSource) {
String sql = "select t1.url,t1.userName,t1.`password`,t2.driverClassName from " +
"td_bi_datasource t1 inner join td_bi_datasourcetype t2 on " +
"t1.dataSourceType_id=t2.id where " +
"t1.`id`=? and t1.is_remove=0 AND t2.is_remove=0 and t1.organization_id=? and t2.organization_id=?"; String orgId = Identity.getOrganizationId(); List<DataSourceAndType> dataSourceInfoList = jdbcTemplateExtend.query(sql, new Object[]{keyDataSource, orgId, orgId}, DataSourceAndType.class);
DataSourceAndType dataSourceInfoEntity = null;
if (dataSourceInfoList.size() > 0)
dataSourceInfoEntity = dataSourceInfoList.get(0); if (dataSourceInfoEntity == null)
return null;
DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(dataSourceInfoEntity.getUrl()); dbType.put(keyDataSource, dataSourceInfoEntity.getUrl()); datasource.setUsername(dataSourceInfoEntity.getUserName());
datasource.setPassword(dataSourceInfoEntity.getPassword());
datasource.setDriverClassName(dataSourceInfoEntity.getDriverClassName());
datasource.setMaxWait(13000);
return datasource;
}
}

3) 代码调用方式使用

相信代码调用的方式会让更多人感觉比较舒适吧!

和aspect类似的道理,如下代码:

try {
DataSourceUtil.activateDataSource(dataSourceKey, dataSource);
//做自己的事情
} finally {
DataSourceUtil.resetDataSource(dataSourceKey);
}

常规方式可以使用try finally处理,如果你有更好的方式也可以使用哦!思路就是在你的代码前激活数据源,在自己代码调用最后释放数据源。

PS:

① 在最后强调下,不用担心频繁创建数据源之后的性能问题,因为在一次创建之后,多次使用时DataSourceSet会有保存记录,直接切换数据源,不会有任何的性能消耗;

② 如果有临时数据源不希望被缓存则使用DataSourceUtil.activateDataSource(dataSourceKey, dataSource);两个参数的方法重载,第二个参数可以直接自己创建数据源对象传入,使用之后,框架也会将资源释放不做保留;

③ SpringBoot动态数据源中的Bean名称为:dataSource

      

 GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
//spring名称约定为defaultTargetDataSource和targetDataSources
mpv.addPropertyValue("defaultTargetDataSource", defaultTargetDataSource);
mpv.addPropertyValue("targetDataSources", DataSourceSet.getTargetDataSourcesMap());
beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);

项目地址:https://github.com/qq472708969/dynamicDataSource  !

spring boot动态数据源方案的更多相关文章

  1. (43). Spring Boot动态数据源(多数据源自动切换)【从零开始学Spring Boot】

    在上一篇我们介绍了多数据源,但是我们会发现在实际中我们很少直接获取数据源对象进行操作,我们常用的是jdbcTemplate或者是jpa进行操作数据库.那么这一节我们将要介绍怎么进行多数据源动态切换.添 ...

  2. 43. Spring Boot动态数据源(多数据源自动切换)【从零开始学Spring Boot】

    [视频&交流平台] àSpringBoot视频 http://study.163.com/course/introduction.htm?courseId=1004329008&utm ...

  3. SaaS 系统架构,Spring Boot 动态数据源实现!

    这段时候在准备从零开始做一套SaaS系统,之前的经验都是开发单数据库系统并没有接触过SaaS系统,所以接到这个任务的时候也有也些头疼,不过办法部比困难多,难得的机会. 在网上找了很多关于SaaS的资料 ...

  4. Spring Boot 动态数据源(多数据源自己主动切换)

    本文实现案例场景: 某系统除了须要从自己的主要数据库上读取和管理数据外.另一部分业务涉及到其它多个数据库,要求能够在不论什么方法上能够灵活指定详细要操作的数据库. 为了在开发中以最简单的方法使用,本文 ...

  5. Spring Boot 动态数据源(Spring 注解数据源)

    本文实现案例场景:某系统除了需要从自己的主要数据库上读取和管理数据外,还有一部分业务涉及到其他多个数据库,要求可以在任何方法上可以灵活指定具体要操作的数据库. 为了在开发中以最简单的方法使用,本文基于 ...

  6. Spring Boot 动态数据源(多数据源自动切换)

    本文实现案例场景: 某系统除了需要从自己的主要数据库上读取和管理数据外,还有一部分业务涉及到其他多个数据库,要求可以在任何方法上可以灵活指定具体要操作的数据库. 为了在开发中以最简单的方法使用,本文基 ...

  7. 22. Spring Boot 动态数据源(多数据源自动切换)

    转自:https://blog.csdn.net/catoop/article/details/50575038

  8. Spring实现动态数据源,支持动态加入、删除和设置权重及读写分离

    当项目慢慢变大,訪问量也慢慢变大的时候.就难免的要使用多个数据源和设置读写分离了. 在开题之前先说明下,由于项目多是使用Spring,因此下面说到某些操作可能会依赖于Spring. 在我经历过的项目中 ...

  9. Spring Boot多数据源配置(二)MongoDB

    在Spring Boot多数据源配置(一)durid.mysql.jpa 整合中已经讲过了Spring Boot如何配置mysql多数据源.本篇文章讲一下Spring Boot如何配置mongoDB多 ...

随机推荐

  1. Ubuntu和centos离线安装软件包(apt和yum)

    linux安装软件包要解决包依赖问题,不能简单安装包本身. 离线安装基本思想都是先在一台设备上安装下载所有依赖包,然后拷贝所有依赖包到新设备上安装. Ubuntu下apt离线安装包 执行apt upd ...

  2. Linux CentOS 6.9 Minimal 编译 OpenJDK 7

    今天学习<深入理解Java虚拟机:JVM高级特性与最佳实践>一书,并动手在Linux系统上编译OpenJDK 7,初次搞不太顺利,特记录下编译操作细节. 一.前期准备 约定:工具默认安装目 ...

  3. 二叉树转换成森林&amp;森林变成二叉树

    一,树转换成二叉树 1,将全部兄弟结点连起来; 2,保留第一个兄弟结点与父节点的连接,断开其它兄弟结点与父节点的连接,然后以根节点为轴依照顺时针方向旋转45度 watermark/2/text/aHR ...

  4. last

    last reboot 显示系统最后重启的历史记录 last -w 显示自系统启动以来,所有登录的用户(全名)

  5. react获取当前页面的url参数

    react获取当前页面的url参数,必须在url路由对应的组件上获取,在子组件上获取不到,为undefined,获取形如  /news/:id  的后面的参数 id this.props.match. ...

  6. PHP伪造referer突破网盘禁止外链(附115源码)

    新建一个文件file.php.后面的参数就是需要伪造referfer的目标地址吧.如:file.php/http://www.xxx.xxx/xxx.mp3 复制内容到剪贴板 代码: <?$ur ...

  7. Window应急响应(三):勒索病毒

    0x00 前言 ​ 勒索病毒,是一种新型电脑病毒,主要以邮件.程序木马.网页挂马的形式进行传播.该病毒性质恶劣.危害极大,一旦感染将给用户带来无法估量的损失.这种病毒利用各种加密算法对文件进行加密,被 ...

  8. TensorFlow Jupyter Notebook 和matplotlib安装配置

    Jupyter Notebook 和matplotlib Jupyter Notebook安装 Python 3 : python3 -m pip install --upgrade pip pyth ...

  9. centos 安装教程 服务器配置教程 服务器中安装python 服务器中安装Django 安装MySQL 配置MySQL

    一 .解决python编译安装所需的软件依赖 yum install gcc patch libffi-devel python-devel  zlib-devel bzip2-devel opens ...

  10. [No0000124]WPF 扩展控件Behavior的几种方式

    一.使用Attached Dependency Property的方式 (1)定义Attached Dependency Property public static class DigitsOnly ...