点击上方“Java知音”,选择“置顶公众号”
技术文章第一时间送达!
作者:殷天文
www.jianshu.com/p/0a485c965b8b
推荐阅读(点击即可跳转阅读)
2. IntelliJ IDEA 从入门到上瘾教程,2019图文版!
4. 权限设计的一些想法和思考
公司项目需求,由于要兼容老系统的数据库结构,需要搭建一个 可以动态切换、添加数据源的后端服务。
参考了过去的项目,通过配置多个SqlSessionFactory 来实现多数据源,这么做的话,未免过于笨重,而且无法实现动态添加数据源这个需求。
通过 spring AbstractRoutingDataSource 为我们抽象了一个 DynamicDataSource 解决这一问题
简单分析下 AbstractRoutingDataSource 的源码
targetDataSources 就是我们的多个数据源,在初始化的时候会调用afterPropertiesSet(),去解析我们的数据源 然后 put 到 resolvedDataSources
实现了 DataSource 的 getConnection(); 我们看看 determineTargetDataSource(); 做了什么
通过下面的 determineCurrentLookupKey();(这个方法需要我们实现) 返回一个key,然后从 resolvedDataSources (其实也就是 targetDataSources) 中 get 一个数据源,实现了每次调用 getConnection(); 打开连接 切换数据源,如果想动态添加的话 只需要重新 set targetDataSources 再调用 afterPropertiesSet() 即可
Talk is cheap. Show me the code
我使用的springboot版本为 1.5.x,下面是核心代码
完整代码:
https://gitee.com/yintianwen7/spring-dynamic-datasource
/** * 多数据源配置 * * @author Taven * */@Configuration@MapperScan("com.gitee.taven.mapper")public class DataSourceConfigurer { /** * DataSource 自动配置并注册 * * @return data source */ @Bean("db0") @Primary @ConfigurationProperties(prefix = "datasource.db0") public DataSource dataSource0() { return DruidDataSourceBuilder.create().build(); } /** * DataSource 自动配置并注册 * * @return data source */ @Bean("db1") @ConfigurationProperties(prefix = "datasource.db1") public DataSource dataSource1() { return DruidDataSourceBuilder.create().build(); } /** * 注册动态数据源 * * @return */ @Bean("dynamicDataSource") public DataSource dynamicDataSource() { DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put("dynamic_db0", dataSource0()); dataSourceMap.put("dynamic_db1", dataSource1()); dynamicRoutingDataSource.setDefaultTargetDataSource(dataSource0());// 设置默认数据源 dynamicRoutingDataSource.setTargetDataSources(dataSourceMap); return dynamicRoutingDataSource; } /** * Sql session factory bean. * Here to config datasource for SqlSessionFactory * <p> * You need to add @{@code @ConfigurationProperties(prefix = "mybatis")}, if you are using *.xml file, * the {@code 'mybatis.type-aliases-package'} and {@code 'mybatis.mapper-locations'} should be set in * {@code 'application.properties'} file, or there will appear invalid bond statement exception * * @return the sql session factory bean */ @Bean @ConfigurationProperties(prefix = "mybatis") public SqlSessionFactoryBean sqlSessionFactoryBean() { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); // 必须将动态数据源添加到 sqlSessionFactoryBean sqlSessionFactoryBean.setDataSource(dynamicDataSource()); return sqlSessionFactoryBean; } /** * 事务管理器 * * @return the platform transaction manager */ @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dynamicDataSource()); }}
通过 ThreadLocal 获取线程安全的数据源 key
package com.gitee.taven.config;public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() { @Override protected String initialValue() { return "dynamic_db0"; } }; /** * To switch DataSource * * @param key the key */ public static void setDataSourceKey(String key) { contextHolder.set(key); } /** * Get current DataSource * * @return data source key */ public static String getDataSourceKey() { return contextHolder.get(); } /** * To set DataSource as default */ public static void clearDataSourceKey() { contextHolder.remove(); }}
动态 添加、切换数据源
/** * 动态数据源 * * @author Taven * */public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private final Logger logger = LoggerFactory.getLogger(getClass()); private static Map<Object, Object> targetDataSources = new HashMap<>(); /** * 设置当前数据源 * * @return */ @Override protected Object determineCurrentLookupKey() { logger.info("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey()); return DynamicDataSourceContextHolder.getDataSourceKey(); } @Override public void setTargetDataSources(Map<Object, Object> targetDataSources) { super.setTargetDataSources(targetDataSources); DynamicRoutingDataSource.targetDataSources = targetDataSources; } /** * 是否存在当前key的 DataSource * * @param key * @return 存在返回 true, 不存在返回 false */ public static boolean isExistDataSource(String key) { return targetDataSources.containsKey(key); } /** * 动态增加数据源 * * @param map 数据源属性 * @return */ public synchronized boolean addDataSource(Map<String, String> map) { try { Connection connection = null; // 排除连接不上的错误 try { Class.forName(map.get(DruidDataSourceFactory.PROP_DRIVERCLASSNAME)); connection = DriverManager.getConnection( map.get(DruidDataSourceFactory.PROP_URL), map.get(DruidDataSourceFactory.PROP_USERNAME), map.get(DruidDataSourceFactory.PROP_PASSWORD)); System.out.println(connection.isClosed()); } catch (Exception e) { return false; } finally { if (connection != null && !connection.isClosed()) connection.close(); } String database = map.get("database");//获取要添加的数据库名 if (StringUtils.isBlank(database)) return false; if (DynamicRoutingDataSource.isExistDataSource(database)) return true; DruidDataSource druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(map); druidDataSource.init(); Map<Object, Object> targetMap = DynamicRoutingDataSource.targetDataSources; targetMap.put(database, druidDataSource); // 当前 targetDataSources 与 父类 targetDataSources 为同一对象 所以不需要set// this.setTargetDataSources(targetMap); this.afterPropertiesSet(); logger.info("dataSource {} has been added", database); } catch (Exception e) { logger.error(e.getMessage()); return false; } return true; }}
可以通过 AOP 或者 手动 DynamicDataSourceContextHolder.setDataSourceKey(String key) 切换数据源。
需要注意的:当我们开启了事务之后,是无法在去切换数据源的。
本文项目源码:
https://gitee.com/yintianwen7/spring-dynamic-datasource
参考文献:
https://github.com/helloworlde/SpringBoot-DynamicDataSource
看完本文有收获?请转发分享给更多人