Spring动态注册多数据源的实现方法

最近在做SaaS应用,数据库采用了单实例多schema的架构(详见参考资料1),每个租户有一个独立的schema,同时整个数据源有一个共享的schema,因此需要解决动态增删、切换数据源的问题。

在网上搜了很多文章后,很多都是讲主从数据源配置,或都是在应用启动前已经确定好数据源配置的,甚少讲在不停机的情况如何动态加载数据源,所以写下这篇文章,以供参考。

使用到的技术

  • Java8
  • Spring + SpringMVC + MyBatis
  • Druid连接池
  • Lombok
  • (以上技术并不影响思路实现,只是为了方便浏览以下代码片段)

思路

当一个请求进来的时候,判断当前用户所属租户,并根据租户信息切换至相应数据源,然后进行后续的业务操作。

代码实现

TenantConfigEntity(租户信息)
@EqualsAndHashCode(callSuper = false)
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TenantConfigEntity {
 /**
  * 租户id
  **/
 Integer tenantId;
 /**
  * 租户名称
  **/
 String tenantName;
 /**
  * 租户名称key
  **/
 String tenantKey;
 /**
  * 数据库url
  **/
 String dbUrl;
 /**
  * 数据库用户名
  **/
 String dbUser;
 /**
  * 数据库密码
  **/
 String dbPassword;
 /**
  * 数据库public_key
  **/
 String dbPublicKey;
}
DataSourceUtil(辅助工具类,非必要)
public class DataSourceUtil {
 private static final String DATA_SOURCE_BEAN_KEY_SUFFIX = "_data_source";
 private static final String JDBC_URL_ARGS = "?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&zeroDateTimeBehavior=convertToNull";
 private static final String CONNECTION_PROPERTIES = "config.decrypt=true;config.decrypt.key=";
 /**
  * 拼接数据源的spring bean key
  */
 public static String getDataSourceBeanKey(String tenantKey) {
  if (!StringUtils.hasText(tenantKey)) {
   return null;
  }
  return tenantKey + DATA_SOURCE_BEAN_KEY_SUFFIX;
 }
 /**
  * 拼接完整的JDBC URL
  */
 public static String getJDBCUrl(String baseUrl) {
  if (!StringUtils.hasText(baseUrl)) {
   return null;
  }
  return baseUrl + JDBC_URL_ARGS;
 }
 /**
  * 拼接完整的Druid连接属性
  */
 public static String getConnectionProperties(String publicKey) {
  if (!StringUtils.hasText(publicKey)) {
   return null;
  }
  return CONNECTION_PROPERTIES + publicKey;
 }
}

DataSourceContextHolder

使用 ThreadLocal 保存当前线程的数据源key name,并实现set、get、clear方法;

public class DataSourceContextHolder {
 private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<>();
 public static void setDataSourceKey(String tenantKey) {
  dataSourceKey.set(tenantKey);
 }
 public static String getDataSourceKey() {
  return dataSourceKey.get();
 }
 public static void clearDataSourceKey() {
  dataSourceKey.remove();
 }
}

DynamicDataSource(重点)

继承 AbstractRoutingDataSource (建议阅读其源码,了解动态切换数据源的过程),实现动态选择数据源;

public class DynamicDataSource extends AbstractRoutingDataSource {
 @Autowired
 private ApplicationContext applicationContext;
 @Lazy
 @Autowired
 private DynamicDataSourceSummoner summoner;
 @Lazy
 @Autowired
 private TenantConfigDAO tenantConfigDAO;
 @Override
 protected String determineCurrentLookupKey() {
  String tenantKey = DataSourceContextHolder.getDataSourceKey();
  return DataSourceUtil.getDataSourceBeanKey(tenantKey);
 }
 @Override
 protected DataSource determineTargetDataSource() {
  String tenantKey = DataSourceContextHolder.getDataSourceKey();
  String beanKey = DataSourceUtil.getDataSourceBeanKey(tenantKey);
  if (!StringUtils.hasText(tenantKey) || applicationContext.containsBean(beanKey)) {
   return super.determineTargetDataSource();
  }
  if (tenantConfigDAO.exist(tenantKey)) {
   summoner.registerDynamicDataSources();
  }
  return super.determineTargetDataSource();
 }
}

DynamicDataSourceSummoner(重点中的重点)

从数据库加载数据源信息,并动态组装和注册spring bean,

