基于注解的动态数据源实现

基于注解的动态数据源实现

需求

有些项目不只访问一个数据库,可能需要访问多个数据库,那么就会有一个问题,怎么进行数据源的切换.

动态数据源

解决这个需求的一个常见解决方案是使用动态数据源.下面将按部就班的来介绍一下如何实现基于注解的动态数据源.完整的代码请参考github.com/CodeShowZz/....

第一步:配置数据源

将项目中需要使用的数据源放到一个配置文件中,比如叫做jdbc.properties,在我的例子中,我有两个数据源,一个是learning库,另外一个是test库.

数据库配置文件:

ini 复制代码
spring.datasource.test.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.test.jdbc-url=jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
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://localhost:3306/learning?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.learning.username=root
spring.datasource.learning.password=123456

数据源常量类:

arduino 复制代码
public class DataSourceConstants {
​
    public static final String DB_LEARNING = "learning";
​
    public static final String DB_TEST= "test";
}

动态数据源类:

scala 复制代码
public class DynamicDataSource extends AbstractRoutingDataSource {
​
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getContextKey();
    }
}

这里使用了一个DynamicDataSourceContextHolder类,将在下面进行讲解.

数据源配置:

less 复制代码
@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,将两个数据源以(名称,数据源)的形式放入.接着调用setTargetDataSourcesMap设置进去,并通过setDefaultTargetDataSource设置了默认数据源.在每次执行sql语句时,将通过DynamicDataSource类实现的determineCurrentLookupKey方法返回的key从Map中找到对应的数据源,如果没有找到,将使用默认数据源.

了解了这个原理,那么改变determineCurrentLookupKey方法返回的key就可以实现数据源的切换,那如何改造这个方法使得可以动态切换数据源呢?通常来说,会将它放在ThreadLocal中.

第二步:引入ThreadLocal

定义ThreadLocal对象:

csharp 复制代码
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值,以此来决定某次数据库操作使用的是哪个数据源.至此,一个简单的动态数据源实现就搞定了,接下来可以测试一下.

第三步:测试

ini 复制代码
    @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

注解:

less 复制代码
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
    /**
     * 数据源名称
     */
    String value() default DataSourceConstants.DB_TEST;
}

AOP:

less 复制代码
@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层接口的类或方法上添加注解:

java 复制代码
@Mapper
public interface StudentDao {
    @DS(DataSourceConstants.DB_TEST)
    Student queryById(Integer id);
}
less 复制代码
@Mapper
@DS(DataSourceConstants.DB_LEARNING)
public interface UserDao {
    User selectById(Integer id);
}

第五步:再次测试

csharp 复制代码
 @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));
    }

这样基于注解的动态数据源就实现完成了.

相关推荐
朦胧之10 分钟前
AI 编程开发思维
前端·后端·ai编程
希望永不加班2 小时前
Spring AOP 代理模式:CGLIB 与 JDK 动态代理区别
java·开发语言·后端·spring·代理模式
浮游本尊3 小时前
一次合同同步背后的多阶段流水线:从外部主数据到本地歧义消解
后端
lv__pf3 小时前
springboot原理
java·spring boot·后端
段小二4 小时前
服务一重启全丢了——Spring AI Alibaba Agent 三层持久化完整方案
java·后端
UIUV4 小时前
Go语言入门到精通学习笔记
后端·go·编程语言
lizhongxuan4 小时前
开发 Agent 的坑
后端
段小二4 小时前
Agent 自动把机票改错了,推理完全正确——这才是真正的风险
java·后端
itjinyin4 小时前
ShardingSphere-jdbc 5.5.0 + spring boot 基础配置 - 实战篇
java·spring boot·后端