JAVA语言之Spring动态注册多数据源的实现方法[Java代码]
龚超 2018-07-18 来源 : 阅读 866 评论 0

摘要:本文主要向大家介绍了JAVA语言的Spring动态注册多数据源的实现方法,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

本文主要向大家介绍了JAVA语言的Spring动态注册多数据源的实现方法,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

最近在做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 ThreadLocaldataSourceKey = 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{

// 跟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配置

ListtenantConfigEntities = tenantConfigDAO.listAll();

if (CollectionUtils.isEmpty(tenantConfigEntities)) {

throw new IllegalStateException("应用程序初始化失败,请先配置数据源");

}

// 把数据源bean注册到容器中

addDataSourceBeans(tenantConfigEntities);

}

/**

* 根据DataSource创建bean并注册到容器中

*/

private void addDataSourceBeans(ListtenantConfigEntities) {

MaptargetDataSources = 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


<bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource"

init-method="init" destroy-method="close">










复制代码

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;

}

}

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注编程语言JAVA频道!

本文由 @职坐标 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论
本文作者 联系TA

擅长针对企业软件开发的产品设计及开发的细节与流程设计课程内容。座右铭:大道至简!

  • 370
    文章
  • 23049
    人气
  • 87%
    受欢迎度

已有23人表明态度,87%喜欢该老师!

进入TA的空间
求职秘籍 直通车
  • 索取资料 索取资料 索取资料
  • 答疑解惑 答疑解惑 答疑解惑
  • 技术交流 技术交流 技术交流
  • 职业测评 职业测评 职业测评
  • 面试技巧 面试技巧 面试技巧
  • 高薪秘笈 高薪秘笈 高薪秘笈
TA的其他文章 更多>>
WEB前端必须会的基本知识题目
经验技巧 93% 的用户喜欢
Java语言中四种遍历List的方法总结(推荐)
经验技巧 91% 的用户喜欢
Java语言之SHA-256加密的两种实现方法详解
经验技巧 75% 的用户喜欢
java语言实现把两个有序数组合并到一个数组的实例
经验技巧 75% 的用户喜欢
通过Java语言代码来创建view的方法
经验技巧 80% 的用户喜欢
其他海同师资 更多>>
吕益平
吕益平 联系TA
熟悉企业软件开发的产品设计及开发
孔庆琦
孔庆琦 联系TA
对MVC模式和三层架构有深入的研究
周鸣君
周鸣君 联系TA
擅长Hadoop/Spark大数据技术
范佺菁
范佺菁 联系TA
擅长Java语言,只有合理的安排和管理时间你才能做得更多,行得更远!
金延鑫
金延鑫 联系TA
擅长与学生或家长及时有效沟通
经验技巧30天热搜词 更多>>

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程