简单实现了根据注解动态切换数据源,支持同一个数据库的声明式事务,但不支持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;
}
}
代码码云地址(说明一下,项目中有部分代码是直接拷贝druid springboot starter的源码:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter )