# 动态多数据源

# 💎 需求介绍

❄ 动态加载多个数据源的配置

❄ 可以实时切换使用的数据源

# 💎 引入maven包

<dependency>
    <groupId>com.fast4cloud</groupId>
    <artifactId>framework-starter-dynamic-data-source</artifactId>
</dependency>
1
2
3
4

# 💎 当前依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
1
2
3
4

# 💎 实际使用

# 引入当前组件,和对于需要使用的数据库驱动

当前连的是mysql,那么需要引入mysql驱动,多种数据库类型,则引入多种驱动

<!---多数据源  start-->
<dependency>
    <groupId>com.fast4cloud</groupId>
    <artifactId>framework-starter-dynamic-data-source</artifactId>
</dependency>
<!--多数据源  end-->
<!--mysql  start-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.9-rc</version>
</dependency>		
<!--mysql  end-->
1
2
3
4
5
6
7
8
9
10
11
12
13

# 多数据源配置信息

# 多数据源配置
custom:
  datasource:
    ds1:
      driver-class-name: oracle.jdbc.OracleDriver
      url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
      username: SYS_DATA
      password: SYS_DATA
    ds2:
      #文档注册相关数据集
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/fast_cloud?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
      username: root
      password: 'root'
    defaultname: default
    names: ds1,ds2
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/fast_cloud?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
    username: root
    password: 'root'
    minIdle: 20
    maxPoolSize: 20
    maxLifetime: 60000
    connectionTimeout: 60000
    idleTimeout: 60000
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 需要进行数据库账户密码加密的,可以这样写

# 参考文献

https://blog.csdn.net/kecong532664/article/details/82257931

jasypt:
  encryptor:
    password: SYS_DATA #加密密钥
# 多数据源配置
custom:
  datasource:
    ds1:
      driver-class-name: oracle.jdbc.OracleDriver
      url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
      username: SYS_DATA
      password: SYS_DATA
    defaultname: default
    names: ds1,ds2,ds3
    driver-class-name: oracle.jdbc.OracleDriver
    url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
    username: ENC(32ASD2SAX)
    password: ENC(32ASD2SAX)
    minIdle: 20
    maxPoolSize: 20
    maxLifetime: 60000
    connectionTimeout: 60000
    idleTimeout: 60000
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 手动切换数据源

 DynamicDataSourceContextHolder.setDataSourceType("ds1");
1

# 动态数据源注册类编写

package com.fast4cloud.datasource.config;

/**
 * Create By lzb
 * 2019/8/22 16:08
 */

import com.alibaba.fastjson.JSONObject;
import com.fast4cloud.datasource.enums.HikariConfigEnum;
import com.fast4cloud.datasource.enums.PoolEnum;
import com.ulisesbocchio.jasyptspringboot.encryptor.DefaultLazyEncryptor;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.jasypt.encryption.StringEncryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.datasource.lookup.DataSourceLookupFailureException;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
 * 动态数据源注册
 * 启动动态数据源请在启动类中 添加 @Import(DynamicDataSourceRegister.class)
 */

/**
 * @Description 注册动态数据源
 * 初始化数据源和提供了执行动态切换数据源的工具类
 * EnvironmentAware(获取配置文件配置的属性值)
 */
//通过 Spring Cloud 原生注解 @RefreshScope 实现配置自动更新:

