多數據源系統接入mybatis-plus, 實現動態數據源、動態事務。

目錄:

  • 實現思想
  • 導入依賴、配置說明
  • 代碼實現
  • 問題總結

一.實現思想

  接手一個舊系統,SpringBoot 使用的是純粹的 mybatis ,既沒有使用規範的代碼生成器,也沒有使用 JPA 或者 mybatis-plus

  想着接入 mybatis-plus,為以後敲代碼省點力氣。普通的接入 mybatis-plus 可以直接參考官方文檔 //mp.baomidou.com/ 

  但我接手的系統是個多數據源系統,本來最優的方法是使用官方的 動態數據源 支持 //mp.baomidou.com/guide/dynamic-datasource.html 

  但我因為亂七八糟的依賴衝突,決定自己實現 動態數據源 的支持。

  實現的核心邏輯:使用一個 代理數據源,來管理 其他數據源 的分發請求。(通過AOP分發)

 

二.導入依賴、配置說明

  因為依賴的衝突,我沒有直接使用

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1.tmp</version>
</dependency>

  而是引入的

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus</artifactId>
    <version>3.3.1.tmp</version>
</dependency>

  數據庫配置文件大致如下

spring:
  datasource: #數據庫配置
    primary: #數據庫1
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://
      username: root
      password: 3
      type: com.alibaba.druid.pool.DruidDataSource
    second: #數據庫2
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://
      username: root
      password: 3
      type: com.alibaba.druid.pool.DruidDataSource
    third: #數據庫3
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://
      username: root
      password: 3
      type: com.alibaba.druid.pool.DruidDataSource
    

 

  MybatisPlus 配置大致如下(mybatis 的配置可以刪除)

mybatis-plus:
  # 掃描 mapper.xml
  mapper-locations: classpath:mapper/*.xml #也可以不配置,在代碼中設置
#  configuration:
#    map-underscore-to-camel-case: false

 

 

三.代碼實現

  1.我們先新建 數據源的枚舉

public enum DataSourceEnums {

    PRIMARY("primaryDataSource"),
    SECOND("secondDataSource"),
    THIRD("thirdDataSource");

    private String value;

    DataSourceEnums(String value){this.value=value;}

    public String getValue() {
        return value;
    }

}

  2.用來標記數據源的 註解(在哪裡使用哪個數據源)。

/**
 * @author zhaww
 * @date 2020/4/14
 * @Description .自定義 - 區分數據源的註解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyDataSource {
    DataSourceEnums value() default DataSourceEnums.PRIMARY;
}

  3.動態數據源管理器,繼承 AbstractRoutingDataSource  

 

/**
 * @author zhaww
 * @date 2020/4/10
 * @Description .動態數據源管理器
 */public class DataSourceContextHolder extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> contextHolder = new InheritableThreadLocal<>();

    /**
     * 重寫這個方法,這裡返回使用的數據源 key 值
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
//        log.info("動態切換數據源:" + DataSourceContextHolder.getDataSource());
        return contextHolder.get();
    }

    /**
     *  設置數據源
     * @param db
     */
    public static void setDataSource(String db){
        contextHolder.set(db);
    }

    /**
     * 取得當前數據源
     * @return
     */
    public static String getDataSource(){
        return contextHolder.get();
    }

    /**
     * 清除上下文數據
     */
    public static void clear(){
        contextHolder.remove();
    }

}

 

  4. mybatis-plus 的配置類

/**
 * @author zhaww
 * @date 2020/4/10
 * @Description .
 */