@Slf4j
@Component
public class DynamicDataSourceSummoner implements ApplicationListener<ContextRefreshedEvent> {
 // 跟spring-data-source.xml的默认数据源id保持一致
 private static final String DEFAULT_DATA_SOURCE_BEAN_KEY = "defaultDataSource";
 @Autowired
 private ConfigurableApplicationContext applicationContext;
 @Autowired
 private DynamicDataSource dynamicDataSource;
 @Autowired
 private TenantConfigDAO tenantConfigDAO;
 private static boolean loaded = false;
 /**
  * Spring加载完成后执行
  */
 @Override
 public void onApplicationEvent(ContextRefreshedEvent event) {
  // 防止重复执行
  if (!loaded) {
   loaded = true;
   try {
    registerDynamicDataSources();
   } catch (Exception e) {
    log.error("数据源初始化失败, Exception:", e);
   }
  }
 }
 /**
  * 从数据库读取租户的DB配置,并动态注入Spring容器
  */
 public void registerDynamicDataSources() {
  // 获取所有租户的DB配置
  List<TenantConfigEntity> tenantConfigEntities = tenantConfigDAO.listAll();
  if (CollectionUtils.isEmpty(tenantConfigEntities)) {
   throw new IllegalStateException("应用程序初始化失败,请先配置数据源");
  }
  // 把数据源bean注册到容器中
  addDataSourceBeans(tenantConfigEntities);
 }
 /**
  * 根据DataSource创建bean并注册到容器中
  */
 private void addDataSourceBeans(List<TenantConfigEntity> tenantConfigEntities) {
  Map<Object, Object> targetDataSources = Maps.newLinkedHashMap();
  DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
  for (TenantConfigEntity entity : tenantConfigEntities) {
   String beanKey = DataSourceUtil.getDataSourceBeanKey(entity.getTenantKey());
   // 如果该数据源已经在spring里面注册过,则不重新注册
   if (applicationContext.containsBean(beanKey)) {
    DruidDataSource existsDataSource = applicationContext.getBean(beanKey, DruidDataSource.class);
    if (isSameDataSource(existsDataSource, entity)) {
     continue;
    }
   }
   // 组装bean
   AbstractBeanDefinition beanDefinition = getBeanDefinition(entity, beanKey);
   // 注册bean
   beanFactory.registerBeanDefinition(beanKey, beanDefinition);
   // 放入map中,注意一定是刚才创建bean对象
   targetDataSources.put(beanKey, applicationContext.getBean(beanKey));
  }
  // 将创建的map对象set到 targetDataSources;
  dynamicDataSource.setTargetDataSources(targetDataSources);
  // 必须执行此操作,才会重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有这样,动态切换才会起效
  dynamicDataSource.afterPropertiesSet();
 }
 /**
  * 组装数据源spring bean
  */
 private AbstractBeanDefinition getBeanDefinition(TenantConfigEntity entity, String beanKey) {
  BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
  builder.getBeanDefinition().setAttribute("id", beanKey);
  // 其他配置继承defaultDataSource
  builder.setParentName(DEFAULT_DATA_SOURCE_BEAN_KEY);
  builder.setInitMethodName("init");
  builder.setDestroyMethodName("close");
  builder.addPropertyValue("name", beanKey);
  builder.addPropertyValue("url", DataSourceUtil.getJDBCUrl(entity.getDbUrl()));
  builder.addPropertyValue("username", entity.getDbUser());
  builder.addPropertyValue("password", entity.getDbPassword());
  builder.addPropertyValue("connectionProperties", DataSourceUtil.getConnectionProperties(entity.getDbPublicKey()));
  return builder.getBeanDefinition();
 }
 /**
  * 判断Spring容器里面的DataSource与数据库的DataSource信息是否一致
  * 备注:这里没有判断public_key,因为另外三个信息基本可以确定唯一了
  */
 private boolean isSameDataSource(DruidDataSource existsDataSource, TenantConfigEntity entity) {
  boolean sameUrl = Objects.equals(existsDataSource.getUrl(), DataSourceUtil.getJDBCUrl(entity.getDbUrl()));
  if (!sameUrl) {
   return false;
  }
  boolean sameUser = Objects.equals(existsDataSource.getUsername(), entity.getDbUser());
  if (!sameUser) {
   return false;
  }
  try {
   String decryptPassword = ConfigTools.decrypt(entity.getDbPublicKey(), entity.getDbPassword());
   return Objects.equals(existsDataSource.getPassword(), decryptPassword);
  } catch (Exception e) {
   log.error("数据源密码校验失败,Exception:{}", e);
   return false;
  }
 }
}