public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
  private Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class);

  //指定默认数据源(springboot2.0默认数据源是hikari如何想使用其他数据源可以自己配置)
  private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";

  private static final String DATASOURCE_PREFIX = "custom.datasource";
  private static final String POINT = ".";
  //默认数据源
  private DataSource defaultDataSource;
  //用户自定义数据源
  private Map<String, DataSource> customDataSources = new HashMap<String, DataSource>();
  @Override
  public void setEnvironment(Environment environment) {
    String defaultname = environment.getProperty(DATASOURCE_PREFIX + POINT + "defaultname");
    if (StringUtils.isEmpty(defaultname)) {
      initDataSources(environment);
    } else {
      initDefaultDataSource(environment);
      initcustomDataSources(environment);
    }
  }

  /**
   * 初始化数据源
   * @param env
   */

  private void initDataSources(Environment env) {
    // 读取配置文件获取更多数据源
    String dsPrefixs = env.getProperty(DATASOURCE_PREFIX + POINT + "names");
    if (StringUtils.isEmpty(dsPrefixs)) {
      logger.error("names未配置,不加载数据源");
      return;
    }
    boolean flag = true;
    for (String dsPrefix : dsPrefixs.split(",")) {
      // 多个数据源
      DataSource ds = initPoolConfig(env, DATASOURCE_PREFIX + POINT + dsPrefix,dsPrefix);
      if (flag) {
        defaultDataSource = ds;
        flag = false;
      }
      if (ds != null) {
        customDataSources.put(dsPrefix, ds);
      }
    }
  }

  /**
   * 初始化主数据源
   *
   * @param env
   */
  @Deprecated
  private void initDefaultDataSource(Environment env) {
    String defaultname = env.getProperty(DATASOURCE_PREFIX + POINT + "defaultname");
    // 读取主数据源
    defaultDataSource = initPoolConfig(env, DATASOURCE_PREFIX,defaultname);
    if (defaultDataSource == null) {
      logger.info("默认数据源连接异常,请检查数据库连接配置");
      throw new DataSourceLookupFailureException("默认数据源连接异常,请检查数据库连接配置");
    }
    customDataSources.put(defaultname, defaultDataSource);
  }

  /**
   * 初始化其他数据源
   *
   * @param env
   */
  @Deprecated
  private void initcustomDataSources(Environment env) {
    // 读取配置文件获取更多数据源
    String dsPrefixs = env.getProperty(DATASOURCE_PREFIX + POINT + "names");
    if (StringUtils.isEmpty(dsPrefixs)) {
      logger.info("names未配置,不加载其他数据源");
      return;
    }
    for (String dsPrefix : dsPrefixs.split(",")) {
      DataSource ds = initPoolConfig(env, DATASOURCE_PREFIX + POINT + dsPrefix, dsPrefix);
      if (ds != null) {
        customDataSources.put(dsPrefix, ds);
      }
    }
  }

  /**
   * 初始设置连接池配置
   *
   * @param env
   * @param prefix
   */
  private DataSource initPoolConfig(Environment env, String prefix,String ds) {

    String poolType = env.getProperty(prefix + ".poolType", "HikariCP");
    if (poolType.equals(PoolEnum.HikariCP.getCode())) {
      return initHikariConfig(env, prefix, ds);
    }
    return null;
  }

  /**
   * 初始化HikariCP连接池配置
   */
  private DataSource initHikariConfig(Environment env, String prefix, String ds) {
    try {
      JSONObject configJson = new JSONObject();
      for (HikariConfigEnum hikariConfigEnum: HikariConfigEnum.values()) {
        String val = env.getProperty(prefix + POINT + hikariConfigEnum.getCode());
        if (StringUtils.isEmpty(val)) {
          if (!StringUtils.isEmpty(hikariConfigEnum.getDefaultValue())) {
            configJson.put(hikariConfigEnum.getTransform(),hikariConfigEnum.getDefaultValue());
          }
        } else {
          StringEncryptor stringEncryptor = new DefaultLazyEncryptor(new StandardEnvironment());
          if (val.indexOf("ENC(") == 0) {
            val = val.substring(4);
            val = val.substring(0,val.length() - 1);
            try {
              String pwd = env.getProperty("jasypt.encryptor.password");
              System.setProperty("jasypt.encryptor.password", pwd);
              System.setProperty("java.version", "1.8");
              val = stringEncryptor.decrypt(val);
            } catch (Exception e) {
              logger.error("数据库密码解密失败,异常信息为:{}",e);
            }
          }
          configJson.put(hikariConfigEnum.getTransform(),val);
        }
      }
      logger.info( ds + "数据库配置读取为:{}",configJson.toJSONString());
      HikariConfig hikariConfig = JSONObject.parseObject(configJson.toJSONString(),HikariConfig.class);
      String dbtype = env.getProperty(prefix + POINT + "dbtype");
      DynamicDataSourceContextHolder.dataDbTyps.put(ds,dbtype);
      logger.info("hikariConfig配置:{}",JSONObject.toJSONString(hikariConfig));
      DataSource hikariDataSource = new HikariDataSource(hikariConfig);
      return hikariDataSource;
    } catch (Exception e) {
      logger.error("相关数据源未配置:{}", prefix);
      logger.error("数据库连接池初始化异常,异常信息为:{}",e);
    }
    return null;
  }

  @Override
  public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
    Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
    //添加默认数据源
    targetDataSources.put("dataSource", this.defaultDataSource);
    DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
    //添加其他数据源
    targetDataSources.putAll(customDataSources);
    for (String key : customDataSources.keySet()) {
      DynamicDataSourceContextHolder.dataSourceIds.add(key);
    }

    //创建DynamicDataSource
    GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
    beanDefinition.setBeanClass(DynamicDataSource.class);
    beanDefinition.setSynthetic(true);
    MutablePropertyValues mpv = beanDefinition.getPropertyValues();
    mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
    mpv.addPropertyValue("targetDataSources", targetDataSources);
    //注册 - BeanDefinitionRegistry
    beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
    logger.info("Dynamic DataSource Registry");
  }

}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214

