@@ -63,6 +63,7 @@ | |||||
<module>spring-boot-demo-codegen</module> | <module>spring-boot-demo-codegen</module> | ||||
<module>spring-boot-demo-graylog</module> | <module>spring-boot-demo-graylog</module> | ||||
<module>spring-boot-demo-ldap</module> | <module>spring-boot-demo-ldap</module> | ||||
<module>spring-boot-demo-dynamic-datasource</module> | |||||
</modules> | </modules> | ||||
<packaging>pom</packaging> | <packaging>pom</packaging> | ||||
@@ -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/ |
@@ -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'); |
@@ -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'); |
@@ -0,0 +1,71 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | |||||
<modelVersion>4.0.0</modelVersion> | |||||
<artifactId>spring-boot-demo-dynamic-datasource</artifactId> | |||||
<version>1.0.0-SNAPSHOT</version> | |||||
<packaging>jar</packaging> | |||||
<name>spring-boot-demo-dynamic-datasource</name> | |||||
<description>Demo project for Spring Boot</description> | |||||
<parent> | |||||
<groupId>com.xkcoding</groupId> | |||||
<artifactId>spring-boot-demo</artifactId> | |||||
<version>1.0.0-SNAPSHOT</version> | |||||
</parent> | |||||
<properties> | |||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | |||||
<java.version>1.8</java.version> | |||||
</properties> | |||||
<dependencies> | |||||
<dependency> | |||||
<groupId>org.springframework.boot</groupId> | |||||
<artifactId>spring-boot-starter-web</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework.boot</groupId> | |||||
<artifactId>spring-boot-starter-aop</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>tk.mybatis</groupId> | |||||
<artifactId>mapper-spring-boot-starter</artifactId> | |||||
<version>2.1.5</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>mysql</groupId> | |||||
<artifactId>mysql-connector-java</artifactId> | |||||
<scope>runtime</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.projectlombok</groupId> | |||||
<artifactId>lombok</artifactId> | |||||
<optional>true</optional> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework.boot</groupId> | |||||
<artifactId>spring-boot-starter-test</artifactId> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
</dependencies> | |||||
<build> | |||||
<finalName>spring-boot-demo-dynamic-datasource</finalName> | |||||
<plugins> | |||||
<plugin> | |||||
<groupId>org.springframework.boot</groupId> | |||||
<artifactId>spring-boot-maven-plugin</artifactId> | |||||
</plugin> | |||||
</plugins> | |||||
</build> | |||||
</project> |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 启动器 | |||||
* </p> | |||||
* | |||||
* @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<DatasourceConfig> datasourceConfigs = configMapper.selectAll(); | |||||
System.out.println("加载其余数据源配置列表: " + datasourceConfigs); | |||||
datasourceConfigs.forEach(config -> DatasourceConfigCache.INSTANCE.addConfig(config.getId(), config)); | |||||
} | |||||
} |
@@ -0,0 +1,17 @@ | |||||
package com.xkcoding.dynamic.datasource.annotation; | |||||
import java.lang.annotation.*; | |||||
/** | |||||
* <p> | |||||
* 默认数据源 | |||||
* </p> | |||||
* | |||||
* @author yangkai.shen | |||||
* @date Created in 2019/9/4 17:37 | |||||
*/ | |||||
@Target({ElementType.METHOD}) | |||||
@Retention(RetentionPolicy.RUNTIME) | |||||
@Documented | |||||
public @interface DefaultDatasource { | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 数据源选择器切面 | |||||
* </p> | |||||
* | |||||
* @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(); | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 数据源配置 | |||||
* </p> | |||||
* | |||||
* @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(); | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 通用 mapper 自定义 mapper 文件 | |||||
* </p> | |||||
* | |||||
* @author yangkai.shen | |||||
* @date Created in 2019/9/4 16:23 | |||||
*/ | |||||
@RegisterMapper | |||||
public interface MyMapper<T> extends Mapper<T>, MySqlMapper<T> { | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* mybatis配置 | |||||
* </p> | |||||
* | |||||
* @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(); | |||||
} | |||||
} |
@@ -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.*; | |||||
/** | |||||
* <p> | |||||
* 数据源配置 Controller | |||||
* </p> | |||||
* | |||||
* @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); | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 用户 Controller | |||||
* </p> | |||||
* | |||||
* @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<User> getUserList() { | |||||
return userMapper.selectAll(); | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 数据源配置缓存 | |||||
* </p> | |||||
* | |||||
* @author yangkai.shen | |||||
* @date Created in 2019/9/4 17:13 | |||||
*/ | |||||
public enum DatasourceConfigCache { | |||||
/** | |||||
* 当前实例 | |||||
*/ | |||||
INSTANCE; | |||||
/** | |||||
* 管理动态数据源列表。 | |||||
*/ | |||||
private static final Map<Long, DatasourceConfig> 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); | |||||
} | |||||
} |
@@ -0,0 +1,40 @@ | |||||
package com.xkcoding.dynamic.datasource.datasource; | |||||
/** | |||||
* <p> | |||||
* 数据源标识管理 | |||||
* </p> | |||||
* | |||||
* @author yangkai.shen | |||||
* @date Created in 2019/9/4 14:16 | |||||
*/ | |||||
public class DatasourceConfigContextHolder { | |||||
private static final ThreadLocal<Long> 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); | |||||
} | |||||
} |
@@ -0,0 +1,91 @@ | |||||
package com.xkcoding.dynamic.datasource.datasource; | |||||
import com.zaxxer.hikari.HikariDataSource; | |||||
import java.util.Map; | |||||
import java.util.concurrent.ConcurrentHashMap; | |||||
/** | |||||
* <p> | |||||
* 数据源管理 | |||||
* </p> | |||||
* | |||||
* @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<Long, DatasourceManager> 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); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,57 @@ | |||||
package com.xkcoding.dynamic.datasource.datasource; | |||||
import com.zaxxer.hikari.HikariDataSource; | |||||
import lombok.Getter; | |||||
import java.time.LocalDateTime; | |||||
/** | |||||
* <p> | |||||
* 数据源管理类 | |||||
* </p> | |||||
* | |||||
* @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(); | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 数据源缓存释放调度器 | |||||
* </p> | |||||
* | |||||
* @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); | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 动态数据源 | |||||
* </p> | |||||
* | |||||
* @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; | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 数据源配置 Mapper | |||||
* </p> | |||||
* | |||||
* @author yangkai.shen | |||||
* @date Created in 2019/9/4 16:20 | |||||
*/ | |||||
@Mapper | |||||
public interface DatasourceConfigMapper extends MyMapper<DatasourceConfig> { | |||||
} |
@@ -0,0 +1,15 @@ | |||||
package com.xkcoding.dynamic.datasource.mapper; | |||||
import com.xkcoding.dynamic.datasource.config.MyMapper; | |||||
import com.xkcoding.dynamic.datasource.model.User; | |||||
/** | |||||
* <p> | |||||
* 用户 Mapper | |||||
* </p> | |||||
* | |||||
* @author yangkai.shen | |||||
* @date Created in 2019/9/4 16:49 | |||||
*/ | |||||
public interface UserMapper extends MyMapper<User> { | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 数据源配置表 | |||||
* </p> | |||||
* | |||||
* @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); | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* 用户 | |||||
* </p> | |||||
* | |||||
* @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; | |||||
} |
@@ -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; | |||||
/** | |||||
* <p> | |||||
* Spring 工具类 | |||||
* </p> | |||||
* | |||||
* @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> T getBean(String name) { | |||||
return (T) applicationContext.getBean(name); | |||||
} | |||||
/** | |||||
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. | |||||
*/ | |||||
public static <T> T getBean(Class<T> 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(); | |||||
} | |||||
} |
@@ -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 |
@@ -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() { | |||||
} | |||||
} |