动态数据源
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);