EnvironmentAware

凡注册到Spring容器内的bean,实现了EnvironmentAware接口重写setEnvironment方法后,在工程启动时可以获得application.properties的配置文件配置的属性值。

  • org.springframework.context.EnvironmentAware
    • setEnvironment(Environment environment)
  • 任何实现这个接口的bean将可以获得其运行的环境
  • 此处用来获取springboot的properties配置
@Override
public void setEnvironment(Environment environment){
        //初始化主数据源
        initDefaultDataSource(environment);
        //初始化其他数据源
        initcustomDataSources(environment);
        }
1
2
3
4
5
6
7
  • 初始化连接池配置
  • 暂时只添加了HikariCP连接池,可以动态扩展,在配置文件中的poolType来动态配置使用的连接池配置(未配置时,默认使用HikariCP连接池)
    /**
     * 初始设置连接池配置
     * @param env
     * @param prefix
     */
    private DataSource initPoolConfig(Environment env,String prefix){
        String poolType = env.getProperty(prefix+".poolType","HikariCP");
        if (poolType.equals(PoolEnum.HikariCP.getCode())) {
            return initHikariConfig(env,prefix);
        }
        return null;
    }
1
2
3
4
5
6
7
8
9
10
11
12
  • HikariCP连接池的初始化
    /**
     * 初始化HikariCP连接池配置
     */
    private DataSource initHikariConfig(Environment env,String prefix){
        HikariConfig hikariConfig = new HikariConfig();
        try {
            hikariConfig.setDriverClassName(env.getProperty(prefix+".driver-class-name"));
            hikariConfig.setJdbcUrl(env.getProperty(prefix+".url"));
            hikariConfig.setUsername(env.getProperty(prefix+".username"));
            hikariConfig.setPassword(env.getProperty(prefix+".password"));
            hikariConfig.setMinimumIdle(Integer.parseInt(env.getProperty(prefix+".minIdle","1")));
            hikariConfig.setMaximumPoolSize(Integer.parseInt(env.getProperty(prefix+".maxPoolSize","20")));
            hikariConfig.setMaxLifetime(Integer.parseInt(env.getProperty(prefix+".maxLifetime","60000")));
            DataSource hikariDataSource = new HikariDataSource(hikariConfig);
            return hikariDataSource;
        } catch (Exception e) {
            logger.info("相关数据源未配置:{}",prefix);
            logger.info("数据库连接池初始化异常");
        }
        return null;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 连接池配置说明:
    • driver-class-name:数据库驱动配置
    • url:数据库连接url
    • username:数据库连接用户名
    • password:数据库连接密码
    • minIdle:最小空闲连接数量(默认值1)
    • maxPoolSize:最大连接数(默认值20)
    • maxLifetime:池中连接最长生命周期 MINUTES.toMillis(30) = 1800000 1800000 如果不等于0且小于30秒则会被重置回30分钟

ImportBeanDefinitionRegistrar

ImportBeanDefinitionRegistrar接口是也是spring的扩展点之一,它可以支持我们自己写的代码封装成BeanDefinition对象;实现此接口的类会回调postProcessBeanDefinitionRegistry方法,注册到spring容器中。把bean注入到spring容器不止有 @Service @Component等注解方式;还可以实现此接口。

  • ImportBeanDefinitionRegistrar类只能通过其他类@Import的方式来加载,通常是启动类或配置类。
  • 使用@Import,如果括号中的类是ImportBeanDefinitionRegistrar的实现类,则会调用接口方法,将其中要注册的类注册成bean。
  • 实现该接口的类拥有注册bean的能力。
    /**
 * 动态注册多数据源
 */
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata,BeanDefinitionRegistry beanDefinitionRegistry){
        Map<Object, Object> targetDataSources=new HashMap<Object, Object>();
        //添加默认数据源
        targetDataSources.put("dataSource",this.defaultDataSource);
        DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
        //添加其他数据源
        targetDataSources.putAll(customDataSources);
        for(String key:customDataSources.keySet()){
        DynamicDataSourceContextHolder.dataSourceIds.add(key);
        }
        //创建DynamicDataSource
        GenericBeanDefinition beanDefinition=new GenericBeanDefinition();
        beanDefinition.setBeanClass(DynamicDataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv=beanDefinition.getPropertyValues();
        mpv.addPropertyValue("defaultTargetDataSource",defaultDataSource);
        mpv.addPropertyValue("targetDataSources",targetDataSources);
        //注册 - BeanDefinitionRegistry
        beanDefinitionRegistry.registerBeanDefinition("dataSource",beanDefinition);

        logger.info("Dynamic DataSource Registry");
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • DynamicDataSourceContextHolder 一个线程安全的DatabaseType容器
package com.fast4cloud.datasource.config;


import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * Create By lzb
 * 保存一个线程安全的DatabaseType容器
 */
@Slf4j
public class DynamicDataSourceContextHolder {
  private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>();
  public static List<String> dataSourceIds = new ArrayList<String>();

  public static void setDataSourceType(String dataSourceType) {
    log.info("数据源切换为:[{}]", dataSourceType);
    CONTEXT_HOLDER.set(dataSourceType);
  }

  public static String getDataSourceType() {
    return CONTEXT_HOLDER.get();
  }

  public static void clearDataSourceType() {
    CONTEXT_HOLDER.remove();
  }

  /**
   * 判断指定DataSrouce当前是否存在
   */
  public static boolean containsDataSource(String dataSourceId) {
    return dataSourceIds.contains(dataSourceId);
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  • 多数据源的dataSourceIds 列表添加(记录已添加数据源)

  • 现场安全的数据源切换 contextHolder

    用来切换数据源,并保证线程安全,防止多线程数据串改。

GenericBeanDefinition

  • 自定义多数据源的bean
 //创建DynamicDataSource
GenericBeanDefinition beanDefinition=new GenericBeanDefinition();
        beanDefinition.setBeanClass(DynamicDataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv=beanDefinition.getPropertyValues();
        mpv.addPropertyValue("defaultTargetDataSource",defaultDataSource);
        mpv.addPropertyValue("targetDataSources",targetDataSources);
1
2
3
4
5
6
7
  • 动态多数据源的定义
package com.fast4cloud.datasource.config;


import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * Create By lzb
 * 2019/8/22 14:15
 * 动态数据源(需要继承AbstractRoutingDataSource)
 * 作用:使用DatabaseContextHolder获取当前线程的DatabaseType
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
  protected Object determineCurrentLookupKey() {
    return DynamicDataSourceContextHolder.getDataSourceType();
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

AbstractRoutingDataSource

    @Nullable
    private Map<Object, Object> targetDataSources;
    @Nullable
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;
1
2
3
4
5
6
7
8
9
10
  • determineCurrentLookupKey方法用来获取当前线程数据源
  • 继承类AbstractRoutingDataSource 有内部变量
    • targetDataSources:目标数据源map
    • defaultTargetDataSource:默认数据源
    • lenientFallback:如果找不到当前查找键的特定数据源,请指定是否对默认数据源应用宽限回退。
    • dataSourceLookup:DataSourceLookup为一个函数式接口 只有一个方法DataSource getDataSource(String dataSourceName);此处使用的默认实现为JNDI。
    • resolvedDataSources:
    • resolvedDefaultDataSource:

registerBeanDefinition

注册动态数据源

beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
1

# 引用

# 💎 参考

🔍 HikariConfig配置详解 (opens new window)

🔍 ImportBeanDefinitionRegistrar动态注册bean (opens new window)

🔍 ImportBeanDefinitionRegistrar (opens new window)

🔍 一个故事讲明白 ThreadLocal (opens new window)

🔍 Java GenericBeanDefinition.setBeanClass方法代码示例 (opens new window)

🔍 Spring的BeanDefinition使用和理解 (opens new window)

🔍 利用AbstractRoutingDataSource实现动态数据源切换determineCurrentLookupKey方法 (opens new window)

Last Updated: 10/1/2023, 10:26:44 PM