查看: 3211|回复: 0

[Java代码] Spring动态注册多数据源的实现方法

发表于 2018-3-4 17:45:14

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

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

使用到的技术

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

思路

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

代码实现

  1. TenantConfigEntity(租户信息)
  2. @EqualsAndHashCode(callSuper = false)
  3. @Data
  4. @FieldDefaults(level = AccessLevel.PRIVATE)
  5. public class TenantConfigEntity {
  6. /**
  7. * 租户id
  8. **/
  9. Integer tenantId;
  10. /**
  11. * 租户名称
  12. **/
  13. String tenantName;
  14. /**
  15. * 租户名称key
  16. **/
  17. String tenantKey;
  18. /**
  19. * 数据库url
  20. **/
  21. String dbUrl;
  22. /**
  23. * 数据库用户名
  24. **/
  25. String dbUser;
  26. /**
  27. * 数据库密码
  28. **/
  29. String dbPassword;
  30. /**
  31. * 数据库public_key
  32. **/
  33. String dbPublicKey;
  34. }
  35. DataSourceUtil(辅助工具类,非必要)
  36. public class DataSourceUtil {
  37. private static final String DATA_SOURCE_BEAN_KEY_SUFFIX = "_data_source";
  38. private static final String JDBC_URL_ARGS = "?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&zeroDateTimeBehavior=convertToNull";
  39. private static final String CONNECTION_PROPERTIES = "config.decrypt=true;config.decrypt.key=";
  40. /**
  41. * 拼接数据源的spring bean key
  42. */
  43. public static String getDataSourceBeanKey(String tenantKey) {
  44. if (!StringUtils.hasText(tenantKey)) {
  45. return null;
  46. }
  47. return tenantKey + DATA_SOURCE_BEAN_KEY_SUFFIX;
  48. }
  49. /**
  50. * 拼接完整的JDBC URL
  51. */
  52. public static String getJDBCUrl(String baseUrl) {
  53. if (!StringUtils.hasText(baseUrl)) {
  54. return null;
  55. }
  56. return baseUrl + JDBC_URL_ARGS;
  57. }
  58. /**
  59. * 拼接完整的Druid连接属性
  60. */
  61. public static String getConnectionProperties(String publicKey) {
  62. if (!StringUtils.hasText(publicKey)) {
  63. return null;
  64. }
  65. return CONNECTION_PROPERTIES + publicKey;
  66. }
  67. }
复制代码

DataSourceContextHolder

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

  1. public class DataSourceContextHolder {
  2. private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<>();
  3. public static void setDataSourceKey(String tenantKey) {
  4. dataSourceKey.set(tenantKey);
  5. }
  6. public static String getDataSourceKey() {
  7. return dataSourceKey.get();
  8. }
  9. public static void clearDataSourceKey() {
  10. dataSourceKey.remove();
  11. }
  12. }
复制代码

DynamicDataSource(重点)

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

  1. public class DynamicDataSource extends AbstractRoutingDataSource {
  2. @Autowired
  3. private ApplicationContext applicationContext;
  4. @Lazy
  5. @Autowired
  6. private DynamicDataSourceSummoner summoner;
  7. @Lazy
  8. @Autowired
  9. private TenantConfigDAO tenantConfigDAO;
  10. @Override
  11. protected String determineCurrentLookupKey() {
  12. String tenantKey = DataSourceContextHolder.getDataSourceKey();
  13. return DataSourceUtil.getDataSourceBeanKey(tenantKey);
  14. }
  15. @Override
  16. protected DataSource determineTargetDataSource() {
  17. String tenantKey = DataSourceContextHolder.getDataSourceKey();
  18. String beanKey = DataSourceUtil.getDataSourceBeanKey(tenantKey);
  19. if (!StringUtils.hasText(tenantKey) || applicationContext.containsBean(beanKey)) {
  20. return super.determineTargetDataSource();
  21. }
  22. if (tenantConfigDAO.exist(tenantKey)) {
  23. summoner.registerDynamicDataSources();
  24. }
  25. return super.determineTargetDataSource();
  26. }
  27. }
复制代码

