1. 程式人生 > >springboot+mybatis多資料來源配置實現

springboot+mybatis多資料來源配置實現

簡單實現了根據註解動態切換資料來源,支援同一個資料庫的宣告式事務,但不支援JTA事務。處理流程:

  • 根據配置的資料來源資訊,建立動態資料來源bean
  • 利用DataSourceAspect處理@DataSource註解,設定當前要使用的具體資料來源

pom.xml(注意這裡的spring-aop要使用spring5的,spring4不支援在mybatis的mapper介面添加註解)

<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.13.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aop</artifactId>
			<version>5.0.9.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.11</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.8</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
	</dependencies>

動態資料來源配置類

import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.alibaba.druid.pool.DruidDataSource;
import com.zl.datasource.config.properties.DynamicDataSourceProperties;
import com.zl.datasource.config.properties.DynamicDataSourceProperties.DataSouceProperties;
import com.zl.datasource.config.properties.GlobalProperties;
import com.zl.datasource.config.stat.DruidFilterConfiguration;
import com.zl.datasource.config.stat.DruidSpringAopConfiguration;
import com.zl.datasource.config.stat.DruidStatViewServletConfiguration;
import com.zl.datasource.config.stat.DruidWebStatFilterConfiguration;

/**
 * Druid動態資料來源配置類
 * 
 * @author Zhouych
 * @Date: 2018年6月21日 下午5:27:28
 * @since JDK 1.8
 */
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@Import({ DruidSpringAopConfiguration.class, DruidStatViewServletConfiguration.class,
		DruidWebStatFilterConfiguration.class, DruidFilterConfiguration.class })
@Configuration
@EnableTransactionManagement
public class DruidDynamicDataSourceConfig {

	private static String defaultDatasourceName;

	/**
	 * 裝載動態資料來源bean
	 * 
	 * @param globalProperties
	 * @return
	 */
	@Bean
	@Primary
	public DataSource dataSource(GlobalProperties globalProperties) {
		// 獲取資料來源配置
		DynamicDataSourceProperties.Multi multi = globalProperties.getDataSource().getMulti();
		List<DynamicDataSourceProperties.DataSouceProperties> allDataSources = multi.getDataSources();
		DataSource defaultDataSource = null;
		Map<String, DataSource> dataSourceMap = new HashMap<>(2);
		for (int i = 0; i < allDataSources.size(); i++) {
			DataSouceProperties innerDataSouceProperties = allDataSources.get(i);
			if (null != innerDataSouceProperties) {
				// 根據配置資訊建立資料來源
				DruidDataSource dataSource = createDruidDataSource(innerDataSouceProperties);
				String name = DataSourceEnum.fromValue(i + 1).name().toLowerCase();
				dataSource.setName(name);
				dataSourceMap.put(name, dataSource);
				// 篩選出預設的資料來源:這裡如果沒有配置預設資料來源,則把第一個資料來源作為預設。見com.zl.datasource.config.properties.DynamicDataSourceProperties.DataSouceProperties.isDefualt
				if (defaultDataSource == null || innerDataSouceProperties.isDefualt()) {
					defaultDataSource = dataSource;
					defaultDatasourceName = name;
				}
			}
		}
		if (dataSourceMap.size() < 1) {
			throw new IllegalArgumentException("至少需要一個可用的資料來源配置");
		}
		// 生成動態資料來源
		DynamicDataSource dynamicDataSource = new DynamicDataSource(defaultDataSource, dataSourceMap);
		return dynamicDataSource;
	}

