SpringBoot+MyBatisPlus+Druid从单数据源到多数据源动态切换2-基于AbstractRoutingDataSource实现多数据源

系列文章

1. SpringBoot+MyBatisPlus+Druid从单数据源到多数据源动态切换1-单数据源

2. SpringBoot+MyBatisPlus+Druid从单数据源到多数据源动态切换2-基于AbstractRoutingDataSource实现多数据源

3. SpringBoot+MyBatisPlus+Druid从单数据源到多数据源动态切换3-基于dynamic-datasource实现多数据源 


文章目录

1.application.yml中spring.datasource.druid下添加多数据源配置

# 单数据源配置
spring:
  datasource:
#    type: com.alibaba.druid.pool.DruidDataSource
#    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      #主库数据源
      master:
        url: jdbc:mysql://localhost:3306/db-master?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: root
      #从库数据源
      slave:
        # 从数据源开关/默认关闭
        enabled: true
        url: jdbc:mysql://localhost:3306/db-slave?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: root
      # 初始连接数
      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
      # 用于采集web-jdbc关联监控的数据
      webStatFilter:
        enabled: true
      # 用于展示Druid的统计信息(提供监控信息展示的html页面+提供监控信息的JSON Api)
      statViewServlet:
        enabled: true
        # 设置ip白名单(逗号隔开),不填则允许所有访问
        allow:
        # ip黑名单(优先于allow)
        deny:
        # 监控页面的url
        url-pattern: /druid/*
        # 控制台管理用户名和密码
        login-username: ruoyi
        login-password: 123456
      filter:
        # 用于统计监控信息
        stat:
          enabled: true
          # 慢SQL记录
          log-slow-sql: true
          # 慢sql标准,单位毫秒
          slow-sql-millis: 1000
          # 合并sql
          merge-sql: true
        wall:
          config:
            # 是否允许一次执行多条语句,默认关闭
            multi-statement-allow: true

这里添加了两个数据源master和slave相关配置,分别连接db-master和db-slave两个库。 

2.Druid属性配置类DruidProperties

/**
 * druid 配置属性
 * 
 * @author ruoyi
 */
@Configuration
public class DruidProperties
{
    @Value("${spring.datasource.druid.initialSize}")
    private int initialSize;

    @Value("${spring.datasource.druid.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.druid.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.druid.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.validationQuery}")
    private String validationQuery;

    @Value("${spring.datasource.druid.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.datasource.druid.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.datasource.druid.testOnReturn}")
    private boolean testOnReturn;

    public DruidDataSource dataSource(DruidDataSource datasource)
    {
        /** 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMinIdle(minIdle);
        datasource.setMaxActive(maxActive);


        /** 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

        /**
         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
         */
        datasource.setValidationQuery(validationQuery);
        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
        datasource.setTestWhileIdle(testWhileIdle);
        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnBorrow(testOnBorrow);
        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnReturn(testOnReturn);
        return datasource;
    }
}

 通过@Value注解DruidProperties类会将application.yml中druid相关配置赋值给对应的字段。

3.数据源切换处理类DynamicDataSourceContextHolder

/**
 * 数据源切换处理
 * 
 * @author ruoyi
 */
public class DynamicDataSourceContextHolder
{
    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dsType)
    {
        log.info("切换到{}数据源", dsType);
        CONTEXT_HOLDER.set(dsType);
    }

    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType()
    {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType()
    {
        CONTEXT_HOLDER.remove();
    }
}

 DynamicDataSourceContextHolder中使用ThreadLocal为每个调用线程维护了一个String类型的线程本地变量,表示要切换的目标数据源的名字。并提供了set,get和clear方法。

4.新建AbstractRoutingDataSource的实现类DynamicDataSource

/**
 * 动态数据源
 * 
 * @author ruoyi
 */
