SpringBoot动态数据源切换(一)

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


  • 1、Spring的开发者还是挺有先见之明的,为我们提供了扩展Spring的AbstractRoutingDataSource抽象类,我们来看它的源码
    /**
     * 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方法。
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            /**
             * DynamicDataSourceContextHolder代码中使用setDataSource
             * 设置当前的数据源,在路由类中使用getDataSource进行获取,
             * 交给AbstractRoutingDataSource进行注入使用。
             */
            return DynamicDataSourceContextHolder.getDataSource();
        }
    }
  • 3、创建统一数据源管理类DynamicDataSourceContextHolder
    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
    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容器中

    @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注解
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface TargetDataSource {
        String value();
    }

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

  • 7、新建数据源切换AOP切面
@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文件中的配置

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

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

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

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


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


微信公众号
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容