SpringBoot动态数据源切换(一)

最近项目中需要配置两个数据源,并且在不同的包下动态切换,为此,参考网上动态切换数据源的博客,实现了满足项目的数据源动态切换功能。


  • 1、Spring的开发者还是挺有先见之明的,为我们提供了扩展Spring的AbstractRoutingDataSource抽象类,我们来看它的源码
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
   /**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}

/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
protected abstract Object determineCurrentLookupKey();

源码注释解释的很清楚,determineTargetDataSource 方法通过数据源的标识获取当前数据源;determineCurrentLookupKey方法则是获取数据源标识。(作为英语彩笔,有道词典这种翻译软件还是特别好使的)

所以,我们实现动态切换数据源,需要实现determineCurrentLookupKey方法,动态提供数据源标识即可。

  • 2、自定义DynamicDataSource类,继承AbstractRoutingDataSource,并实现determineCurrentLookupKey方法。
1
2
3
4
5
6
7
8
9
10
11
12
public class DynamicDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
/**
* DynamicDataSourceContextHolder代码中使用setDataSource
* 设置当前的数据源,在路由类中使用getDataSource进行获取,
* 交给AbstractRoutingDataSource进行注入使用。
*/
return DynamicDataSourceContextHolder.getDataSource();
}
}
  • 3、创建统一数据源管理类DynamicDataSourceContextHolder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DynamicDataSourceContextHolder {

// 线程本地环境
private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();
// 管理所有的数据源Id
public static List<String> dataSourceIds = new ArrayList<String>();

public static void setDataSource(String dataSource) {
dataSources.set(dataSource);
}
public static String getDataSource() {
return dataSources.get();
}
public static void clearDataSource() {
dataSources.remove();
}

// 判断指定的DataSource当前是否存在
public static boolean containsDataSource(String dataSourceId) {
return dataSourceIds.contains(dataSourceId);
}
}
  • 4、重点来了,创建动态数据源注册器DynamicDataSourceRegister
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
    public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

// 默认数据连接池
public static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";

private Class<? extends DataSource> dataSourceType;

// 默认数据源
private DataSource defaultDataSource;

private Map<String, DataSource> dataSourceMaps = new HashMap<String, DataSource>();

/**
* 加载多数据源配置
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
initDefaultDataSource(environment);
}

/**
* 初始化默认数据源
* @param environment
*/
private void initDefaultDataSource(Environment environment) {
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(environment, "spring.datasource.");
try {
if(propertyResolver.getProperty("type") == null) {
dataSourceType = (Class<? extends DataSource>)Class.forName(DATASOURCE_TYPE_DEFAULT.toString());
} else {
dataSourceType = (Class<? extends DataSource>)Class.forName(propertyResolver.getProperty("type"));
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

// 创建数据源
String jndiName = propertyResolver.getProperty("jndi-name");
String[] jndiNames = jndiName.split(",");
defaultDataSource = new JndiDataSourceLookup().getDataSource(jndiNames[0]);

dataSourceMaps.put("AAA", defaultDataSource);
DataSource dataSource1 = new JndiDataSourceLookup().getDataSource(jndiNames[1]);
dataSourceMaps.put("BBB", dataSource1);
}

@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
Map<String, Object> targetDataSources = new HashMap<String, Object>();
// 将主数据源添加到更多数据源中
targetDataSources.put("dataSource", defaultDataSource);
DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");

// 添加更多数据源
targetDataSources.putAll(dataSourceMaps);
for(String key : dataSourceMaps.keySet()) {
DynamicDataSourceContextHolder.dataSourceIds.add(key);
}

// 创建DynamicDataSource
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicDataSource.class);
beanDefinition.setSynthetic(true);

MutablePropertyValues mutablePropertyValues = beanDefinition.getPropertyValues();
mutablePropertyValues.addPropertyValue("defaultTargetDataSource", defaultDataSource);
mutablePropertyValues.addPropertyValue("targetDataSources", targetDataSources);
beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
}
}

好了,这么一坨代码丢在这儿,相信读者也看着费劲,接下来对动态数据源注册器略作解释

EnvironmentAware接口提供了一个setEnvironment(Environment environment)方法,通过这个方法我们可以从application.properties配置文件中获取到所有数据源的配置信息,然后创建数据源并加载到内存中

ImportBeanDefinitionRegistrar接口,光看接口名字大概都能猜到是做什么的,对,就是注册Bean的。该接口用于在系统处理@Configuration class时注册更多的bean。是bean定义级别的操作,而非@Bean method/instance级别的。该接口提供了registerBeanDefinitions方法,该方法是在Spring加载bean时被Spring调用。通过setEnvironment方法,已经将配置文件中所有的数据源获取到了,然后在registerBeanDefinitions方法中将所有数据源注册到Spring容器中。

5、将动态数据源注册器导入到Spring容器中

1
2
3
4
5
6
7
@SpringBootApplication
@Import({DynamicDataSourceRegister.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

需要注意的是,使用@Import导入的类必须满足符合以下的某一个条件:

  1. 导入的类使用@Configuration进行标注
  2. 导入的类中至少有一个使用@Bean标准的方法
  3. 导入的类实现了ImportSelector接口
  4. 导入的类实现了ImportBeanDefinitionRegistrar接口

到这一步了,是不是就完了呢,当然不是,以上这些步骤只是为切换数据源提供了基础

  • 6、新建一个TargetDataSource注解
1
2
3
4
5
6
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String value();
}

此注解用来标记当前的方法的数据源的,在需要指定数据源的方法上标记@TargetDataSource(“AAA”)注解即可,还没完,继续往下看。

  • 7、新建数据源切换AOP切面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Aspect
@Order(-1) //保证此AOP在@Transactional之前执行
@Component
public class DynamicDataSourceAspect {

private transient static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

// 通过注解切换数据源(细粒度)
@Around("@annotation(targetDataSource)")
public Object changeDataSource(ProceedingJoinPoint joinPoint, TargetDataSource targetDataSource) throws Throwable {
Object object = null;
String dataSourceId = targetDataSource.value();
if(DynamicDataSourceContextHolder.containsDataSource(dataSourceId)) {
logger.info("系统将使用{}数据源", dataSourceId);
DynamicDataSourceContextHolder.setDataSource(dataSourceId);
} else {
logger.debug("数据源{}不存在,将使用默认数据源{}", dataSourceId, joinPoint.getSignature());
}
object=joinPoint.proceed();
DynamicDataSourceContextHolder.clearDataSource();
return object;
}

}

解释解释,这个切面呢,就是切标记了targetDataSource注解的方法,根据targetDataSource注解的value值设置系统当前的数据源。使用注解方式算是一种细粒度的控制,可切换多个数据源;粗粒度的就是直接切某一个包路径,而且只能是两个数据源互切。两种方式各有各的好处,看业务需要。不过总的来说,能解决问题的方法就是好方法。

最后附一下JNDI数据源在application.properties文件中的配置

1
spring.datasource.jndi-name=java:comp/env/jdbc/AAA,java:comp/env/jdbc/BBB

其实,JNDI数据源也可以直接配置到application.properties文件中,或者两种模式都支持,此处不做累述。

————————————————华丽的分割线—————————————————-

在项目的进展中,此数据源切换已被改造,增加了Druid数据源加密功能,因为是多数据源加密,和官网的有些不一样,代码就不一一累述,读者若有需要,可自行研究或联系博主获取


关注我的微信公众号:FramePower
我会不定期发布相关技术积累,欢迎对技术有追求、志同道合的朋友加入,一起学习成长!


微信公众号

如果文章对你有帮助,欢迎点击上方按钮打赏作者