spring-data-source.xml

<!-- 引入jdbc配置文件 -->
 <context:property-placeholder location="classpath:data.properties" ignore-unresolvable="true"/>
 <!-- 公共(默认)数据源 -->
 <bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource"
   init-method="init" destroy-method="close">
  <!-- 基本属性 url、user、password -->
  <property name="url" value="${ds.jdbcUrl}" />
  <property name="username" value="${ds.user}" />
  <property name="password" value="${ds.password}" />
  <!-- 配置初始化大小、最小、最大 -->
  <property name="initialSize" value="5" />
  <property name="minIdle" value="2" />
  <property name="maxActive" value="10" />
  <!-- 配置获取连接等待超时的时间,单位是毫秒 -->
  <property name="maxWait" value="1000" />
  <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
  <property name="timeBetweenEvictionRunsMillis" value="5000" />
  <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
  <property name="minEvictableIdleTimeMillis" value="240000" />
  <property name="validationQuery" value="SELECT 1" />
  <!--单位:秒,检测连接是否有效的超时时间-->
  <property name="validationQueryTimeout" value="60" />
  <!--建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效-->
  <property name="testWhileIdle" value="true" />
  <!--申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。-->
  <property name="testOnBorrow" value="true" />
  <!--归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。-->
  <property name="testOnReturn" value="false" />
  <!--Config Filter-->
  <property name="filters" value="config" />
  <property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${ds.publickey}" />
 </bean>
 <!-- 事务管理器 -->
 <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="multipleDataSource"/>
 </bean>
 <!--多数据源-->
 <bean id="multipleDataSource" class="a.b.c.DynamicDataSource">
  <property name="defaultTargetDataSource" ref="defaultDataSource"/>
  <property name="targetDataSources">
   <map>
    <entry key="defaultDataSource" value-ref="defaultDataSource"/>
   </map>
  </property>
 </bean>
 <!-- 注解事务管理器 -->
 <!--这里的order值必须大于DynamicDataSourceAspectAdvice的order值-->
 <tx:annotation-driven transaction-manager="txManager" order="2"/>
 <!-- 创建SqlSessionFactory,同时指定数据源 -->
 <bean id="mainSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="multipleDataSource"/>
 </bean>
 <!-- DAO接口所在包名,Spring会自动查找其下的DAO -->
 <bean id="mainSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="sqlSessionFactoryBeanName" value="mainSqlSessionFactory"/>
  <property name="basePackage" value="a.b.c.*.dao"/>
 </bean>
 <bean id="defaultSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="defaultDataSource"/>
 </bean>
 <bean id="defaultSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="sqlSessionFactoryBeanName" value="defaultSqlSessionFactory"/>
  <property name="basePackage" value="a.b.c.base.dal.dao"/>
 </bean>
 <!-- 其他配置省略 -->

DynamicDataSourceAspectAdvice

利用AOP自动切换数据源,仅供参考;

@Slf4j
@Aspect
@Component
@Order(1) // 请注意:这里order一定要小于tx:annotation-driven的order,即先执行DynamicDataSourceAspectAdvice切面,再执行事务切面,才能获取到最终的数据源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DynamicDataSourceAspectAdvice {
 @Around("execution(* a.b.c.*.controller.*.*(..))")
 public Object doAround(ProceedingJoinPoint jp) throws Throwable {
  ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  HttpServletRequest request = sra.getRequest();
  HttpServletResponse response = sra.getResponse();
  String tenantKey = request.getHeader("tenant");
  // 前端必须传入tenant header, 否则返回400
  if (!StringUtils.hasText(tenantKey)) {
   WebUtils.toHttp(response).sendError(HttpServletResponse.SC_BAD_REQUEST);
   return null;
  }
  log.info("当前租户key:{}", tenantKey);
  DataSourceContextHolder.setDataSourceKey(tenantKey);
  Object result = jp.proceed();
  DataSourceContextHolder.clearDataSourceKey();
  return result;
 }
}

总结

