springboot整合mybatisplus实现多数据源切换与事务管理

目录

1.背景

2.实现步骤

2.1.数据源配置

2.2.DbContextHolder 配置

2.3.MyRoutingDataSource  配置

2.4.DataSourceConfiguration 配置

2.5.MybatisConfiguration  配置

2.6.TransactionConfig  配置

2.7.DSUser 注解

2.8.DsAspect 切面

3.使用

完美


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即可!

完美

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值