• spring boot动态数据源方案


    动态数据源

    1.背景

    动态数据源在实际的业务场景下需求很多,而且想要沟通多数据库确实需要封装这种工具,针对于bi工具可能涉及到从不同的业务库或者数据仓库中获取数据,动态数据源就更加有意义。

    2.依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>4.3.7.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>4.3.7.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.3.7.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.9</version>
    </dependency>
    <dependency>
        <groupId>com.viewhigh.bi.common</groupId>
        <artifactId>common</artifactId>
        <version>1.0</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.10</version>
    </dependency>

    3.多数据源原理解析

    ① 在应用程序启动的时候初始化默认数据源,并将默认数据源注册到spring上下文中,在这过程中需要实现EnvironmentAware接口中的setEnvironment方法,我们知道setEnvironment方法会在初始化上下文的时候调用,那么利用这个时机就可以根据配置文件初始化默认数据源了,当然可以初始化1个也可以多个。

    /**
     * Created by zzq on 2017/6/14.
     * 负责初始化数据源配置
     */
    public class DataSourceRegister<T> implements EnvironmentAware, ImportBeanDefinitionRegistrar {
        private javax.sql.DataSource defaultTargetDataSource;
        static final String MAINDATASOURCE = "mainDataSource";
    
        public final void setEnvironment(Environment environment) {
            DruidEntity druidEntity = FileUtil.readYmlByClassPath("db_info", DruidEntity.class);
    
            defaultTargetDataSource = DataSourceUtil.createMainDataSource(druidEntity);
        }
    
        public final void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
            // 0.将主数据源添加到数据源集合中
            DataSourceSet.putTargetDataSourcesMap(MAINDATASOURCE, defaultTargetDataSource);
            //1.创建DataSourceBean
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(DataSource.class);
            beanDefinition.setSynthetic(true);
            MutablePropertyValues mpv = beanDefinition.getPropertyValues();
            //spring名称约定为defaultTargetDataSource和targetDataSources
            mpv.addPropertyValue("defaultTargetDataSource", defaultTargetDataSource);
            mpv.addPropertyValue("targetDataSources", DataSourceSet.getTargetDataSourcesMap());
            beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
        }
    }

    在上述代码中注册Data SourceBean时可以指定一个默认数据源,这个数据源就是默认使用的存储于defaultTargetDataSource,而其它的数据源则存在targetDataSources

    ② 那么如果想要使用其它数据源就需要在targetDataSources中通过指定的key去切换就可以。在此之前需要重写Spring中AbstractRoutingDataSource类型的determineCurrentLookupKey方法,而返回值则是即将启动数据源所对应的key,这样就达到了多个数据源切换的目的。

    /**
     * Created by zzq on 2017/6/13.
     */
    public class DataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            String keyDataSource = DataSourceSet.getCurrDataSource();
            LogUtil.info("***当前数据源为[{}]", keyDataSource == null ? "默认数据源" : keyDataSource);
            return keyDataSource;
        }
    }

    4.设计方案

    • 使用方式:

    1) 通在应用程序启动时找到一个初始化时机,并使用import导入数据源注册类即可

    @Import({DataSourceRegister.class})
    @SpringBootApplication//(exclude={DataSourceAutoConfiguration.class,HibernateJpaAutoConfiguration.class})
    @ComponentScan("com.XXX.bi")
    //@EnableCaching
    public class BiApplication {
        public static void main(String[] args) {
            LogUtil.setEnabled(true);//开启日志输出
    
            SpringApplication sa = new SpringApplication(BiApplication.class);
            sa.setBannerMode(Banner.Mode.LOG);
            sa.run(args);
        }
    }

     

    2) 通过注解方式使用

    注解方式比较容易理解,但相对于代码而言处理不够灵活;示例如下:

    @ActivateDataSource("001")
    public List findAll() {
        String sql = "select * from td_bi_datasourcetype where is_remove=0 and organization_id=? ";
    
        Map map = new HashMap();
        map.put("id", String.class);
        map.put("code", String.class);
        map.put("remark", String.class);
        map.put("name", String.class);
        map.put("is_remove", Integer.class);
    
        DynamicBean dynamicBean = new DynamicBean(map);
        String orgId = Identity.getOrganizationId();
        return jdbcTemplateExtend.query(sql, new Object[]{orgId}, dynamicBean.getObject().getClass());
    }

    提供了在方法开始时标记注解,并指定数据源key,为注解参数,则在方法调用过程中即可使用当前key所对应的数据源。

    内幕相信你已经猜到了,我们在数据源初始化的时候维护了一个DataSourceSet集合,该集合中存储了数据源key和对应实际的DataSource对象。而且在contextHolder中存储了当前已经设置的数据源key值,这样在触发查询方法时直接调用了系统determineCurrentLookupKey方法,则在这个方法中使用了contextHolder的key值;

    /**
     * Created by zzq on 2017/6/13.
     */
    public class DataSourceSet {
        private static final ThreadLocal<String> contextHolder = new ThreadLocal();
    
        private static List<String> dataSourceKeyList = new CopyOnWriteArrayList<String>();
    
        private static Map targetDataSourcesMap = new ConcurrentHashMap();
    
        public static Object putTargetDataSourcesMap(Object key, Object dataSource) {
            dataSourceKeyList.add(key.toString());
            return targetDataSourcesMap.put(key, dataSource);
        }
    
        public static Object removeTargetDataSourcesMap(Object key) {
            try {
                dataSourceKeyList.remove(key);
                return targetDataSourcesMap.remove(key);
            } catch (Exception e) {
                e.printStackTrace();
                throw new CustomException(00000, "移除DataSourceSet数据源信息时出现异常,可能由于dataSourceKeyList或targetDataSourcesMap没有该item项");
            }
        }
    
        public static Map getTargetDataSourcesMap() {
            return targetDataSourcesMap;
        }
    
        public static void setCurrDataSource(String ds) {
            contextHolder.set(ds);
        }
    
        public static String getCurrDataSource() {
            return contextHolder.get();
        }
    
        public static void clearCurrDataSource() {
            contextHolder.remove();
        }
    
        public static boolean containsDataSource(String dataSourceKey) {
            return dataSourceKeyList.contains(dataSourceKey);
        }
    }

    这样就可以在aspectJ的aop环绕方式中,方法开始时调用DataSourceSet的设置数据源key来达到切换数据源的目的,在方法调用结束后调用重置key的方法来切换回原来的数据源;

    public class DataSourceAspect {
        @Before("@annotation(ads)")
        public void activateDataSource(JoinPoint point, ActivateDataSource ads) throws Throwable {
            String keyDataSource = ads.value();
            if (!process(keyDataSource, point))
                return;
            LogUtil.info("method:{} ", point.getSignature().getName());
            DataSourceUtil.activateDataSource(keyDataSource, null);
        }
    
        @After("@annotation(ads)")
        public void resetDataSource(JoinPoint point, ActivateDataSource ads) {
            String keyDataSource = ads.value();
            if (!process(keyDataSource, point))
                return;
            LogUtil.info("method:{} ", point.getSignature().getName());
            DataSourceUtil.resetDataSource(keyDataSource);
        }
    
        private boolean process(String keyDataSource, JoinPoint point) {
            if (keyDataSource == null) {
                LogUtil.info("数据源注解已经标识,但value为null[{}]", point.getSignature().getName());
                return false;
            }
            if (keyDataSource.equals(DataSourceRegister.MAINDATASOURCE)) return false;
            return true;
        }
    }

    而在DataSourceUtil中则封装了数据源创建时的一系列动作;那么这个时候你也很有可能会发问,应用程序在启动时会创建一次数据源,如果在程序运行期动态创建数据源怎么办呢,下面就可以揭开这个问题:

    /**
     * 从bean获取数据源
     *
     * @param keyDataSource
     * @return
     */
    private static DataSource loadDataSource(String keyDataSource) {
        if (dataSourceGetStrategy == null) {
            synchronized (DataSourceUtil.class) {
                if (dataSourceGetStrategy == null) {
                    if (!App.getContext().containsBeanDefinition(DATASOURCEGETSTRATEGY))
                        throw new CustomException(ResType.OverrideGetDataSourceInfo);
                    dataSourceGetStrategy = (DataSourceGetStrategy) App.getContext().getBean(DATASOURCEGETSTRATEGY);
                }
            }
        }
        return dataSourceGetStrategy.getDataSource(keyDataSource);
    }

    那么在数据源帮助类中提供了一个抽象类:

    /**
     * 该抽象类必须由子类实现其抽象方法,用于负责动态数据源信息获取
     * <p>
     * Created by zzq on 2017/6/19.
     */
    public abstract class DataSourceGetStrategy {
        public abstract javax.sql.DataSource getDataSource(String keyDataSource);
    
        @Bean(name = DataSourceUtil.DATASOURCEGETSTRATEGY)
        public DataSourceGetStrategy getDataSourceReadStrategy() {
            return this;
        }
    }

    如果想要使用动态数据源框架则必须实现其getDataSource方法,那么在这个方法中你可以获取之前传入的datasourcekey,就可以按照自己的方式创建数据源了,如下示例为创建了一个阿里的druid数据源:

    /**
     * Created by zzq on 2017/6/19.
     */
    @Configuration
    public class GetDataSource extends DataSourceGetStrategy {
        @Autowired
        private JdbcTemplateExtend jdbcTemplateExtend;
    
        @Override
        public DataSource getDataSource(String keyDataSource) {
            String sql = "select t1.url,t1.userName,t1.`password`,t2.driverClassName from " +
                    "td_bi_datasource t1 inner join td_bi_datasourcetype t2 on " +
                    "t1.dataSourceType_id=t2.id where " +
                    "t1.`id`=? and t1.is_remove=0 AND t2.is_remove=0 and t1.organization_id=? and t2.organization_id=?";
    
            String orgId = Identity.getOrganizationId();
    
            List<DataSourceAndType> dataSourceInfoList = jdbcTemplateExtend.query(sql, new Object[]{keyDataSource, orgId, orgId}, DataSourceAndType.class);
            DataSourceAndType dataSourceInfoEntity = null;
            if (dataSourceInfoList.size() > 0)
                dataSourceInfoEntity = dataSourceInfoList.get(0);
    
            if (dataSourceInfoEntity == null)
                return null;
            DruidDataSource datasource = new DruidDataSource();
    
            datasource.setUrl(dataSourceInfoEntity.getUrl());
    
            dbType.put(keyDataSource, dataSourceInfoEntity.getUrl());
    
            datasource.setUsername(dataSourceInfoEntity.getUserName());
            datasource.setPassword(dataSourceInfoEntity.getPassword());
            datasource.setDriverClassName(dataSourceInfoEntity.getDriverClassName());
            datasource.setMaxWait(13000);
            return datasource;
        }
    }

    3) 代码调用方式使用

    相信代码调用的方式会让更多人感觉比较舒适吧!

    和aspect类似的道理,如下代码:

    try {
        DataSourceUtil.activateDataSource(dataSourceKey, dataSource);
        //做自己的事情
    } finally {
        DataSourceUtil.resetDataSource(dataSourceKey);
    }

    常规方式可以使用try finally处理,如果你有更好的方式也可以使用哦!思路就是在你的代码前激活数据源,在自己代码调用最后释放数据源。

    PS:

    ① 在最后强调下,不用担心频繁创建数据源之后的性能问题,因为在一次创建之后,多次使用时DataSourceSet会有保存记录,直接切换数据源,不会有任何的性能消耗;

    ② 如果有临时数据源不希望被缓存则使用DataSourceUtil.activateDataSource(dataSourceKey, dataSource);两个参数的方法重载,第二个参数可以直接自己创建数据源对象传入,使用之后,框架也会将资源释放不做保留;

    ③ SpringBoot动态数据源中的Bean名称为:dataSource

          

     GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(DataSource.class);
            beanDefinition.setSynthetic(true);
            MutablePropertyValues mpv = beanDefinition.getPropertyValues();
            //spring名称约定为defaultTargetDataSource和targetDataSources
            mpv.addPropertyValue("defaultTargetDataSource", defaultTargetDataSource);
            mpv.addPropertyValue("targetDataSources", DataSourceSet.getTargetDataSourcesMap());
            beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);

     项目地址:https://github.com/qq472708969/dynamicDataSource  !

  • 相关阅读:
    Linux | Ubuntu 生成二维码实例
    Ubuntu 添加wine安装程序的快捷方式
    mysql 中文 排序
    Received disconnect from **.**).***.*** port 22:2: Too many authentication failures 解决办法
    php 数组与URL相互转换
    ssh `快捷键` 快速链接服务器
    使用ssh生成密钥并保存在不同的文件(ubuntu)
    H5移动端调试 weinre
    简单的 图片下载 php
    linux 系统生成二维码
  • 原文地址:https://www.cnblogs.com/zzq-include/p/7614853.html
Copyright © 2020-2023  润新知