Spring原生支持多数据源动态切换,继承AbstractRoutingDataSource类重写determineCurrentLookupKey方法即可,为了方便切换可通过切面拦截自定义注解实现,代码如下:
1、SpringBoot使用Druid配置多数据源
spring:
#出现错误时, 直接抛出异常(便于异常统一处理,否则捕获不到404)
mvc:
throw-exception-if-no-handler-found: true
view:
prefix: classpath:/static/
suffix: .html
static-path-pattern: /**
resources:
static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/templates/
thymeleaf:
prefix: classpath:/templates/
suffix: .html
encoding: utf-8
cache: false
mode: LEGACYHTML5
enabled: true
http:
encoding:
force: true
charset: UTF-8
force-request: true
enabled: true
datasource:
#配置DruidDatasouce连接池
type: com.alibaba.druid.pool.DruidDataSource
druid:
#连接数据库
driver-class-name: oracle.jdbc.driver.OracleDriver
#druid配置详情信息
max-active: 100 #最大连接数
initial-size: 1 #初始化连接数
max-wait: 60000 #获取最大等待时间
min-idle: 1 #最小连接数
validation-query: select * from dual
time-between-eviction-runs-millis: 60000 #一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000 #间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
pool-prepared-statements: false #是否缓存preparedStatement 在mysql的环境下建议关闭 因为对数据库性能消耗大
max-open-prepared-statements: 50
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall #配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,'wall'用于防火墙
stat-view-servlet:
url-pattern: /druid/*
druidDataSource1:
url: jdbc:oracle:thin:@localhost:1521:orcl
username: rim
password: rim
druidDataSource2:
url: jdbc:oracle:thin:@localhost:1521:orcl
username: rim
password: rim
druidDataSource3:
url: jdbc:oracle:thin:@localhost:1521:orcl
username: rim
password: rim
2、用于声明数据源的自定义注解TargetDataSource
package com.bsoft.core.datasource;
import com.bsoft.commons.constant.DataSourceKey;
import java.lang.annotation.*;
/**
* @author :Liujian
* @date :2019/11/15 16:48
* @description:自定义DataSource注解
* @version:
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
DataSourceKey value() default DataSourceKey.FIRST;
}
3、定义继承AbstractRoutingDataSource类重写determineCurrentLookupKey方法用于实现数据源动态切换
package com.bsoft.core.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author :Liujian
* @date :2019/11/15 16:48
* @description:动态切换数据源
* @version:
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
/**
* 获取与数据源相关的key
* 此key是Map resolvedDataSources 中与数据源绑定的key值
* 在通过determineTargetDataSource获取目标数据源时使用
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDataSourceRouterKey();
}
}
4、定义数据源的连接字符串线程绑定DynamicDataSourceHolder
package com.bsoft.core.datasource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* @author :Liujian
* @date :2019/11/15 16:48
* @description:动态切换数据源
* @version:
*/
public class DynamicDataSourceHolder {
private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceHolder.class);
/**
* 存储已经注册的数据源的key
*/
public static final List<Object> dataSourceKeys = new ArrayList<>();
/**
* 线程级别的私有变量
*/
private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
public static String getDataSourceRouterKey () {
return HOLDER.get();
}
public static void setDataSourceRouterKey (String dataSourceRouterKey) {
logger.info("切换至{}数据源", dataSourceRouterKey);
HOLDER.set(dataSourceRouterKey);
}
/**
* 设置数据源之前一定要先移除
*/
public static void removeDataSourceRouterKey () {
HOLDER.remove();
}
/**
* 判断指定DataSrouce当前是否存在
*
* @param dataSourceId
* @return
*/
public static boolean containsDataSource(String dataSourceId){
return dataSourceKeys.contains(dataSourceId);
}
}
5、SpringBoot的数据源配置类DataSourceConfig
package com.bsoft.core.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.bsoft.commons.constant.DataSourceKey;
import com.bsoft.core.datasource.DynamicDataSourceHolder;
import com.bsoft.core.datasource.DynamicRoutingDataSource;
import com.bsoft.core.datasource.DynamicRoutingDataSourceTransactionFactory;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author :Liujian
* @date :2019/11/15 20:52
* @description:
* @version:
*/
@Configuration
public class DataSourceConfig {
private static Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);
@Bean(name = "first")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.druidDataSource1")
public DataSource first() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "second")
@ConfigurationProperties(prefix = "spring.datasource.druid.druidDataSource2")
public DataSource second() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "third")
@ConfigurationProperties(prefix = "spring.datasource.druid.druidDataSource3")
public DataSource third() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicRoutingDataSource")
public DataSource dynamicRoutingDataSource(){
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<Object, Object>(3);
dataSourceMap.put(DataSourceKey.FIRST.getValue(), first());
dataSourceMap.put(DataSourceKey.SECOND.getValue(), second());
dataSourceMap.put(DataSourceKey.THIRD.getValue(), third());
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
DynamicDataSourceHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
return dynamicRoutingDataSource;
}
/**
* 事务
* @return
*/
@Bean("transactionManager")
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dynamicRoutingDataSource());
}
@Bean(name="sqlSessionFactory")
public SqlSessionFactory sqlSessionFactorys() throws Exception {
logger.info("-------------------- sqlSessionFactory init ---------------------");
try {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dynamicRoutingDataSource());
sessionFactoryBean.setTransactionFactory(new DynamicRoutingDataSourceTransactionFactory());
// 读取配置
sessionFactoryBean.setTypeAliasesPackage("com.bsoft.*.entity");
//设置mapper.xml文件所在位置
Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml");
sessionFactoryBean.setMapperLocations(resources);
return sessionFactoryBean.getObject();
} catch (IOException e) {
logger.error("mybatis resolver mapper*xml is error", e);
return null;
} catch (Exception e) {
logger.error("mybatis sqlSessionFactoryBean create error", e);
return null;
}
}
}
6、定义拦截数据源注解切换的切面类DynamicDataSourceAspect
package com.bsoft.core.aspect;
import com.bsoft.commons.constant.DataSourceKey;
import com.bsoft.core.datasource.DynamicDataSourceHolder;
import com.bsoft.core.datasource.TargetDataSource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @author :Liujian
* @date :2019/11/20 12:10
* @description:
* @version:
*/
@Aspect
@Component
@Order(0)
public class DynamicDataSourceAspect {
private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
@Pointcut("execution(* com.bsoft..service.impl.*.*(..))")
public void dataSourcePointcut(){
}
@Before("dataSourcePointcut()")
public void before(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
String method = joinPoint.getSignature().getName();
Class<?>[] clazz = target.getClass().getInterfaces();
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
try {
Method m = clazz[0].getMethod(method, parameterTypes);
if (clazz[0].isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource source = clazz[0].getAnnotation(TargetDataSource.class);
DynamicDataSourceHolder.removeDataSourceRouterKey();
DynamicDataSourceHolder.setDataSourceRouterKey(source.value().getValue());
} else if (m != null && m.isAnnotationPresent(TargetDataSource.class)) { //如果方法上存在切换数据源的注解,则根据注解内容进行数据源切换
TargetDataSource source = clazz[0].getAnnotation(TargetDataSource.class);
DynamicDataSourceHolder.removeDataSourceRouterKey();
DynamicDataSourceHolder.setDataSourceRouterKey(source.value().getValue());
logger.info(String.format("class[%s],method[%s],使用数据源[%s]", clazz[0].getName(), m.getName(),
DynamicDataSourceHolder.getDataSourceRouterKey()));
} else {
DynamicDataSourceHolder.removeDataSourceRouterKey();
DynamicDataSourceHolder.setDataSourceRouterKey(DataSourceKey.FIRST.getValue());
logger.debug("switch datasource fail,use default");
}
} catch (Exception e) {
logger.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e);
}
}
@After("dataSourcePointcut()")
public void after(JoinPoint joinPoint){
DynamicDataSourceHolder.removeDataSourceRouterKey();
}
}
7、代码测试
在类或方法上加自定义注解实现拦截并动态切换
@TargetDataSource(DataSourceKey.THIRD)
以上,已完成动态切换,但是开启声明式事务后切换会存在问题,请参考我另一篇博文https://blog.csdn.net/kugeliujian/article/details/103318505