当前位置: 移动技术网 > IT编程>开发语言>Java > Spring动态注册多数据源的实现方法

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

2019年07月19日  | 移动技术网IT编程  | 我要评论

龚诗尧,青衣谣 郁可唯,专利技术

最近在做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动态注册多数据源的实现方法,希望对大家有所帮助

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网