# spring-boot-demo-dynamic-datasource
> 此 demo 主要演示了 Spring Boot 项目如何通过接口`动态添加/删除`数据源,添加数据源之后如何`动态切换`数据源,然后使用 mybatis 查询切换后的数据源的数据。
## 1. 环境准备
1. 执行 db 目录下的SQL脚本
2. 在默认数据源下执行 `init.sql`
3. 在所有数据源分别执行 `user.sql`
## 2. 主要代码
### 2.1.pom.xml
```xml
* 数据源配置 *
* * @author yangkai.shen * @date Created in 2019-09-04 10:27 */ @Configuration public class DatasourceConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { DataSourceBuilder> dataSourceBuilder = DataSourceBuilder.create(); dataSourceBuilder.type(DynamicDataSource.class); return dataSourceBuilder.build(); } } ``` - MybatisConfiguration.java > 这个类主要是将我们上一步构建出来的数据源配置到 Mybatis 的 `SqlSessionFactory` 里 ```java /** ** mybatis配置 *
* * @author yangkai.shen * @date Created in 2019-09-04 16:20 */ @Configuration @MapperScan(basePackages = "com.xkcoding.dynamicdatasource.mapper", sqlSessionFactoryRef = "sqlSessionFactory") public class MybatisConfiguration { /** * 创建会话工厂。 * * @param dataSource 数据源 * @return 会话工厂 */ @Bean(name = "sqlSessionFactory") @SneakyThrows public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } } ``` ### 2.3. 动态数据源主要逻辑 - DatasourceConfigContextHolder.java > 该类主要用于绑定当前线程所使用的数据源 id,通过 ThreadLocal 保证同一线程内不可被修改 ```java /** ** 数据源标识管理 *
* * @author yangkai.shen * @date Created in 2019-09-04 14:16 */ public class DatasourceConfigContextHolder { private static final ThreadLocal* 动态数据源 *
* * @author yangkai.shen * @date Created in 2019-09-04 10:41 */ @Slf4j public class DynamicDataSource extends HikariDataSource { @Override public Connection getConnection() throws SQLException { // 获取当前数据源 id Long id = DatasourceConfigContextHolder.getCurrentDatasourceConfig(); // 根据当前id获取数据源 HikariDataSource datasource = DatasourceHolder.INSTANCE.getDatasource(id); if (null == datasource) { datasource = initDatasource(id); } return datasource.getConnection(); } /** * 初始化数据源 * @param id 数据源id * @return 数据源 */ private HikariDataSource initDatasource(Long id) { HikariDataSource dataSource = new HikariDataSource(); // 判断是否是默认数据源 if (DatasourceHolder.DEFAULT_ID.equals(id)) { // 默认数据源根据 application.yml 配置的生成 DataSourceProperties properties = SpringUtil.getBean(DataSourceProperties.class); dataSource.setJdbcUrl(properties.getUrl()); dataSource.setUsername(properties.getUsername()); dataSource.setPassword(properties.getPassword()); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); } else { // 不是默认数据源,通过缓存获取对应id的数据源的配置 DatasourceConfig datasourceConfig = DatasourceConfigCache.INSTANCE.getConfig(id); if (datasourceConfig == null) { throw new RuntimeException("无此数据源"); } dataSource.setJdbcUrl(datasourceConfig.buildJdbcUrl()); dataSource.setUsername(datasourceConfig.getUsername()); dataSource.setPassword(datasourceConfig.getPassword()); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); } // 将创建的数据源添加到数据源管理器中,绑定当前线程 DatasourceHolder.INSTANCE.addDatasource(id, dataSource); return dataSource; } } ``` - DatasourceScheduler.java > 该类主要用于调度任务 ```java /** ** 数据源缓存释放调度器 *
* * @author yangkai.shen * @date Created in 2019-09-04 14:42 */ public enum DatasourceScheduler { /** * 当前实例 */ INSTANCE; private AtomicInteger cacheTaskNumber = new AtomicInteger(1); private ScheduledExecutorService scheduler; DatasourceScheduler() { create(); } private void create() { this.shutdown(); this.scheduler = new ScheduledThreadPoolExecutor(10, r -> new Thread(r, String.format("Datasource-Release-Task-%s", cacheTaskNumber.getAndIncrement()))); } private void shutdown() { if (null != this.scheduler) { this.scheduler.shutdown(); } } public void schedule(Runnable task,long delay){ this.scheduler.scheduleAtFixedRate(task, delay, delay, TimeUnit.MILLISECONDS); } } ``` - DatasourceManager.java > 该类主要用于管理数据源,记录数据源最后使用时间,同时判断是否长时间未使用,超过一定时间未使用,会被释放连接 ```java /** ** 数据源管理类 *
* * @author yangkai.shen * @date Created in 2019-09-04 14:27 */ public class DatasourceManager { /** * 默认释放时间 */ private static final Long DEFAULT_RELEASE = 10L; /** * 数据源 */ @Getter private HikariDataSource dataSource; /** * 上一次使用时间 */ private LocalDateTime lastUseTime; public DatasourceManager(HikariDataSource dataSource) { this.dataSource = dataSource; this.lastUseTime = LocalDateTime.now(); } /** * 是否已过期,如果过期则关闭数据源 * * @return 是否过期,{@code true} 过期,{@code false} 未过期 */ public boolean isExpired() { if (LocalDateTime.now().isBefore(this.lastUseTime.plusMinutes(DEFAULT_RELEASE))) { return false; } this.dataSource.close(); return true; } /** * 刷新上次使用时间 */ public void refreshTime() { this.lastUseTime = LocalDateTime.now(); } } ``` - DatasourceHolder.java > 该类主要用于管理数据源,同时通过 `DatasourceScheduler` 定时检查数据源是否长时间未使用,超时则释放连接 ```java /** ** 数据源管理 *
* * @author yangkai.shen * @date Created in 2019-09-04 14:23 */ public enum DatasourceHolder { /** * 当前实例 */ INSTANCE; /** * 启动执行,定时5分钟清理一次 */ DatasourceHolder() { DatasourceScheduler.INSTANCE.schedule(this::clearExpiredDatasource, 5 * 60 * 1000); } /** * 默认数据源的id */ public static final Long DEFAULT_ID = -1L; /** * 管理动态数据源列表。 */ private static final Map* 数据源配置缓存 *
* * @author yangkai.shen * @date Created in 2019-09-04 17:13 */ public enum DatasourceConfigCache { /** * 当前实例 */ INSTANCE; /** * 管理动态数据源列表。 */ private static final Map* 启动器 *
* * @author yangkai.shen * @date Created in 2019-09-04 17:57 */ @SpringBootApplication @RequiredArgsConstructor(onConstructor_ = @Autowired) public class SpringBootDemoDynamicDatasourceApplication implements CommandLineRunner { private final DatasourceConfigMapper configMapper; public static void main(String[] args) { SpringApplication.run(SpringBootDemoDynamicDatasourceApplication.class, args); } @Override public void run(String... args) { // 设置默认的数据源 DatasourceConfigContextHolder.setDefaultDatasource(); // 查询所有数据库配置列表 List* 数据源选择器切面 *
* * @author yangkai.shen * @date Created in 2019-09-04 16:52 */ @Aspect @Component @RequiredArgsConstructor(onConstructor_ = @Autowired) public class DatasourceSelectorAspect { @Pointcut("execution(public * com.xkcoding.dynamic.datasource.controller.*.*(..))") public void datasourcePointcut() { } /** * 前置操作,拦截具体请求,获取header里的数据源id,设置线程变量里,用于后续切换数据源 */ @Before("datasourcePointcut()") public void doBefore(JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); // 排除不可切换数据源的方法 DefaultDatasource annotation = method.getAnnotation(DefaultDatasource.class); if (null != annotation) { DatasourceConfigContextHolder.setDefaultDatasource(); } else { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes; HttpServletRequest request = attributes.getRequest(); String configIdInHeader = request.getHeader("Datasource-Config-Id"); if (StringUtils.hasText(configIdInHeader)) { long configId = Long.parseLong(configIdInHeader); DatasourceConfigContextHolder.setCurrentDatasourceConfig(configId); } else { DatasourceConfigContextHolder.setDefaultDatasource(); } } } /** * 后置操作,设置回默认的数据源id */ @AfterReturning("datasourcePointcut()") public void doAfter() { DatasourceConfigContextHolder.setDefaultDatasource(); } } ``` 此时需要考虑,我们是否每个方法都允许用户去切换数据源呢?答案肯定是不行的,所以我们定义了一个注解去标识,当前方法仅可以使用默认数据源。 ```java /** ** 用户标识仅可以使用默认数据源 *
* * @author yangkai.shen * @date Created in 2019-09-04 17:37 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DefaultDatasource { } ``` 完结,撒花✿✿ヽ(°▽°)ノ✿