在网上搜集了一波资料发现,关于spring boot动态数据源的文章少之甚少。大部分都是已知数据源,增一套配置,写一个bean。
经过笔者日以彻夜的研究,终于得出了方案。
1、遍历配置,获得数据源配置
2、创建数据源连接池
3、使用AbstractRoutingDataSource接管数据源
===============================================
yml配置:
spring:
datasource:
master-source:
jdbcUrl: jdbc:mysql://xxx:3306/db?autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8
username: user
password: password
minimumIdle: 16
maximumPoolSize: 1024
connectionTestQuery: SELECT 1 FROM DUAL
driverClassName: com.mysql.jdbc.Driver
dataSource:
cachePrepStmts: true
prepStmtCacheSize: 1024
prepStmtCacheSqlLimit: 4096
slave-a-source:
jdbcUrl: jdbc:mysql://xxx:3306/db?autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8
username: user
password: password
minimumIdle: 16
maximumPoolSize: 1024
connectionTestQuery: SELECT 1 FROM DUAL
driverClassName: com.mysql.jdbc.Driver
dataSource:
cachePrepStmts: true
prepStmtCacheSize: 1024
prepStmtCacheSqlLimit: 4096
Bean配置:
package com.jiujun.voice.common.jdbc.source;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.ConfigurablePropertyResolver;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySources;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.util.ConcurrentReferenceHashMap;
import com.jiujun.voice.common.jdbc.source.DynamicDataSource;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@Configuration
public class DataSourceConfig {
private static final String PREFIX = "spring.datasource";
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dynamicDataSources(ConfigurablePropertyResolver environment)
throws SQLException, InterruptedException {
// 抽取配置容器
PropertySources propertySources = getFieldValue(environment, "propertySources");
List<PropertySource<?>> list = getFieldValue(propertySources, "propertySourceList");
PropertySource<?> propertySource = getByCollections(list, "name", "configurationProperties");
ConcurrentReferenceHashMap<PropertySource<?>, ConfigurationPropertySource> propertySourceMap = getFieldValue(
propertySource.getSource(), "cache");
// 筛选yml配置
List<ConfigurationPropertySource> ymlPropertys = new ArrayList<ConfigurationPropertySource>();
for (PropertySource<?> key : propertySourceMap.keySet()) {
if (isNullOrEmpty(propertySourceMap.get(key))) {
continue;
}
ConfigurationPropertySource value = propertySourceMap.get(key);
if (key.getName().startsWith("applicationConfig:")) {
ymlPropertys.add(value);
}
}
Map<String, Object> propertyMap = new HashMap<String, Object>();
// 合并配置
for (ConfigurationPropertySource source : ymlPropertys) {
MapPropertySource underlyingSource = (MapPropertySource) source.getUnderlyingSource();
for (String propertyName : underlyingSource.getPropertyNames()) {
propertyMap.put(propertyName, underlyingSource.getProperty(propertyName));
}
}
// 整合数据源配置
Map<String, Properties> dataSourceConfigs = new HashMap<String, Properties>();
for (String propertyName : propertyMap.keySet()) {
if (!propertyName.startsWith(PREFIX)) {
continue;
}
String dataSourceName = getDatasourceNameByPropertyName(propertyName);
String fieldName = getFieldNameByPropertyName(propertyName);
Object fieldValue = propertyMap.get(propertyName);
if (!dataSourceConfigs.containsKey(dataSourceName)) {
Properties properties = new Properties();
dataSourceConfigs.put(dataSourceName, properties);
}
dataSourceConfigs.get(dataSourceName).put(fieldName, fieldValue);
}
// 创建数据源
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
for (String dataSourceName : dataSourceConfigs.keySet()) {
Properties properties = dataSourceConfigs.get(dataSourceName);
HikariConfig configuration = new HikariConfig(properties);
HikariDataSource dataSource = new HikariDataSource(configuration);
targetDataSources.put(dataSourceName, dataSource);
}
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.afterPropertiesSet();
return dynamicDataSource;
}
@Bean(name = "jdbcTemplate")
public JdbcTemplate instanceJdbcTemplate(DynamicDataSource dynamicDataSource) {
return new JdbcTemplate(dynamicDataSource);
}
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
return new DataSourceTransactionManager(dataSource);
}
private static String getFieldNameByPropertyName(String propertyName) {
propertyName = propertyName.substring(PREFIX.length() + 1);
String datasourceName = propertyName.substring(0, propertyName.indexOf("."));
String fieldName = propertyName.substring(datasourceName.length() + 1);
return fieldName;
}
private static String getDatasourceNameByPropertyName(String propertyName) {
propertyName = propertyName.substring(PREFIX.length() + 1);
String datasourceName = propertyName.substring(0, propertyName.indexOf("."));
return datasourceName;
}
@SuppressWarnings("unchecked")
private static <T> T getFieldValue(Object object, String fieldName) {
if (isNullOrEmpty(object)) {
return null;
}
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
if (!fieldName.equals(field.getName())) {
continue;
}
try {
field.setAccessible(true);
return (T) field.get(object);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
return null;
}
public static boolean isNullOrEmpty(Object obj) {
try {
if (obj == null) {
return true;
}
if (obj instanceof CharSequence) {
return ((CharSequence) obj).length() == 0;
}
if (obj instanceof Collection) {
return ((Collection<?>) obj).isEmpty();
}
if (obj instanceof Map) {
return ((Map<?, ?>) obj).isEmpty();
}
if (obj instanceof Object[]) {
Object[] object = (Object[]) obj;
if (object.length == 0) {
return true;
}
boolean empty = true;
for (int i = 0; i < object.length; i++) {
if (!isNullOrEmpty(object[i])) {
empty = false;
break;
}
}
return empty;
}
return false;
} catch (Exception e) {
return true;
}
}
@SuppressWarnings("unchecked")
private static <T> T getByCollections(Collection<?> collections, String fieldName, Object fieldValue) {
if (isNullOrEmpty(collections)) {
return null;
}
for (Object object : collections) {
Object value = getFieldValue(object, fieldName);
if (fieldValue == null && value == null) {
return (T) object;
}
if (fieldValue == value || fieldValue.equals(value)) {
return (T) object;
}
}
return null;
}
}
动态数据源对象:
package com.jiujun.voice.common.jdbc.source;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.apache.log4j.Logger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源
*
* @author Coody
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
static Logger logger=Logger.getLogger(DynamicDataSource.class);
private static final ThreadLocal<String> CURRENT_SOURCE = new ThreadLocal<String>();
public static final String MASTER_FLAG = "master";
public static final String SLAVE_FLAG = "slave";
private static List<String> masters = new ArrayList<String>();
private static List<String> slaves = new ArrayList<String>();
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
for (Object key : targetDataSources.keySet()) {
String dataSourceName = key.toString();
if (dataSourceName.startsWith(MASTER_FLAG)) {
masters.add(dataSourceName);
this.setDefaultTargetDataSource(targetDataSources.get(dataSourceName));
continue;
}
if (dataSourceName.startsWith(SLAVE_FLAG)) {
slaves.add(dataSourceName);
continue;
}
throw new RuntimeException("未知的数据源类型,数据源命名只能以" + MASTER_FLAG + "、" + SLAVE_FLAG + "开头");
}
if (masters==null||masters.isEmpty()) {
throw new RuntimeException("缺少主库数据源");
}
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
String dataSourceName = getDataSourceName();
logger.debug("使用数据源>>" + dataSourceName);
return dataSourceName;
}
public static void setDataSourceFlag(String sourceFlag) {
CURRENT_SOURCE.set(sourceFlag);
}
public static String getDataSourceName() {
String flag = CURRENT_SOURCE.get();
if (flag == null) {
flag = MASTER_FLAG;
}
if (flag.equals(MASTER_FLAG)) {
if (masters.size() == 1) {
return masters.get(0);
}
return masters.get(new Random().nextInt(masters.size()));
}
if (flag.equals(SLAVE_FLAG)) {
if (slaves.size() == 1) {
return slaves.get(0);
}
return slaves.get(new Random().nextInt(slaves.size()));
}
logger.error("多数据源警告:指定数据源标记错误,只能指定master、slave相关数值");
return masters.get(0);
}
public static void clearDataSourceFlag() {
CURRENT_SOURCE.remove();
}
}
数据源切面(默认使用从库,当执行碰到update则自动切换为主库):
package com.jiujun.voice.common.jdbc.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import com.jiujun.voice.common.jdbc.source.DynamicDataSource;
/**
* 多数据源控制
* @author Coody
* @date 2018年9月21日
*/
@Aspect
@Component
public class DataSourceAspect {
/**
* 为所有CMD指定从库
*
* @param pjp
* @return
* @throws Throwable
*/
@Around("@annotation(com.jiujun.voice.common.cmd.anntation.CmdAction)")
public Object buildCmdDataSource(ProceedingJoinPoint pjp) throws Throwable {
StopWatch sw = new StopWatch(getClass().getSimpleName());
try {
// AOP启动监听
sw.start(pjp.getSignature().toShortString());
//默认指定从库
DynamicDataSource.setDataSourceFlag(DynamicDataSource.SLAVE_FLAG);
return pjp.proceed();
} finally {
//释放线程数据源
DynamicDataSource.clearDataSourceFlag();
sw.stop();
}
}
}
sql语句更新处(mybatis和hibernate可采用拦截器对sql进行拦截,并设置主库):