diff --git a/pom.xml b/pom.xml index 1ee5d37..d20d3fc 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,7 @@ spring-boot-demo-codegen spring-boot-demo-graylog spring-boot-demo-ldap + spring-boot-demo-dynamic-datasource pom diff --git a/spring-boot-demo-dynamic-datasource/.gitignore b/spring-boot-demo-dynamic-datasource/.gitignore new file mode 100644 index 0000000..a2a3040 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/spring-boot-demo-dynamic-datasource/db/init.sql b/spring-boot-demo-dynamic-datasource/db/init.sql new file mode 100644 index 0000000..5cabd07 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/db/init.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS `datasource_config` +( + `id` bigint(13) NOT NULL AUTO_INCREMENT COMMENT '主键', + `host` varchar(255) NOT NULL COMMENT '数据库地址', + `port` int(6) NOT NULL COMMENT '数据库端口', + `username` varchar(100) NOT NULL COMMENT '数据库用户名', + `password` varchar(100) NOT NULL COMMENT '数据库密码', + `database` varchar(100) DEFAULT 0 COMMENT '数据库名称', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8 COMMENT ='数据源配置表'; + +INSERT INTO `datasource_config`(`id`, `host`, `port`, `username`, `password`, `database`) +VALUES (1, '127.0.01', 3306, 'root', 'root', 'test'); +INSERT INTO `datasource_config`(`id`, `host`, `port`, `username`, `password`, `database`) +VALUES (2, '192.168.239.4', 3306, 'dmcp', 'Dmcp321!', 'test'); \ No newline at end of file diff --git a/spring-boot-demo-dynamic-datasource/db/user.sql b/spring-boot-demo-dynamic-datasource/db/user.sql new file mode 100644 index 0000000..fc215cb --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/db/user.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS `test_user` +( + `id` bigint(13) NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(255) NOT NULL COMMENT '姓名', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8 COMMENT ='用户表'; + +-- 默认数据库插入如下 SQL +INSERT INTO `test_user`(`id`, `name`) +values (1, '默认数据库用户1'); +INSERT INTO `test_user`(`id`, `name`) +values (2, '默认数据库用户2'); + +-- 测试库1插入如下SQL +INSERT INTO `test_user`(`id`, `name`) +values (1, '测试库1用户1'); +INSERT INTO `test_user`(`id`, `name`) +values (2, '测试库1用户2'); + +-- 测试库2插入如下SQL +INSERT INTO `test_user`(`id`, `name`) +values (1, '测试库2用户1'); +INSERT INTO `test_user`(`id`, `name`) +values (2, '测试库2用户2'); \ No newline at end of file diff --git a/spring-boot-demo-dynamic-datasource/pom.xml b/spring-boot-demo-dynamic-datasource/pom.xml new file mode 100644 index 0000000..1345a4a --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + spring-boot-demo-dynamic-datasource + 1.0.0-SNAPSHOT + jar + + spring-boot-demo-dynamic-datasource + Demo project for Spring Boot + + + com.xkcoding + spring-boot-demo + 1.0.0-SNAPSHOT + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-aop + + + + tk.mybatis + mapper-spring-boot-starter + 2.1.5 + + + + mysql + mysql-connector-java + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + spring-boot-demo-dynamic-datasource + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplication.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplication.java new file mode 100644 index 0000000..b9253f7 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplication.java @@ -0,0 +1,39 @@ +package com.xkcoding.dynamic.datasource; + +import com.xkcoding.dynamic.datasource.datasource.DatasourceConfigCache; +import com.xkcoding.dynamic.datasource.datasource.DatasourceConfigContextHolder; +import com.xkcoding.dynamic.datasource.mapper.DatasourceConfigMapper; +import com.xkcoding.dynamic.datasource.model.DatasourceConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.util.List; + +/** + *

+ * 启动器 + *

+ * + * @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 datasourceConfigs = configMapper.selectAll(); + System.out.println("加载其余数据源配置列表: " + datasourceConfigs); + datasourceConfigs.forEach(config -> DatasourceConfigCache.INSTANCE.addConfig(config.getId(), config)); + } +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/annotation/DefaultDatasource.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/annotation/DefaultDatasource.java new file mode 100644 index 0000000..6d9602e --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/annotation/DefaultDatasource.java @@ -0,0 +1,17 @@ +package com.xkcoding.dynamic.datasource.annotation; + +import java.lang.annotation.*; + +/** + *

+ * 默认数据源 + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 17:37 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DefaultDatasource { +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/aspect/DatasourceSelectorAspect.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/aspect/DatasourceSelectorAspect.java new file mode 100644 index 0000000..fbc4330 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/aspect/DatasourceSelectorAspect.java @@ -0,0 +1,70 @@ +package com.xkcoding.dynamic.datasource.aspect; + +import com.xkcoding.dynamic.datasource.annotation.DefaultDatasource; +import com.xkcoding.dynamic.datasource.datasource.DatasourceConfigContextHolder; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; + +/** + *

+ * 数据源选择器切面 + *

+ * + * @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() { + } + + /** + * 前置通知 用于拦截操作 + */ + @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(); + } + } + } + + @AfterReturning("datasourcePointcut()") + public void doAfter() { + DatasourceConfigContextHolder.setDefaultDatasource(); + } + +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/DatasourceConfiguration.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/DatasourceConfiguration.java new file mode 100644 index 0000000..150ac64 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/DatasourceConfiguration.java @@ -0,0 +1,29 @@ +package com.xkcoding.dynamic.datasource.config; + +import com.xkcoding.dynamic.datasource.datasource.DynamicDataSource; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +/** + *

+ * 数据源配置 + *

+ * + * @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(); + } +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MyMapper.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MyMapper.java new file mode 100644 index 0000000..2410581 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MyMapper.java @@ -0,0 +1,17 @@ +package com.xkcoding.dynamic.datasource.config; + +import tk.mybatis.mapper.annotation.RegisterMapper; +import tk.mybatis.mapper.common.Mapper; +import tk.mybatis.mapper.common.MySqlMapper; + +/** + *

+ * 通用 mapper 自定义 mapper 文件 + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 16:23 + */ +@RegisterMapper +public interface MyMapper extends Mapper, MySqlMapper { +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MybatisConfiguration.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MybatisConfiguration.java new file mode 100644 index 0000000..30d4945 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MybatisConfiguration.java @@ -0,0 +1,37 @@ +package com.xkcoding.dynamic.datasource.config; + +import lombok.SneakyThrows; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tk.mybatis.spring.annotation.MapperScan; + +import javax.sql.DataSource; + +/** + *

+ * 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(); + } +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/DatasourceConfigController.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/DatasourceConfigController.java new file mode 100644 index 0000000..83bae72 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/DatasourceConfigController.java @@ -0,0 +1,44 @@ +package com.xkcoding.dynamic.datasource.controller; + +import com.xkcoding.dynamic.datasource.annotation.DefaultDatasource; +import com.xkcoding.dynamic.datasource.datasource.DatasourceConfigCache; +import com.xkcoding.dynamic.datasource.mapper.DatasourceConfigMapper; +import com.xkcoding.dynamic.datasource.model.DatasourceConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + *

+ * 数据源配置 Controller + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 17:31 + */ +@RestController +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class DatasourceConfigController { + private final DatasourceConfigMapper configMapper; + + /** + * 保存 + */ + @PostMapping("/config") + @DefaultDatasource + public DatasourceConfig insertConfig(@RequestBody DatasourceConfig config) { + configMapper.insertUseGeneratedKeys(config); + DatasourceConfigCache.INSTANCE.addConfig(config.getId(), config); + return config; + } + + /** + * 保存 + */ + @DeleteMapping("/config/{id}") + @DefaultDatasource + public void removeConfig(@PathVariable Long id) { + configMapper.deleteByPrimaryKey(id); + DatasourceConfigCache.INSTANCE.removeConfig(id); + } +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/UserController.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/UserController.java new file mode 100644 index 0000000..67ce6a9 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/UserController.java @@ -0,0 +1,33 @@ +package com.xkcoding.dynamic.datasource.controller; + +import com.xkcoding.dynamic.datasource.mapper.UserMapper; +import com.xkcoding.dynamic.datasource.model.User; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + *

+ * 用户 Controller + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 16:40 + */ +@RestController +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class UserController { + private final UserMapper userMapper; + + /** + * 获取用户列表 + */ + @GetMapping("/user") + public List getUserList() { + return userMapper.selectAll(); + } + +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigCache.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigCache.java new file mode 100644 index 0000000..51324c4 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigCache.java @@ -0,0 +1,58 @@ +package com.xkcoding.dynamic.datasource.datasource; + +import com.xkcoding.dynamic.datasource.model.DatasourceConfig; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + *

+ * 数据源配置缓存 + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 17:13 + */ +public enum DatasourceConfigCache { + /** + * 当前实例 + */ + INSTANCE; + + /** + * 管理动态数据源列表。 + */ + private static final Map CONFIG_CACHE = new ConcurrentHashMap<>(); + + /** + * 添加数据源配置 + * + * @param id 数据源配置id + * @param config 数据源配置 + */ + public synchronized void addConfig(Long id, DatasourceConfig config) { + CONFIG_CACHE.put(id, config); + } + + /** + * 查询数据源配置 + * + * @param id 数据源配置id + * @return 数据源配置 + */ + public synchronized DatasourceConfig getConfig(Long id) { + if (CONFIG_CACHE.containsKey(id)) { + return CONFIG_CACHE.get(id); + } + return null; + } + + /** + * 清除数据源配置 + */ + public synchronized void removeConfig(Long id) { + CONFIG_CACHE.remove(id); + // 同步清除 DatasourceHolder 对应的数据源 + DatasourceHolder.INSTANCE.removeDatasource(id); + } +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigContextHolder.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigContextHolder.java new file mode 100644 index 0000000..389441f --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigContextHolder.java @@ -0,0 +1,40 @@ +package com.xkcoding.dynamic.datasource.datasource; + +/** + *

+ * 数据源标识管理 + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 14:16 + */ +public class DatasourceConfigContextHolder { + private static final ThreadLocal DATASOURCE_HOLDER = ThreadLocal.withInitial(() -> DatasourceHolder.DEFAULT_ID); + + /** + * 设置默认数据源 + */ + public static void setDefaultDatasource() { + DATASOURCE_HOLDER.remove(); + setCurrentDatasourceConfig(DatasourceHolder.DEFAULT_ID); + } + + /** + * 获取当前数据源配置id + * + * @return 数据源配置id + */ + public static Long getCurrentDatasourceConfig() { + return DATASOURCE_HOLDER.get(); + } + + /** + * 设置当前数据源配置id + * + * @param id 数据源配置id + */ + public static void setCurrentDatasourceConfig(Long id) { + DATASOURCE_HOLDER.set(id); + } + +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceHolder.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceHolder.java new file mode 100644 index 0000000..f1ab5b9 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceHolder.java @@ -0,0 +1,91 @@ +package com.xkcoding.dynamic.datasource.datasource; + +import com.zaxxer.hikari.HikariDataSource; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + *

+ * 数据源管理 + *

+ * + * @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 DATASOURCE_CACHE = new ConcurrentHashMap<>(); + + /** + * 添加动态数据源 + * + * @param id 数据源id + * @param dataSource 数据源 + */ + public synchronized void addDatasource(Long id, HikariDataSource dataSource) { + DatasourceManager datasourceManager = new DatasourceManager(dataSource); + DATASOURCE_CACHE.put(id, datasourceManager); + } + + /** + * 查询动态数据源 + * + * @param id 数据源id + * @return 数据源 + */ + public synchronized HikariDataSource getDatasource(Long id) { + if (DATASOURCE_CACHE.containsKey(id)) { + DatasourceManager datasourceManager = DATASOURCE_CACHE.get(id); + datasourceManager.refreshTime(); + return datasourceManager.getDataSource(); + } + return null; + } + + /** + * 清除超时的数据源 + */ + public synchronized void clearExpiredDatasource() { + DATASOURCE_CACHE.forEach((k, v) -> { + // 排除默认数据源 + if (!DEFAULT_ID.equals(k)) { + if (v.isExpired()) { + DATASOURCE_CACHE.remove(k); + } + } + }); + } + + /** + * 清除动态数据源 + * @param id 数据源id + */ + public synchronized void removeDatasource(Long id) { + if (DATASOURCE_CACHE.containsKey(id)) { + // 关闭数据源 + DATASOURCE_CACHE.get(id).getDataSource().close(); + // 移除缓存 + DATASOURCE_CACHE.remove(id); + } + } +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceManager.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceManager.java new file mode 100644 index 0000000..ea57f41 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceManager.java @@ -0,0 +1,57 @@ +package com.xkcoding.dynamic.datasource.datasource; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.Getter; + +import java.time.LocalDateTime; + +/** + *

+ * 数据源管理类 + *

+ * + * @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(); + } +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceScheduler.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceScheduler.java new file mode 100644 index 0000000..f8a4939 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceScheduler.java @@ -0,0 +1,44 @@ +package com.xkcoding.dynamic.datasource.datasource; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + *

+ * 数据源缓存释放调度器 + *

+ * + * @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); + } + +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DynamicDataSource.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DynamicDataSource.java new file mode 100644 index 0000000..4b1b2ce --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DynamicDataSource.java @@ -0,0 +1,61 @@ +package com.xkcoding.dynamic.datasource.datasource; + +import com.xkcoding.dynamic.datasource.model.DatasourceConfig; +import com.xkcoding.dynamic.datasource.utils.SpringUtil; +import com.zaxxer.hikari.HikariDataSource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + *

+ * 动态数据源 + *

+ * + * @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(); + } + + private HikariDataSource initDatasource(Long id) { + HikariDataSource dataSource = new HikariDataSource(); + + if (DatasourceHolder.DEFAULT_ID.equals(id)) { + 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 { + // 获取数据库的配置 + 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; + } +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/DatasourceConfigMapper.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/DatasourceConfigMapper.java new file mode 100644 index 0000000..544e31f --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/DatasourceConfigMapper.java @@ -0,0 +1,17 @@ +package com.xkcoding.dynamic.datasource.mapper; + +import com.xkcoding.dynamic.datasource.config.MyMapper; +import com.xkcoding.dynamic.datasource.model.DatasourceConfig; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * 数据源配置 Mapper + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 16:20 + */ +@Mapper +public interface DatasourceConfigMapper extends MyMapper { +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/UserMapper.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/UserMapper.java new file mode 100644 index 0000000..0824d39 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/UserMapper.java @@ -0,0 +1,15 @@ +package com.xkcoding.dynamic.datasource.mapper; + +import com.xkcoding.dynamic.datasource.config.MyMapper; +import com.xkcoding.dynamic.datasource.model.User; + +/** + *

+ * 用户 Mapper + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 16:49 + */ +public interface UserMapper extends MyMapper { +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/DatasourceConfig.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/DatasourceConfig.java new file mode 100644 index 0000000..5477a94 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/DatasourceConfig.java @@ -0,0 +1,69 @@ +package com.xkcoding.dynamic.datasource.model; + +import lombok.Data; + +import javax.persistence.Column; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import java.io.Serializable; + +/** + *

+ * 数据源配置表 + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 10:58 + */ +@Data +@Table(name = "datasource_config") +public class DatasourceConfig implements Serializable { + /** + * 主键 + */ + @Id + @Column(name = "`id`") + @GeneratedValue(generator = "JDBC") + private Long id; + + /** + * 数据库地址 + */ + @Column(name = "`host`") + private String host; + + /** + * 数据库端口 + */ + @Column(name = "`port`") + private Integer port; + + /** + * 数据库用户名 + */ + @Column(name = "`username`") + private String username; + + /** + * 数据库密码 + */ + @Column(name = "`password`") + private String password; + + /** + * 数据库名称 + */ + @Column(name = "`database`") + private String database; + + /** + * 构造JDBC URL + * + * @return JDBC URL + */ + public String buildJdbcUrl() { + return String.format("jdbc:mysql://%s:%s/%s?useUnicode=true&characterEncoding=utf-8&useSSL=false", this.host, this.port, this.database); + } + +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/User.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/User.java new file mode 100644 index 0000000..0c29077 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/User.java @@ -0,0 +1,35 @@ +package com.xkcoding.dynamic.datasource.model; + +import lombok.Data; + +import javax.persistence.Column; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import java.io.Serializable; + +/** + *

+ * 用户 + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 16:41 + */ +@Data +@Table(name = "test_user") +public class User implements Serializable { + /** + * 主键 + */ + @Id + @Column(name = "`id`") + @GeneratedValue(generator = "JDBC") + private Long id; + + /** + * 姓名 + */ + @Column(name = "`name`") + private String name; +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/utils/SpringUtil.java b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/utils/SpringUtil.java new file mode 100644 index 0000000..a1fb444 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/utils/SpringUtil.java @@ -0,0 +1,86 @@ +package com.xkcoding.dynamic.datasource.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +/** + *

+ * Spring 工具类 + *

+ * + * @author yangkai.shen + * @date Created in 2019/9/4 16:16 + */ +@Slf4j +@Service +@Lazy(false) +public class SpringUtil implements ApplicationContextAware, DisposableBean { + + private static ApplicationContext applicationContext = null; + + /** + * 取得存储在静态变量中的ApplicationContext. + */ + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * 实现ApplicationContextAware接口, 注入Context到静态变量中. + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + SpringUtil.applicationContext = applicationContext; + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) { + return (T) applicationContext.getBean(name); + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + public static T getBean(Class requiredType) { + return applicationContext.getBean(requiredType); + } + + /** + * 清除SpringContextHolder中的ApplicationContext为Null. + */ + public static void clearHolder() { + if (log.isDebugEnabled()) { + log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext); + } + applicationContext = null; + } + + /** + * 发布事件 + * + * @param event 事件 + */ + public static void publishEvent(ApplicationEvent event) { + if (applicationContext == null) { + return; + } + applicationContext.publishEvent(event); + } + + /** + * 实现DisposableBean接口, 在Context关闭时清理静态变量. + */ + @Override + public void destroy() { + SpringUtil.clearHolder(); + } + +} diff --git a/spring-boot-demo-dynamic-datasource/src/main/resources/application.yml b/spring-boot-demo-dynamic-datasource/src/main/resources/application.yml new file mode 100644 index 0000000..8b7e788 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/main/resources/application.yml @@ -0,0 +1,10 @@ +server: + port: 8080 + servlet: + context-path: /demo +spring: + datasource: + url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=utf-8&useSSL=false + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/spring-boot-demo-dynamic-datasource/src/test/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplicationTests.java b/spring-boot-demo-dynamic-datasource/src/test/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplicationTests.java new file mode 100644 index 0000000..f0f7635 --- /dev/null +++ b/spring-boot-demo-dynamic-datasource/src/test/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplicationTests.java @@ -0,0 +1,16 @@ +package com.xkcoding.dynamic.datasource; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SpringBootDemoDynamicDatasourceApplicationTests { + + @Test + public void contextLoads() { + } + +}