基于spring boot多主从数据源-(完全动态)

原创
2019/03/08 09:59
阅读数 1.5K

在网上搜集了一波资料发现,关于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进行拦截,并设置主库):

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
11 收藏
0
分享
返回顶部
顶部