DynamicDataSourceSummoner(重点中的重点)

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

  1. @Slf4j
  2. @Component
  3. public class DynamicDataSourceSummoner implements ApplicationListener<ContextRefreshedEvent> {
  4. // 跟spring-data-source.xml的默认数据源id保持一致
  5. private static final String DEFAULT_DATA_SOURCE_BEAN_KEY = "defaultDataSource";
  6. @Autowired
  7. private ConfigurableApplicationContext applicationContext;
  8. @Autowired
  9. private DynamicDataSource dynamicDataSource;
  10. @Autowired
  11. private TenantConfigDAO tenantConfigDAO;
  12. private static boolean loaded = false;
  13. /**
  14. * Spring加载完成后执行
  15. */
  16. @Override
  17. public void onApplicationEvent(ContextRefreshedEvent event) {
  18. // 防止重复执行
  19. if (!loaded) {
  20. loaded = true;
  21. try {
  22. registerDynamicDataSources();
  23. } catch (Exception e) {
  24. log.error("数据源初始化失败, Exception:", e);
  25. }
  26. }
  27. }
  28. /**
  29. * 从数据库读取租户的DB配置,并动态注入Spring容器
  30. */
  31. public void registerDynamicDataSources() {
  32. // 获取所有租户的DB配置
  33. List<TenantConfigEntity> tenantConfigEntities = tenantConfigDAO.listAll();
  34. if (CollectionUtils.isEmpty(tenantConfigEntities)) {
  35. throw new IllegalStateException("应用程序初始化失败,请先配置数据源");
  36. }
  37. // 把数据源bean注册到容器中
  38. addDataSourceBeans(tenantConfigEntities);
  39. }
  40. /**
  41. * 根据DataSource创建bean并注册到容器中
  42. */
  43. private void addDataSourceBeans(List<TenantConfigEntity> tenantConfigEntities) {
  44. Map<Object, Object> targetDataSources = Maps.newLinkedHashMap();
  45. DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
  46. for (TenantConfigEntity entity : tenantConfigEntities) {
  47. String beanKey = DataSourceUtil.getDataSourceBeanKey(entity.getTenantKey());
  48. // 如果该数据源已经在spring里面注册过,则不重新注册
  49. if (applicationContext.containsBean(beanKey)) {
  50. DruidDataSource existsDataSource = applicationContext.getBean(beanKey, DruidDataSource.class);
  51. if (isSameDataSource(existsDataSource, entity)) {
  52. continue;
  53. }
  54. }
  55. // 组装bean
  56. AbstractBeanDefinition beanDefinition = getBeanDefinition(entity, beanKey);
  57. // 注册bean
  58. beanFactory.registerBeanDefinition(beanKey, beanDefinition);
  59. // 放入map中,注意一定是刚才创建bean对象
  60. targetDataSources.put(beanKey, applicationContext.getBean(beanKey));
  61. }
  62. // 将创建的map对象set到 targetDataSources;
  63. dynamicDataSource.setTargetDataSources(targetDataSources);
  64. // 必须执行此操作,才会重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有这样,动态切换才会起效
  65. dynamicDataSource.afterPropertiesSet();
  66. }
  67. /**
  68. * 组装数据源spring bean
  69. */
  70. private AbstractBeanDefinition getBeanDefinition(TenantConfigEntity entity, String beanKey) {
  71. BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
  72. builder.getBeanDefinition().setAttribute("id", beanKey);
  73. // 其他配置继承defaultDataSource
  74. builder.setParentName(DEFAULT_DATA_SOURCE_BEAN_KEY);
  75. builder.setInitMethodName("init");
  76. builder.setDestroyMethodName("close");
  77. builder.addPropertyValue("name", beanKey);
  78. builder.addPropertyValue("url", DataSourceUtil.getJDBCUrl(entity.getDbUrl()));
  79. builder.addPropertyValue("username", entity.getDbUser());
  80. builder.addPropertyValue("password", entity.getDbPassword());
  81. builder.addPropertyValue("connectionProperties", DataSourceUtil.getConnectionProperties(entity.getDbPublicKey()));
  82. return builder.getBeanDefinition();
  83. }
  84. /**
  85. * 判断Spring容器里面的DataSource与数据库的DataSource信息是否一致
  86. * 备注:这里没有判断public_key,因为另外三个信息基本可以确定唯一了
  87. */
  88. private boolean isSameDataSource(DruidDataSource existsDataSource, TenantConfigEntity entity) {
  89. boolean sameUrl = Objects.equals(existsDataSource.getUrl(), DataSourceUtil.getJDBCUrl(entity.getDbUrl()));
  90. if (!sameUrl) {
  91. return false;
  92. }
  93. boolean sameUser = Objects.equals(existsDataSource.getUsername(), entity.getDbUser());
  94. if (!sameUser) {
  95. return false;
  96. }
  97. try {
  98. String decryptPassword = ConfigTools.decrypt(entity.getDbPublicKey(), entity.getDbPassword());
  99. return Objects.equals(existsDataSource.getPassword(), decryptPassword);
  100. } catch (Exception e) {
  101. log.error("数据源密码校验失败,Exception:{}", e);
  102. return false;
  103. }
  104. }
  105. }
