springboot+mybatis多資料來源配置實現
阿新 • • 發佈:2018-12-11
簡單實現了根據註解動態切換資料來源,支援同一個資料庫的宣告式事務,但不支援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;
}
}