超详细教程-自定义动态切换数据源


话不多说,直接上干货

一、项目思路梳理

  1. 自定义一个注解 @DataSource,将来可以将该注解加在 service 层方法或者类上面,表示方法或者类中的所有方法都使用某一个数据源。
  2. 对于第一步,如果某个方法上面有 @DataSource 注解,那么就将该方法需要使用的数据源名称存入到 ThreadLocal。
  3. 自定义切面,在切面中解析 @DataSource 注解,当一个方法或者类上面有 @DataSource 注解的时候,将 @DataSource 注解所标记的数据源存入到 ThreadLocal 中。
  4. 最后,当 Mapper 执行的时候,需要 DataSource,他会自动去 AbstractRoutingDataSource 类中查找需要的数据源,我们只需要在 AbstractRoutingDataSource 中返回 ThreadLocal 中的值即可。

二、项目结构

在这里插入图片描述

三、步骤

1、pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>


    <groupId>org.datasource.zz</groupId>
    <artifactId>dynamic_datasource</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>dynamic_datasource</name>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.9</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2、yaml 文件

server:
  port: 8081
# 数据源配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    ds:
      # 主库数据源
      master:
        url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      slave:
        url: jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
    # 初始连接数
    initialSize: 5
    # 最小连接池数量
    minIdle: 10
    # 最大连接池数量
    maxActive: 20
    # 配置获取连接等待超时的时间
    maxWait: 60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    timeBetweenEvictionRunsMillis: 60000
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    minEvictableIdleTimeMillis: 300000
    # 配置一个连接在池中最大生存的时间,单位是毫秒
    maxEvictableIdleTimeMillis: 900000
    # 配置检测连接是否有效
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    webStatFilter:
      enabled: true
    statViewServlet:
      enabled: true
      # 设置白名单,不填则允许所有访问
      allow:
      url-pattern: /druid/*
      # 控制台管理用户名和密码
      login-username: tienchin
      login-password: 123456
    filter:
      stat:
        enabled: true
        # 慢SQL记录
        log-slow-sql: true
        slow-sql-millis: 1000
        merge-sql: true
      wall:
        config:
          multi-statement-allow: true

3、annotation包下-注解DataSource

自定义一个注解 @DataSource,将来可以将该注解加在 service 层方法或者类上面,表示方法或者类中的所有方法都使用某一个数据源。

package org.datasource.zz.annotation;

import org.datasource.zz.datasource.DataSourceType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 这个注解将来可以加在某个service类或者方法上,
 * 通过value 属性注定类或者方法应该使用哪一个数据源
 * @author 81230
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface DataSource {
    /**
     * 如果方法上将来 @Datasource 注解,但是却没有指定数据源的名称,那么默认使用 master 数据源
     * @return
     */
    String value() default DataSourceType.DEFAULT_DS_NAME;
}

4、aspect包下-自定义切面

自定义切面,在切面中解析 @DataSource 注解,当一个方法或者类上面有 @DataSource 注解的时候,将 @DataSource 注解所标记的数据源存入到 ThreadLocal 中。

4.1、DataSourceAspect
package org.datasource.zz.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.datasource.zz.annotation.DataSource;
import org.datasource.zz.datasource.DynamicDataSourceContextHolder;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


@Component
@Aspect
public class DataSourceAspect {

    /**
     * @annotation(org.javaboy.dd.annotation.DataSource) 表示方法上有 @DataSource 注解就将方法拦截下来
     * @within(org.javaboy.dd.annotation.DataSource) 表示如果类上面有 @DataSource 注解,就将类中的方法拦截下来
     */
    @Pointcut("@annotation(org.datasource.zz.annotation.DataSource) || @within(org.datasource.zz.annotation.DataSource)")
    public void pc() {

    }

