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  !

 

分类:

技术点:

相关文章: