目录
2.4.DataSourceConfiguration 配置
1.背景
业务需要一个项目里面操作多个数据库
2.实现步骤
2.1.数据源配置
spring:
datasource:
druid:
tc:
driver-class-name: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@192.168.5.100:1521:orcl
username: master
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
user:
driver-class-name: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@192.168.5.200:1521:orcl
username: slave
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
2.2.DbContextHolder 配置
package com.qxnw.tc.common.config.dbConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import javax.sql.DataSource;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Copyright (C) XXXXXXXXXXX科技股份技有限公司
* @Author: lidongping
* @Date: 2021-07-21 18:27
* @Description:
*/
@Slf4j
public class DbContextHolder {
/**
* 项目中配置数据源
*/
private static Map<String, DataSource> dataSources = new ConcurrentHashMap<>();
/**
* 默认数据源
*/
private static String defaultDs = "dataSourceMaster";
private static final ThreadLocal<Deque<String>> contextHolder = new ThreadLocal() {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
};
/**
* 设置当前线程使用的数据源
*
* @param dsName
*/
public static void setCurrentDsStr(String dsName) {
if (StringUtils.isBlank(dsName)) {
log.error("=>dbType is null,throw NullPointerException");
throw new NullPointerException();
}
if (!dataSources.containsKey(dsName)) {
log.error("=>datasource not exists,dsName={}", dsName);
throw new RuntimeException("=>datasource not exists,dsName={" + dsName + "}");
}
contextHolder.get().push(dsName);
}
/**
* 获取当前使用的数据源
*
* @return
*/
public static String getCurrentDsStr() {
return contextHolder.get().peek();
}
/**
* 清空当前线程数据源
* <p>
* 如果当前线程是连续切换数据源
* 只会移除掉当前线程的数据源名称
* </p>
*/
public static void clearCurrentDsStr() {
Deque<String> deque = contextHolder.get();
deque.poll();
if (deque.isEmpty()) {
contextHolder.remove();
}
}
/**
* 添加数据源
*
* @param dsName
* @param dataSource
*/
public static void addDataSource(String dsName, DataSource dataSource) {
if (dataSources.containsKey(dsName)) {
log.error("==========>dataSource={} already exist", dsName);
throw new RuntimeException("dataSource={" + dsName + "} already exist");
}
dataSources.put(dsName, dataSource);
}
/**
* 获取指定数据源
*
* @return
*/
public static DataSource getDefaultDataSource() {
if (StringUtils.isBlank(defaultDs)) {
log.error("==========>default datasource must be configured");
throw new RuntimeException("default datasource must be configured.");
}
if (!dataSources.containsKey(defaultDs)) {
log.error("==========>The default datasource must be included in the datasources");
throw new RuntimeException("==========>The default datasource must be included in the datasources");
}
return dataSources.get(defaultDs);
}
/**
* 设置默认数据源
*
* @param defaultDsStr
*/
public static void setDefaultDs(String defaultDsStr) {
defaultDs = defaultDsStr;
}
/**
* 获取所有 数据源
*
* @return
*/
public static Map<String, DataSource> getDataSources() {
return dataSources;
}
/**
* @return
*/
public static String getDefaultDs() {
return defaultDs;
}
}
2.3.MyRoutingDataSource 配置
package com.qxnw.tc.common.config.dbConfig;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @Copyright (C) XXXXXXXXXXX科技股份技有限公司
* @Author: lidongping
* @Date: 2021-07-21 18:29
* @Description:
*/
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getCurrentDsStr();
}
}
2.4.DataSourceConfiguration 配置
package com.qxnw.tc.common.config.dbConfig;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @Copyright (C) XXXXXXXXXXX科技股份技有限公司
* @Author: lidongping
* @Date: 2021-07-21 18:30
* @Description:
*/
@Configuration
public class DataSourceConfiguration {
private String tcDataName="dataSourceMaster";
private String userDataName="dataSourceSlave";
/**
* 默认是数据源
*/
private String defaultDs=tcDataName;
@Bean(name = "dataSourceMaster")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.master")
public DataSource dataSourceMaster() {
DataSource druidDataSource = DruidDataSourceBuilder.create().build();
DbContextHolder.addDataSource(tcDataName, druidDataSource);
return druidDataSource;
}
@Bean(name = "dataSourceSlave")
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
public DataSource dataSourceSlave() {
DataSource druidDataSource = DruidDataSourceBuilder.create().build();
DbContextHolder.addDataSource(userDataName, druidDataSource);
return druidDataSource;
}
@Bean(name = "myRoutingDataSource")
public MyRoutingDataSource dataSource(@Qualifier("dataSourceMaster") DataSource dataSourceMaster,
@Qualifier("dataSourceSlave") DataSource dataSourceSlave) {
MyRoutingDataSource dynamicDataSource = new MyRoutingDataSource();
Map<Object, Object> targetDataResources = new HashMap<>(2);
targetDataResources.put(tcDataName, dataSourceMaster);
targetDataResources.put(userDataName, dataSourceSlave);
//设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(dataSourceMaster);
dynamicDataSource.setTargetDataSources(targetDataResources);
DbContextHolder.setDefaultDs(defaultDs);
return dynamicDataSource;
}
}
2.5.MybatisConfiguration 配置
package com.qxnw.tc.common.config.dbConfig;
import com.baomidou.mybatisplus.autoconfigure.SpringBootVFS;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
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 javax.sql.DataSource;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* @Copyright (C) XXXXXXXXXXX科技股份技有限公司
* @Author: lidongping
* @Date: 2021-07-21 18:36
* @Description:
*/
@Slf4j
@Configuration
@AutoConfigureAfter({DataSourceConfiguration.class})
@MapperScan({"com.XXX.tc.sys.mapper", "com.XXX.tc.sys.mapperUser"})
public class MybatisConfiguration {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 开启 PageHelper 的支持
return paginationInterceptor;
}
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier(value = "myRoutingDataSource") MyRoutingDataSource myRoutingDataSource) throws
Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
return sqlSessionFactoryBean.getObject();
}
@Bean(name = "mybatisSqlSessionFactoryBean")
@Primary
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier(value = "myRoutingDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
Set<Resource> result = new LinkedHashSet<>(16);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
result.addAll(Arrays.asList(resolver.getResources("classpath*:mapper/*.xml")));
result.addAll(Arrays.asList(resolver.getResources("classpath*:mapperUser/*.xml")));
bean.setMapperLocations(result.toArray(new org.springframework.core.io.Resource[0]));
bean.setDataSource(dataSource);
bean.setVfs(SpringBootVFS.class);
com.baomidou.mybatisplus.core.MybatisConfiguration configuration = new com.baomidou.mybatisplus.core.MybatisConfiguration();
// 日志明细输出
// configuration.setLogImpl(StdOutImpl.class);
configuration.setMapUnderscoreToCamelCase(true);
//添加 分页插件
configuration.addInterceptor(paginationInterceptor());
bean.setConfiguration(configuration);
bean.setGlobalConfig(globalConfig());
return bean;
}
@Bean
public GlobalConfig globalConfig() {
GlobalConfig conf = new GlobalConfig();
conf.setDbConfig(new GlobalConfig.DbConfig().setKeyGenerator(new OracleKeyGenerator()));
return conf;
}
}
2.6.TransactionConfig 配置
package com.qxnw.tc.common.config.dbConfig;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* @Copyright (C) XXXXXXXXXXX科技股份技有限公司
* @Author: lidongping
* @Date: 2021-07-21 18:02
* @Description:
*/
@Aspect
@Configuration
@Slf4j
public class TransactionConfig {
@Autowired
ConfigurableApplicationContext applicationContext;
private static final int TX_METHOD_TIMEOUT = 300;
private static final String AOP_POINTCUT_EXPRESSION = "execution(* com.XXX.sys.service.impl.*Service.*(..))";
@Bean(name = "txAdvice")
public TransactionInterceptor txAdvice() {
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
// 只读事务,不做更新操作
RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
readOnlyTx.setReadOnly(true);
readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 当前存在事务就使用当前事务,当前不存在事务就创建一个新的事务
RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
requiredTx.setTimeout(TX_METHOD_TIMEOUT);
Map<String, TransactionAttribute> txMap = new HashMap<>();
txMap.put("add*", requiredTx);
txMap.put("save*", requiredTx);
txMap.put("insert*", requiredTx);
txMap.put("create*", requiredTx);
txMap.put("update*", requiredTx);
txMap.put("batch*", requiredTx);
txMap.put("modify*", requiredTx);
txMap.put("delete*", requiredTx);
txMap.put("remove*", requiredTx);
txMap.put("exec*", requiredTx);
txMap.put("set*", requiredTx);
txMap.put("do*", requiredTx);
txMap.put("get*", readOnlyTx);
txMap.put("query*", readOnlyTx);
txMap.put("find*", readOnlyTx);
txMap.put("*", requiredTx);
source.setNameMap(txMap);
TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager(), source);
return txAdvice;
}
@Bean
public Advisor txAdviceAdvisor(@Qualifier("txAdvice") TransactionInterceptor txAdvice) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice);
}
/**
* 自定义 事务管理器 管理我们自定义的 MyRoutingDataSource 数据源
*
* @return
*/
@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(applicationContext.getBean(MyRoutingDataSource.class));
return transactionManager;
}
}
2.7.DSUser 注解
package com.qxnw.tc.common.config.dbConfig;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Copyright (C) XXXXXXXXXXX科技股份技有限公司
* @Author: lidongping
* @Date: 2021-07-21 18:44
* @Description: <p>
* 切换到 slave 数据源
* </p>
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DSUser {
String value() default "dataSourceSlave";
}
2.8.DsAspect 切面
package com.qxnw.tc.common.config.dbConfig;
import lombok.extern.slf4j.Slf4j;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @Copyright (C) XXXXXXXXXXX科技股份技有限公司
* @Author: lidongping
* @Date: 2021-07-21 18:40
* @Description:
*/
@Order(0)
@Aspect
@Component
@Slf4j
public class DsAspect {
/**
* 配置AOP切面的切入点
* 切换放在service接口的方法上
*/
@Pointcut("execution(* com.qxnw.tc.sys.service.impl.*Service.*(..))")
public void dataSourcePointCut() {
}
/**
* 使用 dataSourceSlave 数据源必须贴注解
* 根据切点信息获取调用函数是否用TargetDataSource切面注解描述,
* 如果设置了数据源,则进行数据源切换
*/
@Before("dataSourcePointCut()")
public void before(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
Method m = ((MethodSignature) joinPoint.getSignature()).getMethod();
try {
if (null != m && m.isAnnotationPresent(DSUser.class)) {
// 根据注解 切换数据源
DSUser td = m.getAnnotation(DSUser.class);
String dbStr = td.value();
DbContextHolder.setCurrentDsStr(dbStr);
log.info("-->current thread {} add dataSource[{}] to ThreadLocal, request method name is : {}",
Thread.currentThread().getName(), dbStr, method);
}
} catch (Exception e) {
log.error("-->current thread {} add data to ThreadLocal error,{}", Thread.currentThread().getName(), e);
throw e;
}
}
/**
* 执行完切面后,将线程共享中的数据源名称清空,
* 数据源恢复为原来的默认数据源
*/
@After("dataSourcePointCut()")
public void after(JoinPoint joinPoint) {
DbContextHolder.clearCurrentDsStr();
}
}
3.使用
如果要使用slave数据库只需要在 service方法上增加注解 @DSUser即可!