类似mybatis-plus提供的@DS注解,功能相似
创建注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//写在类上时 子类默认使用此注解
@Inherited
public @interface DataSource {
String value() default "";
}
定义切面
@Aspect
@Component
//最高执行优先级
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
protected Logger logger = LoggerFactory.getLogger(getClass());
//采用注解的方法 或者 采用注解的类及其下方法 都适用此切面
@Pointcut("@annotation(cn.learn.datasource.annotation.DataSource) " +
"|| @within(cn.learn.datasource.annotation.DataSource)")
public void dataSourcePointCut() {
}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
//获取方法的类信息
Class targetClass = point.getTarget().getClass();
//获取方法信息
Method method = signature.getMethod();
//获取方法的类上注解信息 如果有的话
DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class);
//获取方法上注解信息 如果有的话
DataSource methodDataSource = method.getAnnotation(DataSource.class);
if(targetDataSource != null || methodDataSource != null){
String value;
//优先采用方法上的注解信息
if(methodDataSource != null){
value = methodDataSource.value();
}else {
value = targetDataSource.value();
}
//修改数据源
DynamicContextHolder.push(value);
logger.debug("set datasource is {}", value);
}
try {
return point.proceed();
} finally {
DynamicContextHolder.poll();
logger.debug("clean datasource");
}
}
}
数据源上下文
记录不同线程自身的数据源信息以及提供操作数据源的方法
public class DynamicContextHolder {
//每个线程单独的局部变量 Deque是一种双向队列 这里是后进先出的栈 记录当前数据源名称
private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
};
/**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String peek() {
//返回栈顶的数据源名称
return CONTEXT_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
*
* @param dataSource 数据源名称
*/
public static void push(String dataSource) {
CONTEXT_HOLDER.get().push(dataSource);
}
/**
* 清空当前线程数据源
*/
public static void poll() {
//获取当前数据源名称
Deque<String> deque = CONTEXT_HOLDER.get();
//清除
deque.poll();
//栈为空清除变量 防止内存泄漏
if (deque.isEmpty()) {
CONTEXT_HOLDER.remove();
}
}
}
实现spring提供的动态数据源类
AbstractRoutingDataSource:是 Spring 框架提供的一个抽象类,它是为了解决数据源路由的问题,即动态切换数据源,通常用在多数据源的场景下。这个类允许你在运行时根据某些标准(如用户的身份、特定的算法或者其他标准)来决定使用哪一个数据源
这里重写了方法,每次与数据库交互时,都会经过这个方法,让他根据当前数据源名称切换至指定的数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicContextHolder.peek();
}
}
读取配置文件
接下来就是指定DynamicDataSource初始用什么数据源,并告诉它备选什么数据源(你后续要切换的数据源)
首先,确定配置类需要的信息,并写入DruidDataSource
DruidDataSource:阿里巴巴提供的数据库连接池实现,它是 Druid 数据库连接池的核心类。Druid 是一个开源的 JDBC 连接池,它提供了高性能和可扩展性,专门为企业级应用设计
数据源属性,get、set方法太长,我这里就缩略了
public class DataSourceProperties {
private String driverClassName;
private String url;
private String username;
private String password;
/**
* Druid默认参数
*/
private int initialSize = 2;
private int maxActive = 10;
private int minIdle = -1;
private long maxWait = 60 * 1000L;
private long timeBetweenEvictionRunsMillis = 60 * 1000L;
private long minEvictableIdleTimeMillis = 1000L * 60L * 30L;
private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7;
private String validationQuery = "select 1";
private int validationQueryTimeout = -1;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;
private boolean testWhileIdle = true;
private boolean poolPreparedStatements = false;
private int maxOpenPreparedStatements = -1;
private boolean sharePreparedStatements = false;
private String filters = "stat,wall";
public String getXxxClassName() {return xxx;}
public void setXxxClassName(String driverClassName) {this.xxx = xxx;}
/.../
}
简单工厂将配置信息写入DruidDataSource
public class DynamicDataSourceFactory {
public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getDriverClassName());
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword());
druidDataSource.setInitialSize(properties.getInitialSize());
druidDataSource.setMaxActive(properties.getMaxActive());
druidDataSource.setMinIdle(properties.getMinIdle());
druidDataSource.setMaxWait(properties.getMaxWait());
druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
druidDataSource.setValidationQuery(properties.getValidationQuery());
druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
druidDataSource.setTestOnReturn(properties.isTestOnReturn());
druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());
try {
druidDataSource.setFilters(properties.getFilters());
druidDataSource.init();
} catch (SQLException e) {
e.printStackTrace();
}
return druidDataSource;
}
}
然后我们还要读取多数据源的信息,他将会读取配置文件(application.yml)dynamic.datasource下的数组并填入自己的datasource属性
@ConfigurationProperties(prefix = "dynamic")
public class DynamicDataSourceProperties {
private Map<String, DataSourceProperties> datasource = new LinkedHashMap<>();
public Map<String, DataSourceProperties> getDatasource() {
return datasource;
}
public void setDatasource(Map<String, DataSourceProperties> datasource) {
this.datasource = datasource;
}
}
yml这样写(注意不要写到spring.datasource下面,dynamic是最顶的一级)
#多数据源的配置
dynamic:
datasource:
slave1:
driver-class-name: xxx.xxx.xxx
url: xxx
username: xxx
password: xxx
slave2:
driver-class-name: xxx.xxx.xxx
url: xxx
username: xxx
password: xxx
后续可以通过@DataSource("slave1")这样的数据源名称方式切换数据源
最后就是重要的配置类了
@Configuration
//读取刚刚多数据源配置信息 填入下面的私有属性properties
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
@Autowired
private DynamicDataSourceProperties properties;
//读取默认的数据源信息
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//设置多数据源信息
dynamicDataSource.setTargetDataSources(getDynamicDataSource());
//默认数据源
DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
}
private Map<Object, Object> getDynamicDataSource(){
//从DynamicDataSourceProperties拿过来的配置信息
Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
//写入map
dataSourcePropertiesMap.forEach((k, v) -> {
DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
targetDataSources.put(k, druidDataSource);
});
return targetDataSources;
}
}
yml默认数据源的配置(druid的其他属性也可以自行配置,这里就填了基本的参数)
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxxxxx:xxxx/xxxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: xxxx
password: xxxxx
注意事项(使用mybatis-plus的@DS也是同理)
1.使用事务时,数据源会使用最初的数据源,所以无法在事务里切换数据源
2.方法调用同一个类里的方法(比如同一个service里有A、B、C三个方法,A调用B、C方法,其中B方法上添加了切换数据源的注解),通常不会进过spring代理,意味着切换数据源失效,建议写两个mapper,mapper上用注解切换数据源
本文参考gitee开源项目tesco源码,个人理解,如有错误,欢迎指出