需求:动态从数据库加载数据源配置
框架:springboot2.5.4、mybatis-plus3.4、druid1.2.5、swagger2.9.2
核心代码:通过继承AbstractRoutingDataSource实现多数据源
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 线程局部变量,保存当前数据源信息
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
private volatile Map<Object, Object> customDataSources = new HashMap<>();
@Value("${source.max-active:2}")
private int maxActive;
@Value("${source.initial-size:1}")
private int initialSize;
@Value("${source.min-idle:1}")
private int minIdle;
@Value("${source.max-wait:30000}")
private int maxWait;
@Value("${spring.datasource.druid.time-between-eviction-runs-millis:60000}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.min-evictable-idle-time-millis:300000}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.test-while-idle:true}")
private boolean testWhileIdle;
@Value("${spring.datasource.druid.test-on-borrow:false}")
private boolean testOnBorrow;
@Value("${spring.datasource.druid.test-on-return:false}")
private boolean testOnReturn;
@Value("${spring.datasource.druid.filters:stat}")
private String filters;
@Value("${source.break-after-acquire-failure:true}")
private boolean breakAfterAcquireFailure;
@Value("${source.connection-error-retry-attempts:0}")
private int connectionErrorRetryAttempts;
/**
* 配置数据源
* @param defaultTargetDataSource 默认数据源
* @param targetDataSources 目标数据源
*/
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
public void setTargetDataSources(Map<Object, Object> param) {
super.setTargetDataSources(param);
this.customDataSources = param;
}
@Override
public Object determineCurrentLookupKey() {
String dataSourceId = CONTEXT_HOLDER.get();
if(StringUtils.isNotBlank(dataSourceId)) {
Map<Object, Object> map = this.customDataSources;
if(map.containsKey(dataSourceId)) {
log.info("当前数据源是:{}", dataSourceId);
} else {
log.info("不存在数据源:{}", dataSourceId);
return null;
}
} else {
log.info("当前是默认数据源");
}
return dataSourceId;
}
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
public void checkCreateDataSource(Repository repository) {
clearDataSource();
String dataSourceId = repository.getRepositoryId();
Map<Object, Object> map = this.customDataSources;
setDataSource(repository.getRepositoryId());
if(map.containsKey(dataSourceId)) {
DruidDataSource druidDataSource = (DruidDataSource)map.get(dataSourceId);
//连接是否有效
boolean isAvailable = true;
DruidPooledConnection connection = null;
try {
connection = druidDataSource.getConnection();
} catch (SQLException sqlException) {
//抛出异常,则说明连接失效。删除链接
log.error(sqlException.getMessage());
isAvailable = false;
deleteDataSource(dataSourceId);
} finally {
if(null != connection) {
try{
connection.close();
} catch (SQLException sqlException) {
log.error(sqlException.getMessage());
}
}
}
if(!isAvailable) {
createDataSource(repository);
}
} else {
createDataSource(repository);
}
}
/**
* 创建数据库连接
* @param repository
*/
private void createDataSource(Repository repository) {
String jdbcUrl = repository.getDbURL();
String username = repository.getDatabaseUsername();
//密码解密
String password = repository.getDatabasePassword();//MD5Utils.convertMD5(repository.getDatabasePassword());
String dirverName = repository.getDriverName();
DruidDataSource instance = new DruidDataSource();
if (StringUtils.isEmpty(dirverName)) {
logger.error("Driver Class Not null: DbId=" + repository.getRepositoryId());
return;
} else {
instance.setDriverClassName(dirverName);
}
instance.setName(repository.getRepositoryId());
instance.setUrl(jdbcUrl.trim());
instance.setUsername(username);
instance.setPassword(password);
instance.setInitialSize(initialSize);
instance.setMinIdle(minIdle);
instance.setMaxActive(maxActive);
instance.setMaxWait(maxWait);
instance.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
instance.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
instance.setTestWhileIdle(false);
instance.setTestOnBorrow(testOnBorrow);
instance.setTestOnReturn(testOnReturn);
instance.setConnectionErrorRetryAttempts(connectionErrorRetryAttempts);
instance.setBreakAfterAcquireFailure(breakAfterAcquireFailure);
try {
instance.init();
this.customDataSources.put(repository.getRepositoryId(), instance);
// 将map赋值给父类的TargetDataSources
setTargetDataSources(this.customDataSources);
// 将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
} catch (Exception e) {
logger.error("Exception during pool initialization", e);
return;
}
}
/**
* 删除无效的数据库连接
* @param dataSourceId
*/
private void deleteDataSource(String dataSourceId) {
Map<Object, Object> map = this.customDataSources;
Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
for (DruidDataSource dataSource : druidDataSourceInstances) {
if (dataSourceId.equals(dataSource.getName())) {
map.remove(dataSourceId);
//从实例中移除当前dataSource
DruidDataSourceStatManager.removeDataSource(dataSource);
// 将map赋值给父类的TargetDataSources
setTargetDataSources(map);
// 将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
}
}
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
public DataSource getCurrentDataSource() {
return (DataSource)this.customDataSources.get(CONTEXT_HOLDER.get());
}
}
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource")
public DataSource masterDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Qualifier("dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
return new DynamicDataSource(masterDataSource, targetDataSources);
}
}
server:
port: 8080
spring:
springmvc:
pathmatch:
matching-strategy: ant_path_matcher
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT&useOldAliasMetadataBehavior=true
username: root
password: root
druid:
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'X' FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
allow:
deny:
login-username: admin
login-password: admin
reset-enable: false
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true
use-global-data-source-stat: true
filters: stat,wall
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
logging:
level:
com.example.demo: debug
#server.servlet.context-path=/demo