public class DynamicDataSource extends AbstractRoutingDataSource
{
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
    {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * 获取目标数据源的key:
     * 调用了DynamicDataSourceContextHolder的getDataSourceType(),即读取线程本地变量CONTEXT_HOLDER的值
     * @return: java.lang.Object
     * @author: gefeng
     * @date: 2022/5/7 15:54
     */
    @Override
    protected Object determineCurrentLookupKey()
    {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

 新增了构造方法可设置默认的目标数据源和备用的目标数据源。

5.Druid多数据源配置类DruidConfig

/**
 * druid 配置多数据源
 * 3个Bean的加载顺序:
 * dynamicDataSource
 * ->masterDataSource(是dynamicDataSource的参数)
 * ->slaveDataSource(setDataSource中SpringUtils.getBean("slaveDataSource")中有使用)
 * 
 * @author ruoyi
 */
@Configuration
public class DruidConfig
{
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    //@ConfigurationProperties将自动将spring.datasource.druid.master下的配置属性赋值给该Bean(DruidDataSource的实例)
    public DataSource masterDataSource(DruidProperties druidProperties)
    {
        //创建一个DruidDataSource的实例,并将配置文件中druid属性配置赋值给该实例
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
//    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    //spring.datasource.druid.slave.enabled=true时,该bean将被Spring上下文加载
    //@ConfigurationProperties将自动将spring.datasource.druid.slave下的配置属性赋值给该Bean(DruidDataSource的实例)
    public DataSource slaveDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource)
    {
        //注意targetDataSources中数据源的key(DataSourceType.MASTER.name(),DataSourceType.SLAVE.name())
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }
    
    /**
     * 设置数据源
     * 
     * @param targetDataSources 备选数据源集合
     * @param sourceName 数据源名称
     * @param beanName bean名称
     */
    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
    {
        try
        {
            DataSource dataSource = SpringUtils.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        }
        catch (Exception e)
        {
        }
    }

    /**
     * 去除监控页面底部的广告
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
    {
        // 获取web监控页面的参数
        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
        // 提取common.js的配置路径
        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
        final String filePath = "support/http/resources/js/common.js";
        // 创建filter进行过滤
        Filter filter = new Filter()
        {
            @Override
            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
            {
            }
            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                    throws IOException, ServletException
            {
                chain.doFilter(request, response);
                // 重置缓冲区,响应头不会被重置
                response.resetBuffer();
                // 获取common.js
                String text = Utils.readFromResource(filePath);
                // 正则替换banner, 除去底部的广告信息
                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
                text = text.replaceAll("powered.*?shrek.wang</a>", "");
                response.getWriter().write(text);
            }
            @Override
            public void destroy()
            {
            }
        };
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(filter);
        registrationBean.addUrlPatterns(commonJsPattern);
        return registrationBean;
    }
}

 SpringBoot启动时在加载名为dynamicDataSource的Bean时,调用了DynamicDataSource中的构造方法,注意targetDataSources中数据源的key(DataSourceType.MASTER.name(),DataSourceType.SLAVE.name())。

6.数据源枚举类DataSourceType

/**
 * 数据源
 * 
 * @author ruoyi
 */
public enum DataSourceType
{
    /**
     * 主库
     */
    MASTER,

    /**
     * 从库
     */
    SLAVE
}

包含MASTER和SLAVE两个枚举值 ,对应上面提到的targetDataSources中数据源的key。

 7.自定义数据源注解@DataSource

/**
 * 自定义多数据源切换注解
 *
 * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
 * 如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类
 *
 * @author ruoyi
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
    /**
     * 切换数据源名称
     */
    public DataSourceType value() default DataSourceType.MASTER;
}

@DataSource只有只一个字段,用来设置目标数据源在targetDataSources中的key,类型为DataSourceType,默认为DataSourceType.MASTER。

8.多数据源动态切换AOP切面类DataSourceAspect

/**
 * 实现多数据源动态切换的aop切面
 * @within 对象级别
 * @annotation 方法级别
 * 
 * @author ruoyi
 */
@Aspect
@Order(1)
@Component
public class DataSourceAspect
{
    protected Logger logger = LoggerFactory.getLogger(getClass());

    //切点:@DataSource注解的方法和@DataSource注解的类的对象
    @Pointcut("@annotation(com.doit.annotation.DataSource)"
            + "|| @within(com.doit.annotation.DataSource)")
    public void dsPointCut()
    {

    }

    /**
     * 切点执行前:通过DynamicDataSourceContextHolder设置调用线程本地变量CONTEXT_HOLDER
     * 切点执行后:通过DynamicDataSourceContextHolder清除调用线程本地变量CONTEXT_HOLDER
     * @param point 1
     * @return: java.lang.Object
     * @author: gefeng
     * @date: 2022/5/7 16:24
     */
    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable
    {
        DataSource dataSource = getDataSource(point);

        if (StringUtils.isNotNull(dataSource))
        {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }

        try
        {
            return point.proceed();
        }
        finally
        {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSource getDataSource(ProceedingJoinPoint point)
    {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource))
        {
            return dataSource;
        }

        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

 切点为@DataSource注解的方法和@DataSource注解的类的对象被调用时。

9.在Controller的方法中如何使用@DataSource

@RestController
@RequestMapping("/user")
public class UserController extends BaseController{
    @Autowired
    private UserService userService;

    @GetMapping("/list")
    @DataSource(DataSourceType.MASTER)
    public TableDataInfo list(){
        List<User> list = userService.list();
        return getDataTable(list);
    }

}

UserContoller中list方法使用了master数据源。

@RestController
@RequestMapping("/employee")
public class EmployeeController extends BaseController{
    @Autowired
    private EmployeeService employeeService;

    @GetMapping("/list")
    @DataSource(DataSourceType.SLAVE)
    public TableDataInfo list(){
        List<Employee> list = employeeService.list();
        return getDataTable(list);
    }

}

EmployeeContoller中list方法使用了slave数据源。

二、启动测试

1.项目端口上下文配置

server:
  port: 9998
  servlet:
    context-path: /abstract-routing-demo

2.启动类

//exclude,排除此类的AutoConfig,即禁止 SpringBoot 自动注入数据源配置。
//
//DataSourceAutoConfiguration.class 会自动查找 application.yml 或者 properties 文件
//里的 spring.datasource.* 相关属性并自动配置单数据源「注意这里提到的单数据源」。
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class AbstractRoutingDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(AbstractRoutingDemoApplication.class, args);
    }
}

3.启动项目,使用测试工具insomnia访问接口

访问user/list接口

 控制台打印出sql语句:

 访问成功,可以看到连接的是master数据源。

访问employee/list接口

  控制台打印出sql语句:

 访问成功,可以看到连接的是slave数据源。数据源切换成功,perfect。

4.访问Druid监控页面

使用statViewServlet下配置的login-username和login-password登录可以看到上面的页面,成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值