diff --git a/spring-boot-demo-dynamic-datasource/README.md b/spring-boot-demo-dynamic-datasource/README.md
new file mode 100644
index 0000000..e697230
--- /dev/null
+++ b/spring-boot-demo-dynamic-datasource/README.md
@@ -0,0 +1,669 @@
+# 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/9/4 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/9/4 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/9/4 14:16 + */ +public class DatasourceConfigContextHolder { + private static final ThreadLocal+ * 动态数据源 + *
+ * + * @author yangkai.shen + * @date Created in 2019/9/4 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/9/4 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/9/4 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/9/4 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/9/4 17:13 + */ +public enum DatasourceConfigCache { + /** + * 当前实例 + */ + INSTANCE; + + /** + * 管理动态数据源列表。 + */ + private static final Map+ * 启动器 + *
+ * + * @author yangkai.shen + * @date Created in 2019/9/4 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/9/4 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/9/4 17:37 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DefaultDatasource { +} +``` + +完结,撒花✿✿ヽ(°▽°)ノ✿ \ No newline at end of file diff --git a/spring-boot-demo-dynamic-datasource/assets/image-20190905164824155.png b/spring-boot-demo-dynamic-datasource/assets/image-20190905164824155.png new file mode 100644 index 0000000..c8a18f2 Binary files /dev/null and b/spring-boot-demo-dynamic-datasource/assets/image-20190905164824155.png differ diff --git a/spring-boot-demo-dynamic-datasource/assets/image-20190905165240373.png b/spring-boot-demo-dynamic-datasource/assets/image-20190905165240373.png new file mode 100644 index 0000000..1731547 Binary files /dev/null and b/spring-boot-demo-dynamic-datasource/assets/image-20190905165240373.png differ diff --git a/spring-boot-demo-dynamic-datasource/assets/image-20190905165323097.png b/spring-boot-demo-dynamic-datasource/assets/image-20190905165323097.png new file mode 100644 index 0000000..4dd7a2b Binary files /dev/null and b/spring-boot-demo-dynamic-datasource/assets/image-20190905165323097.png differ diff --git a/spring-boot-demo-dynamic-datasource/assets/image-20190905165350355.png b/spring-boot-demo-dynamic-datasource/assets/image-20190905165350355.png new file mode 100644 index 0000000..3dea585 Binary files /dev/null and b/spring-boot-demo-dynamic-datasource/assets/image-20190905165350355.png differ