@@ -63,6 +63,7 @@ | |||
<module>spring-boot-demo-codegen</module> | |||
<module>spring-boot-demo-graylog</module> | |||
<module>spring-boot-demo-ldap</module> | |||
<module>spring-boot-demo-dynamic-datasource</module> | |||
</modules> | |||
<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() { | |||
} | |||
} |