支持的数据源类型为druid和spring boot 2的默认数据源hikariCP ,如果需要支持其他类型数据源可自行添加。
实现思路为通过解析数据源配置文件创建数据源,然后使用Spring提供的AbstractRoutingDataSource类设置路由数据源,最后通过AOP方式实现数据源的切换。
1. 解析数据源配置文件
创建配置文件解析类,我使用的数据源配置文件格式为Yaml所以创建了默认的解析类为读取Yaml配置文件
public interface DataSourceConfigurationResolver {
static final String MASTER_MARK = "MASTER";
Map<String, Object> getDataSourceProperty() throws DataSourceConfigException;
}
public abstract class AbstractFileDataSourceConfigurationResolver implements DataSourceConfigurationResolver{
protected static final String FILE_PERFIX = "datasource-";
protected static final String FILE_MASTER_MARK = "master";
protected static final String FILE_SLAVE_MARK = "slave";
@Override
public Map<String, Object> getDataSourceProperty() throws DataSourceConfigException{
Map<String, Object> dataSourceMap = new HashMap<String, Object>();
dataSourceMap.put(MASTER_MARK, obatainMasterDataSourceProperty());
dataSourceMap.putAll(obatainSlaveDataSourceProperty());
return dataSourceMap;
}
protected abstract Object obatainMasterDataSourceProperty() throws DataSourceConfigException;
protected abstract Map<String, Object> obatainSlaveDataSourceProperty() throws DataSourceConfigException;
}
public abstract class AbstractYamlFileDataSourceConfigurationResolver extends AbstractFileDataSourceConfigurationResolver{
public static final String FILE_SUFFIX = ".yml";
public static final String DEFAULT_FILE_PATH = "/";
@Override
protected Object obatainMasterDataSourceProperty() throws DataSourceConfigException {
File file = new File(getMasterDataSourceFilePath());
return resolve(file);
}
@Override
protected Map<String, Object> obatainSlaveDataSourceProperty() throws DataSourceConfigException {
Map<String, Object> dataSourcePropertyMap = new HashMap<String, Object>();
File dir = new File(getDataSourceFileDir());
File[] files = dir.listFiles();
for(File file : files) {
if(!file.isDirectory() && validYamlDataSourceFile(file.getName())) {
dataSourcePropertyMap.put(getSlaveName(file.getName()), resolve(file));
}
}
return dataSourcePropertyMap;
}
protected abstract Object resolve(File file) throws DataSourceConfigException;
private String getMasterDataSourceFilePath() {
return getDataSourceFileDir() + File.separator + FILE_PERFIX + FILE_MASTER_MARK + FILE_SUFFIX;
}
private String getDataSourceFileDir() {
return this.getClass().getClassLoader().getResource("").getPath();
}
private boolean validYamlDataSourceFile(String fileName) {
return fileName.startsWith(FILE_PERFIX + FILE_SLAVE_MARK) && fileName.endsWith(FILE_SUFFIX);
}
private String getSlaveName(String fileName) {
return fileName.substring(FILE_PERFIX.length(), fileName.indexOf(FILE_SUFFIX));
}
}
@Component
public class DefaultDataSourceConfigurationResolver extends AbstractYamlFileDataSourceConfigurationResolver{
private Yaml yaml = new Yaml();
@Override
protected Properties resolve(File file) throws DataSourceConfigException {
Map<?, ?> dataSourceConfigurationMap = null;
Properties result = new Properties();
try {
Map<?, ?> configurationMap = (Map<?, ?>) yaml.load(new FileInputStream(file));
if(!configurationMap.containsKey("datasource"))
throw new DataSourceConfigException("解析数据源配置文件失败,未发现key[ datasource ],file[ " + file.getName() + " ]");
dataSourceConfigurationMap = (Map<?, ?>) configurationMap.get("datasource");
} catch (FileNotFoundException e) {
throw new DataSourceConfigException(e);
}
dataSourceConfigurationMap.forEach((k, v) ->{
result.put(k, v);
});
return result;
}
}
public class DataSourceConfigException extends Exception{
private static final long serialVersionUID = 6924248500337718207L;
public DataSourceConfigException(String msg, Throwable e) {
super(msg, e);
}
public DataSourceConfigException(Throwable e) {
super(e);
}
public DataSourceConfigException(String msg) {
super(msg);
}
}
配置文件默认存储路径为src/main/resources目录以datasource-开头 .yaml结尾,文件名后段标识主数据源标识和从数据源标识,如:datasource-master.yml、datasource-slave-1.yml、datasource-slave-2.yml。
以下是 Durid v1.1.10数据源配置项示例
# 数据源配置 Durid v1.1.10
datasource:
driverClassName: com.mysql.jdbc.Driver
# 数据库链接配置
url: jdbc:mysql://localhost:3306/sc?characterEncoding=utf8
username: root
password: root
# 初始化大小,最小,最大
initialSize: '5'
minIdle: '5'
maxctive: '50'
# 获取连接等待超时的时间
maxait: '60000'
# 连接在池中最小生存的时间
minEvictableIdleTimeMillis: '300000'
validationQuery: SELECT 1 FROM DUA
# 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效.建议配置为true,不影响性能,并且保证安全性
testWhileIdle: 'true'
# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: 'false'
# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testOnReturn: 'false'
# 打开PSCache,并且指定每个连接上PSCache的大小。PSCache对支持游标的数据库性能提升巨大,mysql下建议关闭
poolPreparedStatements: 'false'
# 通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
filters: stat,wall,slf4j
2. 设置数据源
数据源设置使用了Spring提供AbstractRoutingDataSource类来实现,该类是一个数据源路由类使用Map持有多个数据源在获取Connection时可以根据自定义策略选择数据源,由于AbstractRoutingDataSource是抽象类我们需要自定义类继承该类,并重写determineCurrentLookupKey方法制定数据源key的获取方式。
public class DynamicDataSource extends AbstractRoutingDataSource{
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
DataSourceContextHolder类中持有 slaveDataSources,从数据源选择使用了简单的随机选择方式
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
private static volatile List<String> slaveDataSources = new ArrayList<>();
private static Random random = new Random();
public static void setDataSource(String type) {
contextHolder.set(type);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void removeDataSource() {
contextHolder.remove();
}
public static void setSlaveDataSource(String dataSourceName) {
slaveDataSources.add(dataSourceName);
}
public static String getSlaveDataSource() {
return slaveDataSources.get(random.nextInt(slaveDataSources.size()));
}
}
定义Conf类添加数据源类型选择和多数据源开关
@Configuration
@PropertySource("classpath:conf.yml")
public class Conf {
@Value("${use-multi-datasource}")
public Boolean useMultiDatasource;
@Value("${datasource-type}")
public String datasourceType;
}
# 数据源类型 DRUID HIKARICP
datasource-type: DRUID
# 是否开启多数据源
use-multi-datasource: false
声明数据源Bean,注意@Primary标签使当前Bean成为主Bean
@Autowired
private DataSourceConfigurationResolver dataSourceConfigurationResolver;
@Autowired
private Conf conf;
@Bean
@Primary
public DataSource dataSource() throws Exception {
Map<String, Object> propMap = dataSourceConfigurationResolver.getDataSourceProperty();
Map<Object, Object> targetDataSources = new HashMap<>();
DataSource defalultDataSource = DataSourceFactory.getDataSource((Properties) propMap.get(DataSourceConfigurationResolver.MASTER_MARK), conf.datasourceType);
if(!conf.useMultiDatasource) {
return defalultDataSource;
}
for(Entry<String, Object> propEntry : propMap.entrySet()) {
String databaseType = propEntry.getKey();
if(!DataSourceConfigurationResolver.MASTER_MARK.equals(databaseType))
DataSourceContextHolder.setSlaveDataSource(databaseType);
targetDataSources.put(databaseType, DataSourceFactory.getDataSource((Properties) propEntry.getValue(), conf.datasourceType));
}
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(defalultDataSource);
return dataSource;
}
public class DataSourceFactory {
public static DataSource getDataSource(Properties properties, String type) throws Exception {
if("DRUID".equals(type)) {
return DruidDataSourceFactory.createDataSource(properties);
}else if("HIKARICP".equals(type)) {
return new HikariDataSource(new HikariConfig(properties));
}
return DruidDataSourceFactory.createDataSource(properties);
}
}
3. 设置数据源切面
数据源切面设置在服务层可以通过方法名判断需要切换为哪个数据源
@Aspect
@Component
@Order(1)
public class DataSourceAspect {
@Autowired
private Conf conf;
@Pointcut("execution(* priv.jieying.service.impl.*.*(..))")
public void declareJointPointExpression() {
}
@Before("declareJointPointExpression()")
public void setDataSourceKey(JoinPoint point) {
if(conf.useMultiDatasource) {
if(StringUtils.startsWithAny(point.getSignature().getName(), "insert", "save", "update", "modify", "delete", "remove")) {
DataSourceContextHolder.setDataSource(DataSourceConfigurationResolver.MASTER_MARK);
}else {
DataSourceContextHolder.setDataSource(DataSourceContextHolder.getSlaveDataSource());
}
}
}
}
以上示例代码已上传到码云 https://gitee.com/sc325/spring-boot-multi-datasource