第二十周_S-AbstractRoutingDataSource搭建多数据源

249 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情 项目中一直在使用数据源,就一个注解,实现数据源切换,感觉很屌。特此,学习一下,自己从 0 搭建一个支持多数据源切换的学习 demo。

搭建基础的 SpringBoot 项目,然后引入下面的 jar 包

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid-spring-boot-starter</artifactId>
  <version>1.2.8</version>
</dependency>

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.3.0</version>
</dependency>

以下的代码是基于项目已经能正常访问数据库,只是想多一个数据源的情况。

工具类

通过实例名获取实例 Bean

@Component
    public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware
{
    /** Spring应用上下文环境 */
    private static ConfigurableListableBeanFactory beanFactory;

    private static ApplicationContext applicationContext;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
    {
        SpringUtils.beanFactory = beanFactory;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
    {
        SpringUtils.applicationContext = applicationContext;
    }

    /**
* 获取对象
*
* 
*/

    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException
    {
        return (T) beanFactory.getBean(name);
    }
}

多数据源枚举

public enum DataSourceType {
    master,
    slave
}

配置类

DruidConfig

  1. 实现多数据源的 Bean,几个数据源就得写几个
  2. 设置到基于实现AbstractRoutingDataSource抽象类的 DynamicDataSource 类,注入容器。
@Configuration
public class DruidConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    /**
     * @param druidProperties
     * @return
     * @ConditionalOnProperty 通过配置文件的属性值,来判定此 Bean 是否会被注入
     */
    //如果有多个其他数据源,则写多个即可
    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean(name = "dynamicDataSource")
    @Primary  
    public DynamicDataSource dataSource(DataSource masterDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.master.name(), masterDataSource);
        //若有多个,则下面 set 多个
        setDataSource(targetDataSources, DataSourceType.slave.name(), "slaveDataSource");
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }

    /**
     * 设置数据源
     *
     * @param targetDataSources
     * @param sourceName
     * @param beanName
     */
    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
        try {
            DataSource dataSource = SpringUtils.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        } catch (Exception e) {
        }
    }

}

DynamicDataSource

继承了由 Spring 提供的轻量级多数据源切换类:AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法

public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

//确定当前查找键
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

DynamicDataSourceContextHolder

提供一个 ThreadLocal 保证每个使用切换数据源的线程拥有独立的副本

public class DynamicDataSourceContextHolder {
    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

    /**
     * ThreadLocal 为每个使用该变量的线程提供独立的变量副本
     * 所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程所对应的副本
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER=new ThreadLocal<>();

    /**
     * 设置数据源的变量
     * @param dsType
     */
    public static void setDataSourceType(String dsType){
        log.info("切换到 {} 数据源",dsType);
        CONTEXT_HOLDER.set(dsType);
    }

    /**
     * 获得数据源的变量
     * @return
     */
    public static String getDataSourceType(){
        return CONTEXT_HOLDER.get();
    }

    /**
     *  清空数据源变量
     */
    public static void clearDataSourceType()
    {
        CONTEXT_HOLDER.remove();
    }

}

注解实现

juejin.cn/post/718618… 可以参考以前写切面实战内容

注解 DataSource

@Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface DataSource {

        public DataSourceType value() default DataSourceType.master;

    }

切面实现

@Aspect
    @Order(1)
    @Component
    public class DataSourceAspect {

        @Pointcut("@annotation(com.practice.springdemo.datasourceConfig.annotation.DataSource)"
                  + "|| @within(com.practice.springdemo.datasourceConfig.annotation.DataSource)")
        public void dsPointCut() {
        }

        /**
* 获取需要切换的数据源
*
* @param point
* @return
*/
        public DataSource getDataSource(ProceedingJoinPoint point) {
            MethodSignature signature = (MethodSignature) point.getSignature();
            DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
            if (Objects.nonNull(dataSource)) {
                return dataSource;
            }
            return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
        }

        @Around("dsPointCut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            DataSource dataSource = getDataSource(point);
            if (Objects.nonNull(dataSource)) {
                DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
            }

            try {
                return point.proceed();
            } finally {
                DynamicDataSourceContextHolder.clearDataSourceType();
            }

        }

    }

使用方式

直接在方法上面加上你指定的数据源即可 @DataSource(DataSourceType.slave)

@DataSource(DataSourceType.slave)
    @Override
    public void dataSlave() {
        Map map = dataSourceMapper.dataSlave();
        System.out.println("slave:" + map);
    }

总结

  • AbstractRoutingDataSource 类实现可动态路由的数据源
  • DruidConfig 在注入动态数据源的时候必须使用 @Primary 修饰,这样才能先初始化。否则报错 Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required。
  • @ConditionalOnProperty 注解的使用
  • 实现 InitializingBean 类的 afterPropertiesSet() 方法能在项目初始化的时候运行(AbstractRoutingDataSource 实现了)