    @Around("pc()")
    public Object around(ProceedingJoinPoint pjp) {
        //获取方法上面的有效注解
        DataSource dataSource = getDataSource(pjp);
        if (dataSource != null) {
            //获取注解中数据源的名称
            String value = dataSource.value();
            DynamicDataSourceContextHolder.setDataSourceType(value);
        }
        try {
            return pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        } finally {
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
        return null;
    }

    private DataSource getDataSource(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        //查找方法上的注解
        DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (annotation != null) {
            //说明方法上面有 @DataSource 注解
            return annotation;
        }
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

4.2、GloalDataSourceAspect (全局)
package org.datasource.zz.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.datasource.zz.datasource.DataSourceType;
import org.datasource.zz.datasource.DynamicDataSourceContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpSession;

/**
 * 全局数据源切面
 */
@Aspect
@Component
public class GloalDataSourceAspect {

    @Autowired
    HttpSession session;

    @Pointcut("execution(* org.datasource.zz.service.*.*(..))")
    public void pc() {

    }

    @Around("pc()")
    public Object around(ProceedingJoinPoint pjp) {
        DynamicDataSourceContextHolder.setDataSourceType((String) session.getAttribute(DataSourceType.DS_SESSION_KEY));
        try {
            return pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }finally {
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
        return null;
    }
}

5、datasource包下

5.1、DataSourceType
package org.datasource.zz.datasource;


/**
 * @author 81230
 */
public interface DataSourceType {
    String DEFAULT_DS_NAME = "master";
    String DS_SESSION_KEY = "ds_session_key";
}


5.2、DruidProperties
package org.datasource.zz.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;

import javax.sql.DataSource;
import java.util.Map;

/**
 * @author 81230
 */
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {

    private String type;
    private String driverClassName;
    private Map<String,Map<String,String>> ds;
    // 初始连接数
    private Integer initialSize;
    // 最小连接池数量
    private Integer minIdle;
    // 最大连接池数量
    private Integer maxActive;
    // 配置获取连接等待超时的时间
    private Integer maxWait;

    /**
     * 一会在外部构造好一个 DruidDataSource 对象,
     * 但是这个对象只包含三个核心属性 url、username、password
     * 在这个方法中,给这个对象设置公共属性
     * @param druidDataSource
     * @return
     */
    public DataSource datasource(DruidDataSource druidDataSource){
        druidDataSource.setInitialSize(initialSize);
        druidDataSource.setMinIdle(minIdle);
        druidDataSource.setMaxActive(maxActive);
        druidDataSource.setMaxWait(maxWait);
        return druidDataSource;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public Map<String, Map<String, String>> getDs() {
        return ds;
    }

    public void setDs(Map<String, Map<String, String>> ds) {
        this.ds = ds;
    }

    public Integer getInitialSize() {
        return initialSize;
    }

    public void setInitialSize(Integer initialSize) {
        this.initialSize = initialSize;
    }

    public Integer getMinIdle() {
        return minIdle;
    }

    public void setMinIdle(Integer minIdle) {
        this.minIdle = minIdle;
    }

    public Integer getMaxActive() {
        return maxActive;
    }

    public void setMaxActive(Integer maxActive) {
        this.maxActive = maxActive;
    }

    public Integer getMaxWait() {
        return maxWait;
    }

    public void setMaxWait(Integer maxWait) {
        this.maxWait = maxWait;
    }

}

5.3、DynamicDataSource

最后,当 Mapper 执行的时候,需要 DataSource,他会自动去 AbstractRoutingDataSource 类中查找需要的数据源,我们只需要在 AbstractRoutingDataSource 中返回 ThreadLocal 中的值即可

package org.datasource.zz.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author 81230
 */
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 这个方法用来返回数据源名称,当系统需要获取数据源的时候,会自动调用该方法获取数据源的名称
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }

    public DynamicDataSource(LoadDataSource loadDataSource){
        // 1设置所有数据源
        Map<String, DataSource> allDs = loadDataSource.loadAllDataSource();
        super.setTargetDataSources(new HashMap<>(allDs));
        //2.设置默认的数据源
        // 将来,并不是所有的方法上都有 @DataSource 注解,
        // 对于那些没有 @DataSource 注解的方法,该使用哪个数据源?
        super.setDefaultTargetDataSource(allDs.get(DataSourceType.DEFAULT_DS_NAME));
        // 3
        super.afterPropertiesSet();
    }
}

5.4、DynamicDataSourceContextHolder类

如果某个方法上面有 @DataSource 注解,那么就将该方法需要使用的数据源名称存入到 ThreadLocal。

package org.datasource.zz.datasource;


/**
 * 这个类用来存储当前线程所使用的数据源名称
 * @author 81230
 */
public class DynamicDataSourceContextHolder {
   private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

   public static void setDataSourceType(String dsType) {
       CONTEXT_HOLDER.set(dsType);
   }

   public static String getDataSourceType() {
       return CONTEXT_HOLDER.get();
   }

   public static void clearDataSourceType() {
       CONTEXT_HOLDER.remove();
   }

}

5.5、LoadDataSource
package org.datasource.zz.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;


/**
 * @author 81230
 */
@Component
@EnableConfigurationProperties(DruidProperties.class)
public class LoadDataSource {
    @Autowired
    DruidProperties druidProperties;

    /**
     * 加载全部数据源
     * @return
     */
    public Map<String, DataSource> loadAllDataSource() {
        Map<String, DataSource> map = new HashMap<>();
        Map<String, Map<String, String>> ds = druidProperties.getDs();
        try {
            Set<String> keySet = ds.keySet();
            for (String key : keySet) {
                map.put(key, druidProperties.datasource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }
}

6、model包下 --User

package org.datasource.zz.model;

/**
 * @author 81230
 */
public class User {

    private Integer id;
    private String name;
    private Integer age;

    public Integer getId() {
        return id;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

7、mapper包下 --UserMapper

package org.datasource.zz.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.datasource.zz.model.User;

import java.util.List;


/**
 * @author 81230
 */
@Mapper
public interface UserMapper {
    @Select("select * from user")
    List<User> getAllUsers();
}

8、service包下 --UserMapper

package org.datasource.zz.service;


import org.datasource.zz.annotation.DataSource;
import org.datasource.zz.mapper.UserMapper;
import org.datasource.zz.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;


@Service
public class UserService {
    @Autowired
    UserMapper userMapper;

    @DataSource("master")
    public List<User> getAllUsers() {
        return userMapper.getAllUsers();
    }
}

9、controller包下 --DataSourceController

package org.datasource.zz.controller;

import org.datasource.zz.datasource.DataSourceType;
import org.datasource.zz.model.User;
import org.datasource.zz.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.util.List;

/**
 * @author 81230
 */
@RestController
public class DataSourceController {
    private static final Logger logger = LoggerFactory.getLogger(DataSourceController.class);

    @Autowired
    UserService userService;


    /**
     * 修改数据源
     * @param dsType
     */
    @PostMapping("/dstype")
    public void setDsType(String dsType, HttpSession session){
        session.setAttribute(DataSourceType.DS_SESSION_KEY,dsType);
        logger.info("数据源切换{}",dsType);
    }

    @GetMapping("/users")
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }



}


三、测试

3.1、默认位master数据源

在这里插入图片描述

3.2、切换为slave数据源

在这里插入图片描述

3.3、重新获取(切换数据源成功)

在这里插入图片描述

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mybatis-Plus是一个基于Mybatis的增强工具,它提供了很多便捷的功能来简化开发。在使用Mybatis-Plus时,如果需要实现动态数据的功能,可以按照以下步骤进行配置。 首先,需要创建一个自定义动态数据类,比如`MyDynamicDataSource`。这个类需要继承`AbstractRoutingDataSource`,并实现`determineCurrentLookupKey`方法来返回当前的数据标识。在这个类中,可以使用`ThreadLocal`来保存当前的数据标识,以便在不同的线程中切换数据。同时,还需要提供设置数据标识和清除数据标识的方法。\[1\] 接下来,需要引入`dynamic-datasource-spring-boot-starter`的依赖,可以在`pom.xml`文件中添加以下配置: ```xml <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.1</version> </dependency> ```\[2\] 然后,需要创建一个配置类,比如`DataSourceConfig`,并使用`@Configuration`注解标记。在这个配置类中,可以使用`@Bean`注解来创建主数据和从数据,并使用`@ConfigurationProperties`注解来指定数据的配置信息。同时,还需要使用`@Primary`注解标记主数据。最后,将主数据和从数据添加到`MyDynamicDataSource`中,并返回一个`MyDynamicDataSource`实例。\[3\] 通过以上配置,就可以实现Mybatis-Plus的动态数据功能了。在使用Mybatis-Plus时,可以通过调用`MyDynamicDataSource.setDataSource`方法来设置当前的数据标识,从而实现动态切换数据的功能。 #### 引用[.reference_title] - *1* *3* [MyBatis Plus动态数据](https://blog.csdn.net/qq_44802667/article/details/129247656)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [mybatis-plus动态数据](https://blog.csdn.net/qq_41472891/article/details/123270082)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值