//@EnableTransactionManagement //開啟事務
@Configuration
@MapperScan(value = {"com.zydd.admin.dao"}) //掃描Mapper 層的類
public class MybatisPlusConfig {

    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "secondDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.second")
    public DataSource secondDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "thirdDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.third")
    public DataSource thirdDataSource() {
        return DataSourceBuilder.create().build();
    }


    @Bean(name = "multipleTransactionManager")
    @Primary
    public DataSourceTransactionManager multipleTransactionManager(@Qualifier("multipleDataSource") DataSource dataSource) {
//        return new MyDataSourceTransactionManager(dataSource);
        return new DataSourceTransactionManager(dataSource);
    }

    /**
     * 動態數據源配置
     *
     * @return
     */
    @Bean(name = "multipleDataSource")
    @Primary
    public DataSource multipleDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource,
                                         @Qualifier("secondDataSource") DataSource secondDataSource,
                                         @Qualifier("thirdDataSource") DataSource thirdDataSource) {
        DataSourceContextHolder dynamicDataSource = new DataSourceContextHolder();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceEnums.PRIMARY.getValue(), primaryDataSource);
        targetDataSources.put(DataSourceEnums.SECOND.getValue(), secondDataSource);
        targetDataSources.put(DataSourceEnums.THIRD.getValue(), thirdDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(thirdDataSource); // 默認使用的數據源
        return dynamicDataSource;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(multipleDataSource(primaryDataSource(), secondDataSource(), thirdDataSource()));

      //mybatis-plus yml 配置不生效,要在這裡代碼里配置 MybatisConfiguration configuration
= new MybatisConfiguration(); configuration.setJdbcTypeForNull(JdbcType.NULL); //是否使用轉駝峰 configuration.setMapUnderscoreToCamelCase(false); configuration.setCacheEnabled(false); sqlSessionFactory.setConfiguration(configuration); //添加分頁功能 Interceptor[] plugins = {paginationInterceptor()}; sqlSessionFactory.setPlugins(plugins); //掃描 mapper 路徑 ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resource = resolver.getResources("classpath:mapper/**/*.xml"); sqlSessionFactory.setMapperLocations(resource); return sqlSessionFactory.getObject(); } /** * @Description : mybatis-plus分頁插件 */ @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 設置請求的頁面大於最大頁後操作, true調回到首頁,false 繼續請求 默認false paginationInterceptor.setOverflow(true); // 設置最大單頁限制數量,默認 500 條,-1 不受限制 // paginationInterceptor.setLimit(30); return paginationInterceptor; } }

  5.使用AOP來實現數據源的動態設置。

/**
 * @author zhaww
 * @date 2020/4/9
 * @Description .AOP通用日誌記錄、動態數據源分發
 */
@Aspect
@Component
@Slf4j
@Order(-100)
public class AOP {


    /**
     * Controller層路徑
     */
    @Pointcut("within(com.zydd.admin.controller..*)")
    public void controllerPointcut() {
    }

    /**
     * Service層路徑
     */
    @Pointcut("within(com.zydd.admin.service..*)")
    public void servicePointcut() {
    }


    @Around("servicePointcut()")
    public Object doServiceLogging(ProceedingJoinPoint joinPoint) throws Throwable {
        changeDataSource(joinPoint); //檢查數據源
    }

    /**
     * Mapper層攔截,動態切換 mybatisPlus 數據源
     */
    @Before("execution(* com.zydd.admin.dao.primary..*(..))")
    public void doAdmin(){
        log.info("選擇數據源---" + DataSourceEnums.PRIMARY.getValue());
        DataSourceContextHolder.setDataSource(DataSourceEnums.PRIMARY.getValue());
    }

    /**
     * Mapper層攔截,動態切換 mybatisPlus 數據源
     */
    @Before("execution(* com.zydd.admin.dao.second..*(..))")
    public void doZYDD(){
        log.info("選擇數據源---" + DataSourceEnums.SECOND.getValue());
        DataSourceContextHolder.setDataSource(DataSourceEnums.SECOND.getValue());
    }

    /**
     * Mapper層攔截,動態切換 mybatisPlus 數據源
     */
    @Before("execution(* com.zydd.admin.dao.third..*(..))")
    public void doDW(){
        log.info("選擇數據源---" + DataSourceEnums.THIRD.getValue());
        DataSourceContextHolder.setDataSource(DataSourceEnums.THIRD.getValue());
    }

    
    /**
     * 通過註解 變更數據源
     * @param joinPoint
     */
    private void changeDataSource(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        MyDataSource myDataSource = null;
        //優先判斷方法上的註解
        if (method.isAnnotationPresent(MyDataSource.class)) {
            myDataSource = method.getAnnotation(MyDataSource.class);
            DataSourceContextHolder.setDataSource(myDataSource.value().getValue());
        } else if (method.getDeclaringClass().isAnnotationPresent(MyDataSource.class)) { //其次判斷類上的註解
            myDataSource = method.getDeclaringClass().getAnnotation(MyDataSource.class);
            DataSourceContextHolder.setDataSource(myDataSource.value().getValue());
        }
        log.info("註解方式選擇數據源---" + myDataSource.value().getValue());
    }

}

 注意:我們不但默認通過 Mapper 的路徑來切換數據源,還通過 Service 方法層來切換數據源。

  因為如果 service 有事務的話,進入service方法的時候,DataSourceTransactionManager 就設置好了默認數據源,就算通過Mapper層重新設置數據源,

  DataSourceTransactionManager 的默認數據源還是沒有變。

  所以在 事務管理器 設置默認數據源之前,就切換數據源,實現動態事務+動態數據源。

 

 

  6.實際使用,只要 MyDataSource 註解就ok了。也可以在 ServiceImpl 類上加註解。

    @Override
    @MyDataSource(DataSourceEnums.THIRD)
    @Transactional
    public void test() {
        DwUserMPEntity test2 = new DwUserMPEntity();
        test2.setUuid("test");
        test2.setNickname("test");
        test2.setPhone("test");
        dwUserDao.insert(test2);
//        throw new RuntimeException("lala");
    }

    @Override
    @MyDataSource(DataSourceEnums.PRIMARY)
    @Transactional
    public void test1() {
        SysRoleMPEntity test = new SysRoleMPEntity();
        test.setRole("test");
        test.setDescription("test");
        sysRoleDao.insert(test);
//        throw new RuntimeException("lala");
    }

 

五.問題總結

1.配置文件里 mybatis-plus的配置不生效:因為我們在 SqlSessionFactory 里重新寫了 MybatisConfiguration 。

2.啟用事務的話,動態數據源不生效:因為 service 有事務的話,在進入service方法時,DataSourceTransactionManager 就設置好了默認數據源。