以上所述是小编给大家介绍的Spring动态注册多数据源的实现方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • Spring动态注册多数据源的实现方法

    最近在做SaaS应用,数据库采用了单实例多schema的架构(详见参考资料1),每个租户有一个独立的schema,同时整个数据源有一个共享的schema,因此需要解决动态增删.切换数据源的问题. 在网上搜了很多文章后,很多都是讲主从数据源配置,或都是在应用启动前已经确定好数据源配置的,甚少讲在不停机的情况如何动态加载数据源,所以写下这篇文章,以供参考. 使用到的技术 Java8 Spring + SpringMVC + MyBatis Druid连接池 Lombok (以上技术并不影响思路实现,

  • Spring实现动态切换多数据源的解决方案

    前言 Spring动态配置多数据源,即在大型应用中对数据进行切分,并且采用多个数据库实例进行管理,这样可以有效提高系统的水平伸缩性.而这样的方案就会不同于常见的单一数据实例的方案,这就要程序在运行时根据当时的请求及系统状态来动态的决定将数据存储在哪个数据库实例中,以及从哪个数据库提取数据. Spring2.x以后的版本中采用Proxy模式,就是我们在方案中实现一个虚拟的数据源,并且用它来封装数据源选择逻辑,这样就可以有效地将数据源选择逻辑从Client中分离出来.Client提供选择所需的上下文

  • Spring动态多数据源配置实例Demo

    最近由于咨询spring如何配置多数据源的人很多,一一回答又比较麻烦,而且以前的博文中的配置也是有问题,因此特此重新发布一个Demo给大家. Demo中共有两个数据源,即MySQL和Oracle,并已经进行简单测试,动态切换数据源是没有问题的,希望借此Demo能帮助到大家. Demo下载地址: Spring动态切换多数据源Demo:http://xiazai.jb51.net/201701/yuanma/dynamicDatasourceDemo_jb51.rar 另外我给些说明,阐述下多数据源

  • SpringBean和Controller实现动态注册与注销过程详细讲解

    目录 说明 注册和注销工具类 编写测试用例 测试结果 注册Service 注册controller 注销Controller 部分场景下可能需要下载远程jar包,然后注册jar包中的Bean和Controller 说明 这里的Bean 一般特指 Service层的服务类,Controller本质上也是Bean 注册和注销工具类 这里用了一些 hutool的工具类,hutools是一个不错的基础工具集. package cn.guzt.utils; import cn.hutool.extra.s

  • Spring运行时动态注册bean的方法

    在spring运行时,动态的添加bean,dapeng框架在解析xml的字段时,使用到了动态注册,注册了一个实现了FactoryBean类! 定义一个没有被Spring管理的Controller public class UserController implements InitializingBean{ private UserService userService; public UserService getUserService() { return userService; } pu

  • Spring之动态注册bean的实现方法

    Spring之动态注册bean 什么场景下,需要主动向Spring容器注册bean呢? 如我之前做个的一个支持扫表的基础平台,使用者只需要添加基础配置 + Groovy任务,就可以丢到这个平台上面来运行了,而这个基础平台是一直都在运行的,所以在新来任务时,最直观需要注册的就是 DataSource 数据源这个bean了,那么可以怎么玩? I. 主动注册Bean支持 借助BeanDefinition来实现bean的定义,从最终的使用来看,代码比较少,几行而已 public <T> T regis

  • knockoutjs动态加载外部的file作为component中的template数据源的实现方法

    玩过knockoutjs的都知道,有一个强大的功能叫做component,而这个component有个牛逼的地方就是拥有自己的viewmodel和template,比如下面这样: ko.components.register('message-editor', { viewModel: function(){}, template:"" }); 很显然,viewmodel就是function函数区,而template就是模板区,然后通过register函数将component注册到kn

  • Spring动态数据源实现读写分离详解

    一.创建基于ThreadLocal的动态数据源容器,保证数据源的线程安全性 package com.bounter.mybatis.extension; /** * 基于ThreadLocal实现的动态数据源容器,保证DynamicDataSource的线程安全性 * @author simon * */ public class DynamicDataSourceHolder { private static final ThreadLocal<String> dataSourceHolde

  • Spring Boot 整合mybatis 使用多数据源的实现方法

    前言 本篇教程偏向实战,程序猿直接copy代码加入到自己的项目中做简单的修修改改便可使用,而对于springboot以及mybatis不在此进行展开介绍,如有读者希望了解可以给我留言,并持续关注,我后续会慢慢更新.(黑色区域代码部分,安卓手机可手动向左滑动,来查看全部代码) 整合 其实整合很简单,如果是用gradle的话,在build.gradle文件里加入 compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1')

  • spring多数据源配置实现方法实例分析

    本文实例讲述了spring多数据源配置实现方法.分享给大家供大家参考,具体如下: 在网上找到的配置多数据源的方法. 1.扩展 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource类 实现代码 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource exte

随机推荐