基于注解的动态数据源实现
需求
有些项目不只访问一个数据库,可能需要访问多个数据库,那么就会有一个问题,怎么进行数据源的切换.
动态数据源
解决这个需求的一个常见解决方案是使用动态数据源.下面将按部就班的来介绍一下如何实现基于注解的动态数据源.完整的代码请参考https://github.com/CodeShowZz/data-source/tree/master/dynamic-data-source.
第一步:配置数据源
将项目中需要使用的数据源放到一个配置文件中,比如叫做jdbc.properties,在我的例子中,我有两个数据源,一个是learning库,另外一个是test库.
数据库配置文件:
1 2 3 4 5 6 7 8 9
| spring.datasource.test.driver-class-name=com.mysql.jdbc.Driver spring.datasource.test.jdbc-url=jdbc:mysql: spring.datasource.test.username=root spring.datasource.test.password=123456
spring.datasource.learning.driver-class-name=com.mysql.jdbc.Driver spring.datasource.learning.jdbc-url=jdbc:mysql: spring.datasource.learning.username=root spring.datasource.learning.password=123456
|
数据源常量类:
1 2 3 4 5 6
| public class DataSourceConstants {
public static final String DB_LEARNING = "learning";
public static final String DB_TEST= "test"; }
|
动态数据源类:
1 2 3 4 5 6 7
| public class DynamicDataSource extends AbstractRoutingDataSource {
@Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getContextKey(); } }
|
这里使用了一个DynamicDataSourceContextHolder
类,将在下面进行讲解.
数据源配置:
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
| @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) @Configuration @PropertySource("classpath:jdbc.properties") @MapperScan(basePackages = "com.dynamic.datasource.dao") public class DynamicDataSourceConfig {
@Bean(DataSourceConstants.DB_LEARNING) @ConfigurationProperties(prefix = "spring.datasource.learning") public DataSource learningDataSource() { return DataSourceBuilder.create().build(); }
@Bean(DataSourceConstants.DB_TEST) @ConfigurationProperties(prefix = "spring.datasource.test") public DataSource testDataSource() { return DataSourceBuilder.create().build(); }
@Bean @Primary public DataSource dynamicDataSource() { Map<Object, Object> dataSourceMap = new HashMap(2); dataSourceMap.put(DataSourceConstants.DB_LEARNING, learningDataSource()); dataSourceMap.put(DataSourceConstants.DB_TEST, testDataSource()); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(dataSourceMap); dynamicDataSource.setDefaultTargetDataSource(testDataSource()); return dynamicDataSource; } }
|
在这里讲一下具体的原理,首先我们定义了两个数据源,然后在dynamicDataSource
方法中定义了一个Map
,将两个数据源以(名称,数据源)的形式放入.接着调用setTargetDataSources
将Map
设置进去,并通过setDefaultTargetDataSource
设置了默认数据源.在每次执行sql语句时,将通过DynamicDataSource
类实现的determineCurrentLookupKey
方法返回的key从Map
中找到对应的数据源,如果没有找到,将使用默认数据源.
了解了这个原理,那么改变determineCurrentLookupKey
方法返回的key就可以实现数据源的切换,那如何改造这个方法使得可以动态切换数据源呢?通常来说,会将它放在ThreadLocal
中.
第二步:引入ThreadLocal
定义ThreadLocal对象:
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
| public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();
public static void setContextKey(String key){ DATASOURCE_CONTEXT_KEY_HOLDER.set(key); }
public static String getContextKey(){ String key = DATASOURCE_CONTEXT_KEY_HOLDER.get(); return key == null? DataSourceConstants.DB_TEST:key; }
public static void removeContextKey(){ DATASOURCE_CONTEXT_KEY_HOLDER.remove(); } }
|
很清晰可以看到上面通过ThreadLocal
来动态的修改数据源对应的key值,以此来决定某次数据库操作使用的是哪个数据源.至此,一个简单的动态数据源实现就搞定了,接下来可以测试一下.
第三步:测试
1 2 3 4 5 6 7 8 9 10 11
| @Test public void testDynamicDataSource() { Student student = studentDao.queryById(1); System.out.println(student); DynamicDataSourceContextHolder.setContextKey(DataSourceConstants.DB_LEARNING); System.out.println(userDao.selectById(1)); DynamicDataSourceContextHolder.removeContextKey(); DynamicDataSourceContextHolder.setContextKey(DataSourceConstants.DB_TEST); System.out.println(studentDao.queryById(1)); DynamicDataSourceContextHolder.removeContextKey(); }
|
这样,就可以实现动态数据源了,但是可以很清楚的看到,我们需要在做数据库操作时设置ThreadLocal
的值,使用后还要清除值,如果能够尽可能消除这种样板代码就更好了.我们可以引入AOP,并自定义注解来做这件事.
第四步:引入AOP
注解:
1 2 3 4 5 6 7 8
| @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DS {
String value() default DataSourceConstants.DB_TEST; }
|
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 25 26 27 28 29 30 31 32 33 34
| @Aspect @Component public class DynamicDataSourceAspect {
@Pointcut("@annotation(com.dynamic.datasource.annotation.DS)") public void dataSourcePointCut() {
}
@Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ String dsKey = getDSAnnotation(joinPoint).value(); DynamicDataSourceContextHolder.setContextKey(dsKey); try{ return joinPoint.proceed(); }finally { DynamicDataSourceContextHolder.removeContextKey(); } }
private DS getDSAnnotation(ProceedingJoinPoint joinPoint) { Class<?> targetClass = joinPoint.getTarget().getClass(); DS classAnnotation = targetClass.getAnnotation(DS.class); if (classAnnotation != null) { return classAnnotation; } MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); return methodSignature.getMethod().getAnnotation(DS.class); }
}
|
在Dao层接口的类或方法上添加注解:
1 2 3 4 5
| @Mapper public interface StudentDao { @DS(DataSourceConstants.DB_TEST) Student queryById(Integer id); }
|
1 2 3 4 5
| @Mapper @DS(DataSourceConstants.DB_LEARNING) public interface UserDao { User selectById(Integer id); }
|
第五步:再次测试
1 2 3 4 5 6 7
| @Test public void testDynamicDataSourceUseAnnotation() { Student student = studentDao.queryById(1); System.out.println(student); System.out.println(userDao.selectById(1)); System.out.println(studentDao.queryById(1)); }
|
这样基于注解的动态数据源就实现完成了.