	/**
	 * 生成druid資料來源
	 *
	 * @param dataSourceProperties
	 *            資料來源配置
	 * @return 資料來源
	 */
	private DruidDataSource createDruidDataSource(DataSouceProperties dataSourceProperties) {
		DruidDataSource datasource = new DruidDataSource();
		// druid配置
		datasource.setDriverClassName(dataSourceProperties.getDriverClassName());
		datasource.setUsername(dataSourceProperties.getUsername());
		datasource.setPassword(dataSourceProperties.getPassword());
		datasource.setUrl(dataSourceProperties.getUrl());
		datasource.setInitialSize(dataSourceProperties.getInitialSize());
		datasource.setMinIdle(dataSourceProperties.getMinIdle());
		datasource.setMaxActive(dataSourceProperties.getMaxActive());
		datasource.setMaxWait(dataSourceProperties.getMaxWait());
		datasource.setTimeBetweenEvictionRunsMillis(dataSourceProperties.getTimeBetweenEvictionRunsMillis());
		datasource.setMinEvictableIdleTimeMillis(dataSourceProperties.getMinEvictableIdleTimeMillis());
		datasource.setValidationQuery(dataSourceProperties.getValidationQuery());
		datasource.setValidationQueryTimeout(dataSourceProperties.getValidationQueryTimeout());
		datasource.setTestWhileIdle(dataSourceProperties.isTestWhileIdle());
		datasource.setTestOnBorrow(dataSourceProperties.isTestOnBorrow());
		datasource.setTestOnReturn(dataSourceProperties.isTestOnReturn());
		datasource.setPoolPreparedStatements(dataSourceProperties.isPoolPreparedStatements());
		datasource.setMaxPoolPreparedStatementPerConnectionSize(
				dataSourceProperties.getMaxPoolPreparedStatementPerConnectionSize());
		datasource.setConnectionProperties(dataSourceProperties.getConnectionProperties());
		datasource.setUseGlobalDataSourceStat(dataSourceProperties.isUseGlobalDataSourceStat());
		try {
			datasource.setFilters(dataSourceProperties.getFilters());
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return datasource;
	}

	public static String getDefaultDatasourceName() {
		return defaultDatasourceName;
	}

	/**
	 * 事務管理器
	 * 
	 * @param dataSource
	 * @return
	 */
	@Bean
	@Primary
	@Order(2)
	public DataSourceTransactionManager transactionManager(DataSource dataSource) {
		DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
		transactionManager.setDataSource(dataSource);
		return transactionManager;
	}

}

動態資料來源實現類,繼承spring的AbstractRoutingDataSource

import java.util.HashMap;
import java.util.Map;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 動態資料來源
 * 
 * @author Zhouych
 * @Date: 2018年6月21日 下午5:25:34
 * @since JDK 1.8
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

	private static final ThreadLocal<DynamicDataSourceKey> contextHolder = new ThreadLocal<>();

	/**
	 * 構建動態資料來源物件,設定預設資料來源和配置的多個數據源
	 * 
	 * @param defaultTargetDataSource
	 * @param targetDataSources
	 */
	public DynamicDataSource(javax.sql.DataSource defaultTargetDataSource,
			Map<String, javax.sql.DataSource> targetDataSources) {
		super.setDefaultTargetDataSource(defaultTargetDataSource);
		super.setTargetDataSources(new HashMap<>(targetDataSources));
		super.afterPropertiesSet();
	}

	/**
	 * 根據當前執行緒設定的最近的key,來獲取相應的資料來源
	 */
	@Override
	protected Object determineCurrentLookupKey() {
		return getDataSource();
	}

	/**
	 * 設定當前執行緒的資料來源key
	 * 
	 * @param dataSource
	 */
	public static void setDataSource(String dataSource) {
		DynamicDataSourceKey latestKey = getLatestKey();
		if (null == latestKey) {
			latestKey = DynamicDataSourceKey.builder().key(dataSource).build();
			contextHolder.set(latestKey);
		} else {
			latestKey.setChild(DynamicDataSourceKey.builder().key(dataSource).build());
		}
	}

	/**
	 * 獲取最近的資料來源key
	 * 
	 * @return
	 */
	public static String getDataSource() {
		DynamicDataSourceKey latestKey = getLatestKey();
		if (null == latestKey) {
			return null;
		} else {
			return latestKey.getKey();
		}
	}

	/**
	 * 獲取最近的資料來源key物件
	 * 
	 * @return
	 */
	private static DynamicDataSourceKey getLatestKey() {
		DynamicDataSourceKey dynamicDataSourceKey = contextHolder.get();
		if (null == dynamicDataSourceKey) {
			return null;
		}
		while (null != dynamicDataSourceKey.getChild()) {
			dynamicDataSourceKey = dynamicDataSourceKey.getChild();
		}
		return dynamicDataSourceKey;
	}

	/**
	 * 判斷是否巢狀多層資料來源設定
	 * 
	 * @return
	 */
	private static boolean hasChild() {
		DynamicDataSourceKey dynamicDataSourceKey = contextHolder.get();
		return (null != dynamicDataSourceKey) && (null != dynamicDataSourceKey.getChild());
	}