复制代码

spring-data-source.xml

  1. <!-- 引入jdbc配置文件 -->
  2. <context:property-placeholder location="classpath:data.properties" ignore-unresolvable="true"/>
  3. <!-- 公共(默认)数据源 -->
  4. <bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource"
  5. init-method="init" destroy-method="close">
  6. <!-- 基本属性 url、user、password -->
  7. <property name="url" value="${ds.jdbcUrl}" />
  8. <property name="username" value="${ds.user}" />
  9. <property name="password" value="${ds.password}" />
  10. <!-- 配置初始化大小、最小、最大 -->
  11. <property name="initialSize" value="5" />
  12. <property name="minIdle" value="2" />
  13. <property name="maxActive" value="10" />
  14. <!-- 配置获取连接等待超时的时间,单位是毫秒 -->
  15. <property name="maxWait" value="1000" />
  16. <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
  17. <property name="timeBetweenEvictionRunsMillis" value="5000" />
  18. <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
  19. <property name="minEvictableIdleTimeMillis" value="240000" />
  20. <property name="validationQuery" value="SELECT 1" />
  21. <!--单位:秒,检测连接是否有效的超时时间-->
  22. <property name="validationQueryTimeout" value="60" />
  23. <!--建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效-->
  24. <property name="testWhileIdle" value="true" />
  25. <!--申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。-->
  26. <property name="testOnBorrow" value="true" />
  27. <!--归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。-->
  28. <property name="testOnReturn" value="false" />
  29. <!--Config Filter-->
  30. <property name="filters" value="config" />
  31. <property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${ds.publickey}" />
  32. </bean>
  33. <!-- 事务管理器 -->
  34. <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  35. <property name="dataSource" ref="multipleDataSource"/>
  36. </bean>
  37. <!--多数据源-->
  38. <bean id="multipleDataSource" class="a.b.c.DynamicDataSource">
  39. <property name="defaultTargetDataSource" ref="defaultDataSource"/>
  40. <property name="targetDataSources">
  41. <map>
  42. <entry key="defaultDataSource" value-ref="defaultDataSource"/>
  43. </map>
  44. </property>
  45. </bean>
  46. <!-- 注解事务管理器 -->
  47. <!--这里的order值必须大于DynamicDataSourceAspectAdvice的order值-->
  48. <tx:annotation-driven transaction-manager="txManager" order="2"/>
  49. <!-- 创建SqlSessionFactory,同时指定数据源 -->
  50. <bean id="mainSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  51. <property name="dataSource" ref="multipleDataSource"/>
  52. </bean>
  53. <!-- DAO接口所在包名,Spring会自动查找其下的DAO -->
  54. <bean id="mainSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  55. <property name="sqlSessionFactoryBeanName" value="mainSqlSessionFactory"/>
  56. <property name="basePackage" value="a.b.c.*.dao"/>
  57. </bean>
  58. <bean id="defaultSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  59. <property name="dataSource" ref="defaultDataSource"/>
  60. </bean>
  61. <bean id="defaultSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  62. <property name="sqlSessionFactoryBeanName" value="defaultSqlSessionFactory"/>
  63. <property name="basePackage" value="a.b.c.base.dal.dao"/>
  64. </bean>
  65. <!-- 其他配置省略 -->
复制代码

DynamicDataSourceAspectAdvice

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

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

总结

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



回复

使用道具 举报