Spring Boot 2 AOP方式實現多數據源切換

支持的數據源類型爲druid和spring boot 2的默認數據源hikariCP ,若是須要支持其餘類型數據源可自行添加。java

實現思路爲經過解析數據源配置文件建立數據源,而後使用Spring提供的AbstractRoutingDataSource類設置路由數據源,最後經過AOP方式實現數據源的切換。mysql

1. 解析數據源配置文件

    建立配置文件解析類,我使用的數據源配置文件格式爲Yaml因此建立了默認的解析類爲讀取Yaml配置文件git

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。spring

    如下是 Durid v1.1.10數據源配置項示例sql

# 數據源配置 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類添加數據源類型選擇和多數據源開關dom

@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成爲主Beanide

@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. 設置數據源切面

    數據源切面設置在服務層能夠經過方法名判斷須要切換爲哪一個數據源spring-boot

@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

相關文章
相關標籤/搜索