	/**
	 * 把最近的資料來源設定清楚
	 */
	private static void setLatestKeyNull() {
		DynamicDataSourceKey dynamicDataSourceKey = contextHolder.get();
		DynamicDataSourceKey tmp = null;
		while (true) {
			tmp = dynamicDataSourceKey.getChild();
			if (null == tmp.getChild()) {
				dynamicDataSourceKey.setChild(null);
				break;
			}
			dynamicDataSourceKey = dynamicDataSourceKey.getChild();
		}

	}

	/**
	 * 清楚資料來源設定
	 */
	public static void clearDataSource() {
		if (hasChild()) {
			setLatestKeyNull();
		} else {
			contextHolder.remove();
		}
	}

}

繫結到ThreadLocal的資料來源key樹(為了實現@DataSource的巢狀)

/**
 * 動態資料來源當前執行緒key資訊模型
 * 
 * @author Zhouych
 * @date 2018年9月21日 下午2:49:09
 * @since JDK 1.8
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DynamicDataSourceKey {

	private String key;

	/**
	 * 當同一個執行緒出現多次{@link @DataSource}註解時,key層層遞進
	 */
	private DynamicDataSourceKey child;

}

標誌使用的資料來源註解

import com.zl.datasource.config.DataSourceEnum;

/**
 * 多資料來源註解
 * 
 * @author Zhouych
 * @Date: 2018年6月21日 下午5:21:44
 * @since JDK 1.8
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

	DataSourceEnum value() default DataSourceEnum.ONE;

}

@DataSource註解處理切面


import java.lang.reflect.Method;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import com.zl.datasource.config.DynamicDataSource;
import com.zl.datasource.config.annotation.DataSource;
import com.zl.datasource.util.ReflectUtil;

import lombok.extern.slf4j.Slf4j;

/**
 * 資料來源切面
 * 
 * @author Zhouych
 * @Date: 2018年6月21日 下午5:16:49
 * @since JDK 1.8
 */
@Aspect
@Slf4j
@Component
public class DataSourceAspect implements Ordered {

	/**
	 * 切面定義
	 * <ul>
	 * <li>@within 切類級別的@DataSouce註解</li>
	 * <li>@annotation 切方法級別的@DataSouce註解</li>
	 * </ul>
	 */
	@Pointcut("(@within(com.zl.datasource.config.annotation.DataSource)) || (@annotation(com.zl.datasource.config.annotation.DataSource))")
	public void dataSourcePointCut() {

	}

	/**
	 * 解析{@link DataSource }註解,並設定當前執行緒使用的資料來源,最後清楚設定的資料來源。 首先從方法上{@link DataSource
	 * }註解,如果沒有,再從類上獲取
	 * 
	 * @param point
	 * @return
	 * @throws Throwable
	 */
	@Around("dataSourcePointCut()")
	public Object around(ProceedingJoinPoint point) throws Throwable {
		MethodSignature signature = (MethodSignature) point.getSignature();
		Method method = signature.getMethod();
		// 先獲取方法上的@DataSource註解
		// 基於cglib代理實現的代理類,在這裡拿到@DataSource註解的資訊
		DataSource ds = method.getAnnotation(DataSource.class);
		if (null == ds) {
			// 拿到基於JDK代理實現的代理類的@DataSource註解的資訊
			Object target = point.getTarget();
			Class<?>[] parameterTypes = signature.getParameterTypes();
			ds = ReflectUtil.getMethodAnnotation(target, method.getName(), DataSource.class, parameterTypes);
			// 獲取類級別的@DataSource註解
			if (null == ds) {
				ds = target.getClass().getAnnotation(DataSource.class);
			}
			if (null == ds) {
				ds = method.getDeclaringClass().getAnnotation(DataSource.class);
			}
		}
		// 如果能拿到@DataSource資訊,則設定當前執行緒的dataSource的key為@DataSource的value
		if (null != ds) {
			DynamicDataSource.setDataSource(ds.value().name().toLowerCase());
			log.debug("set datasource to be " + ds.value().name().toLowerCase());
		}
		try {
			// 執行業務
			return point.proceed();
		} finally {
			// 清理最近的dataSource的key
			DynamicDataSource.clearDataSource();
			log.debug("clean latest datasource");
		}
	}

	@Override
	public int getOrder() {
		return 1;
	}

}

碼雲地址