使用 AbstractRoutingDataSource 实现功能,代码完整贴出,直接放心食用。 

从AbstractRoutingDataSource源码角度简单分析为什么可以实现数据库动态切换。


前言

主从数据库的配置,实现数据同步,配置可参考:

windows配置mysql8.0主从数据库_追寻光的方向的博客-CSDN博客

一、AbstractRoutingDataSource

Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们每次访问数据库之前,设置使用的数据源。从而实现动态切换数据源。

1.源码简单分析

 

 AbstractRoutingDataSource 继承了 AbstractDataSource 类 从而实现了DataSource接口的getConnection()方法。

在AbstractRoutingDataSource 实现 getConnection 的具体方法源码可以看到 ,最终获取的数据源是DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); 这一行。

resolvedDataSources是一个Map对象,就是说动态的数据源是放在这个Map对象的。

Object lookupKey = determineCurrentLookupKey(); 提供一个Map的key来获取

那往下看,我们看下这个 resolvedDataSources 是怎么初始化的,

afterPropertiesSet 方法是 InitializingBean 接口的实现,InitializingBean接口为bean提供了初始化方法的方式,凡是继承该接口的类,在初始化bean的时候都会执行该方法。

但是源码中 resolvedDataSources 的来源是 targetDataSources 

那再来看看targetDataSources 是怎么初始化的。

2.源码分析结论

到此,我们思路就有了

1.在每次数据库访问的时候能控制读取存放数据源resolvedDataSources的key值。

2.调用setTargetDataSources方法,来初始化targetDataSources对象。

二、实现步骤

1.创建一个类继承AbstractRoutingDataSource

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class RoutingDataSource extends AbstractRoutingDataSource {
    private Logger logger = LogManager.getLogger();

    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = RoutingDataSourceHolder.getDataSource();
        logger.info("使用数据源:{}", dataSource);
        return dataSource;
    }
}

重写 determineCurrentLookupKey方法,返回要使用的数据源key值。

2.创建一个管理数据源key值得类RoutingDataSourceHolder 

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class RoutingDataSourceHolder {
    private static Logger logger = LogManager.getLogger();

    private static final ThreadLocal<String> dataSources = new ThreadLocal<>();

    //一个事务内用同一个数据源
    public static void setDataSource(String dataSourceName) {
        if (dataSources.get() == null) {
            dataSources.set(dataSourceName);
            logger.info("设置数据源:{}", dataSourceName);
        }
    }

    public static String getDataSource() {
        return dataSources.get();
    }

    public static void clearDataSource() {
        dataSources.remove();
    }
}

代码设置了一个事务内使用同一个数据源。

两个类解决了动态数据源key值的问题,下面处理初始化targetDataSources对象。

3.配置主从数据库类DataSourceConfigurer

1.DataSourceConfigurer

import com.alibaba.druid.pool.DruidDataSource;
import com.custom.common.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 配置主从数据库
 */
@Configuration
public class DataSourceConfigurer {
    private Logger logger = LogManager.getLogger();

    public final static String MASTER_DATASOURCE = "masterDataSource";
    public final static String SLAVE_DATASOURCE = "slaveDataSource";

    @Bean(MASTER_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource masterDataSource(DataSourceProperties properties) {
        DruidDataSource build = properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
        logger.info("配置主数据库:{}", build);
        return build;
    }

    @Bean(SLAVE_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.slave-datasource")
    public DruidDataSource slaveDataSource(DataSourceProperties properties) {
        DruidDataSource build = properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
        logger.info("配置从数据库:{}", build);
        return build;
    }

    /**
     * Primary 优先使用该Bean
     * DependsOn 先执行主从数据库的配置
     * Qualifier 指定使用哪个Bean
     *
     * @param masterDataSource
     * @param slaveDataSource
     * @return
     */
    @Bean
    @Primary
    @DependsOn(value = {MASTER_DATASOURCE, SLAVE_DATASOURCE})
    public DataSource routingDataSource(@Qualifier(MASTER_DATASOURCE) DruidDataSource masterDataSource,
                                        @Qualifier(SLAVE_DATASOURCE) DruidDataSource slaveDataSource) {
        if (StringUtils.isBlank(slaveDataSource.getUrl())) {
            logger.info("没有配置从数据库,默认使用主数据库");
            return masterDataSource;
        }
        Map<Object, Object> map = new HashMap<>();
        map.put(DataSourceConfigurer.MASTER_DATASOURCE, masterDataSource);
        map.put(DataSourceConfigurer.SLAVE_DATASOURCE, slaveDataSource);
        RoutingDataSource routing = new RoutingDataSource();
        //设置动态数据源
        routing.setTargetDataSources(map);
        //设置默认数据源
        routing.setDefaultTargetDataSource(masterDataSource);
        logger.info("主从数据库配置完成");
        return routing;
    }
}

设置初始化targetDataSources对象关键代码

Map<Object, Object> map = new HashMap<>();
map.put(DataSourceConfigurer.MASTER_DATASOURCE, masterDataSource);
map.put(DataSourceConfigurer.SLAVE_DATASOURCE, slaveDataSource);
RoutingDataSource routing = new RoutingDataSource();
//设置动态数据源
routing.setTargetDataSources(map);
//设置默认数据源
routing.setDefaultTargetDataSource(masterDataSource);

2.application.properties

# ----------------------------------------
# 主数据库
# ----------------------------------------
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/custom?useunicode=true&characterEncoding=utf8&serverTimezone=GMT%2b8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# ----------------------------------------
# 从数据库
# ----------------------------------------
spring.slave-datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.slave-datasource.url=jdbc:mysql://127.0.0.1:3309/custom?useunicode=true&characterEncoding=utf8&serverTimezone=GMT%2b8
spring.slave-datasource.username=root
spring.slave-datasource.password=root
spring.slave-datasource.driver-class-name=com.mysql.cj.jdbc.Driver

一个配置类处理了targetDataSources对象的初始化.

那问题都处理了,那具体要怎么使用呢,关键就是在事务之前调用RoutingDataSourceHolder.setDataSource()方法就可以了。我们写一个aop实现吧。

4.创建aop注解和类

1.DataSourceWith

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface DataSourceWith {
    String key() default "";
}

2.DataSourceWithAspect

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Order(-1)// 保证该AOP在@Transactional之前运行
@Component
public class DataSourceWithAspect {

    /**
     * 使用DataSourceWith注解就拦截
     */
    @Pointcut("@annotation(com.custom.configure.datasource.DataSourceWith)||@within(com.custom.configure.datasource.DataSourceWith)")
    public void doPointcut() {

    }

    /**
     * 方法前,为了在事务前设置
     */
    @Before("doPointcut()")
    public void doBefore(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        // 获取注解对象
        DataSourceWith dataSource = method.getAnnotation(DataSourceWith.class);
        if (dataSource == null) {
            //方法没有就获取类上的
            dataSource = method.getDeclaringClass().getAnnotation(DataSourceWith.class);
        }
        String key = dataSource.key();
        RoutingDataSourceHolder.setDataSource(key);
    }

    @After("doPointcut()")
    public void doAfter(JoinPoint joinPoint) {
        RoutingDataSourceHolder.clearDataSource();
    }

}

3.使用和效果

@DataSourceWith在方法上或者类上都可以。

    /**
     * 获取部门列表
     **/
    @DataSourceWith(key = DataSourceConfigurer.SLAVE_DATASOURCE)
    public List<Dept> findDeptTree() {
        logger.debug("获取部门树数据");
        List<Dept> deptList = new ArrayList<>();
        return deptList;
    }

结果:动态切换成功

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