场景:主数据源 * 1 其余数据源共用一套entity。新增源从库里读配置,热加载。
默认数据源配置:
package xxx.xxx.xxx;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 数据源配置类
*/
@Configuration
@EnableTransactionManagement
public class DataSourceConfig {
/**
* 配置库数据源名称,做targetDataSource里的key值。
*/
@Value("${initialization-datasource.config.key}")
private String masterDataSourceKey;
@Value("${initialization-datasource.config.url}")
private String configDbUrl;
@Value("${initialization-datasource.config.username}")
private String configDbUserName;
@Value("${initialization-datasource.config.password}")
private String configDbPassword;
@Value("${initialization-datasource.config.driver-class-name}")
private String configDbDriver;
/**
* 连接池大小,稍微小点。
*/
private static final Integer maximumPoolSize = 5;
@Bean(name = "configDataSource")
public DataSource initConfigDataSource() {
HikariDataSource datasource = new HikariDataSource();
datasource.setJdbcUrl(configDbUrl);
datasource.setUsername(configDbUserName);
datasource.setPassword(configDbPassword);
datasource.setDriverClassName(configDbDriver);
datasource.setMaximumPoolSize(maximumPoolSize);
return datasource;
}
//默认主源
@Primary
@Bean(name = "dynamicRoutingDataSource")
public DynamicRoutingDataSource initDynamicRoutingDataSource(@Qualifier(value = "configDataSource") DataSource configDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(masterDataSourceKey, configDataSource);
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
dynamicRoutingDataSource.setTargetDataSourcesMap(targetDataSources);//记一份
dynamicRoutingDataSource.setTargetDataSources(targetDataSources);
dynamicRoutingDataSource.setDefaultTargetDataSource(configDataSource);//配置通用,默认数据源。
DynamicRoutingDataSourceContext.setDataSourceKey(masterDataSourceKey);
dynamicRoutingDataSource.afterPropertiesSet();
return dynamicRoutingDataSource;
}
}
重写数据源路由方法
package xxx.xxx.xxx;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;
/**
* 数据源路由。
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
/**
* 在这记一份targetDataSources,AbstractRoutingDataSource没提供public set,动态建新的datasource需要修改这个属性。
*/
private Map<Object, Object> targetDataSourcesMap;
/**
* 数据源选择,给resolvedDataSources选key。
*/
@Override
protected Object determineCurrentLookupKey() {
Object lookupKey = DynamicRoutingDataSourceContext.getDataSourceKey();
System.out.println(Thread.currentThread().getName() + " determineCurrentLookupKey : " + lookupKey);
return lookupKey;
}
public Map<Object, Object> getTargetDataSourcesMap() {
return targetDataSourcesMap;
}
public void setTargetDataSourcesMap(Map<Object, Object> targetDataSourcesMap) {
this.targetDataSourcesMap = targetDataSourcesMap;
}
}
请求线程数据源标志上下文
package xxx.xxx.xxx;
import java.util.Objects;
/**
* 数据源路由上下文。
*/
public class DynamicRoutingDataSourceContext {
private static final ThreadLocal<Object/*dataSourceKey*/> dataSourceKeys = new ThreadLocal<>();
public static void setDataSourceKey(Object dataSourceKey) {
System.out.println(Thread.currentThread().getName() + " set RoutingDataSource : " + dataSourceKey);
dataSourceKeys.set(dataSourceKey);
}
public static Object getDataSourceKey() {
Object key = dataSourceKeys.get();
if (Objects.isNull(key)) {
dataSourceKeys.set("master");
}
System.out.println(Thread.currentThread().getName() + " get RoutingDataSource : " + key);
return key;
}
public static void removeDataSourceKey() {
dataSourceKeys.remove();
System.out.println(Thread.currentThread().getName() + " remove RoutingDataSource");
}
}
其他数据源,要热加载,把加载方法写这里。读库拉datasource。
package xxx.xxx.xxx;
import xxx.xxx.master.DatabaseConfigInfo;
import xxx.xxx.xxx.repository.DatabaseConfigInfoRepository;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
@Service
public class DataSourceService {
@Autowired
DatabaseConfigInfoRepository databaseConfigInfoRepository;
@Autowired
DynamicRoutingDataSource dynamicRoutingDataSource;
@Value("${datasource-url-master.url}")
String url;
@Value("${datasource-url-master.placeholder.host}")
String hostPlaceholder;
@Value("${datasource-url-master.placeholder.port}")
String portPlaceholder;
@Value("${datasource-url-master.placeholder.database-name}")
String databaseNamePlaceholder;
@PostConstruct
public void serviceInit() {
initDataSources();
}
/**
* 初始化所有datasource
*/
private void initDataSources() {
List<DatabaseConfigInfo> configInfoList = databaseConfigInfoRepository.findAll();
if (configInfoList.isEmpty()) {
return;
}
configInfoList.forEach(info -> {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(processUrl(info.getHost(), info.getPort(), info.getDatabaseName()));
dataSource.setUsername(info.getUsername());
dataSource.setPassword(info.getPassword());
dataSource.setDriverClassName(info.getDriver());
dataSource.setMaximumPoolSize(info.getMaximumPoolSize());
dynamicRoutingDataSource.getTargetDataSourcesMap().put(info.getBusinessKey(), dataSource);
});
dynamicRoutingDataSource.afterPropertiesSet();
}
private String processUrl(String host, String port, String databaseName) {
String resultUrl = url;
return resultUrl.replace(hostPlaceholder, host)
.replace(portPlaceholder, port)
.replace(databaseNamePlaceholder, databaseName);
}
}
自定义标签
AOP里是SA TOKEN权限框架 session里记的数据key
package xxx.xxx.xxx.xxx.common.datasource;
import java.lang.annotation.*;
/**
* 数据动态源读参路由,不需要内置属性。
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceRouting {
}
package xx.xx.xxx.xxx.common.datasource;
import cn.dev33.satoken.stp.StpUtil;
import cn.meta.malab.whiteverse.domain.UserSession;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import static cn.meta.malab.whiteverse.common.GlobalConstants.UserConstants.USER_SESSION;
@Order(0)
@Aspect
@Component
public class DataSourceRoutingAspect {
@Around("@annotation(dataSourceRouting)")
public Object switchDataSource(ProceedingJoinPoint joinPoint, DataSourceRouting dataSourceRouting) {
try {
if (StpUtil.isLogin()) {
UserSession userSession = (UserSession) StpUtil.getSession().get(USER_SESSION);
DynamicRoutingDataSourceContext.setDataSourceKey(userSession.getDataSourceKey());
}
return joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
} finally {
DynamicRoutingDataSourceContext.removeDataSourceKey();
}
}
}
首次登录的话,要带datasource key过来。
在登录逻辑里加
DynamicRoutingDataSourceContext.setDataSourceKey("数据源key")
登录成功,在session里记源key。后续请求@DataSourceRouting 直接找session里的key。
StpUtil.login("用户id"); StpUtil.getSession().set("sourceKey","123");
线程安全的,AbstractRoutingDataSource这个类跟进去就明白了。