springboot动态切换数据源两种方法示例

动态切换数据源,无非是继承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource这个类,重写determineCurrentLookupKey()这个方法,动态变换数据源的key值,下面介绍两个方法。

方法一

最简单的方法,已经有人把详细代码封装到框架中只要maven引入配置一下,就可以了:dynamic-datasource-spring-boot-starter

maven引入:



    com.baomidou
    dynamic-datasource-spring-boot-starter
    3.2.1


application.properties文件里把原先的数据源配置注释掉,写入新的配置:

#spring.datasource.url=jdbc\:oracle\:thin\:@127.0.0.1\:1521\:xe
#spring.datasource.username=zhaohy
#spring.datasource.password=oracle
#spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

#set default datasource
spring.datasource.dynamic.primary=master
spring.datasource.dynamic.datasource.master.url=jdbc\:oracle\:thin\:@127.0.0.1\:1521\:xe
spring.datasource.dynamic.datasource.master.username=name1
spring.datasource.dynamic.datasource.master.password=xxx
spring.datasource.dynamic.datasource.master.driver-class-name=oracle.jdbc.OracleDriver

spring.datasource.dynamic.datasource.slave_1.url=jdbc\:oracle\:thin\:@127.0.0.1\:1521\:xe
spring.datasource.dynamic.datasource.slave_1.username=name2
spring.datasource.dynamic.datasource.slave_1.password=xxx
spring.datasource.dynamic.datasource.slave_1.driver-class-name=oracle.jdbc.OracleDriver

参考的是官网的例子

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx) # 内置加密,使用请查看详细文档
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver
          schema: db/schema.sql # 配置则生效,自动初始化表结构
          data: db/data.sql # 配置则生效,自动初始化数据
          continue-on-error: true # 默认true,初始化失败是否继续
          separator: ";" # sql默认分号分隔符
          
       #......省略
       #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2

这样就配置好了,master是默认数据源,slave_1是第二个数据源,名字可以随便取
如果不指定数据源,则直接是master默认的,如虚指定数据源,则可去mybatis的mapper层加注解@DS("slave_1")即可,当然也可以加在service层的方法或者类上面。如果只是指定@DS则负载均衡的访问配置好的数据库,如果不是主从数据库还是不要用这个。

测试:
controller:

@RequestMapping("/test/test1.do")
    public void test1(HttpServletRequest request) {
        testService.test1();
    }

serviceImpl:

public void test1() {
        List> list = new ArrayList>();
        Map paramsMap = new HashMap();
        list = testMapper.getUserList(paramsMap);
        System.out.println("===" + JSON.toJSONString(list));
        list = testMapper.getUserList1(paramsMap);
        System.out.println("1===" + JSON.toJSONString(list));
    }

mapper:

package com.zhaohy.app.dao;

import java.util.List;
import java.util.Map;

import com.baomidou.dynamic.datasource.annotation.DS;

public interface TestMapper {

    List> getUserList(Map paramsMap);

    @DS("slave_1")
    List> getUserList1(Map paramsMap);

}

xml:




    
    
    
    

运行效果:


动态数据源测试效果图

从上图可以看到,因为两个数据库的da_user的条数不同,第一个查到了4条,第二个查到了9条,可见生效了。

springboot默认的数据源连接池是HikariDataSource,这个开源动态数据源框架也支持阿里的druid.

集成druid

maven引入druid

        
            com.alibaba
            druid-spring-boot-starter
            1.1.24
        

更改数据源配置:

#spring.datasource.url=jdbc\:oracle\:thin\:@127.0.0.1\:1521\:xe
#spring.datasource.username=zhaohy
#spring.datasource.password=oracle
#spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

#set druid
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=123456

#set default datasource
spring.datasource.dynamic.primary=master
spring.datasource.dynamic.datasource.master.url=jdbc\:oracle\:thin\:@127.0.0.1\:1521\:xe
spring.datasource.dynamic.datasource.master.username=zhaohy
spring.datasource.dynamic.datasource.master.password=oracle
spring.datasource.dynamic.datasource.master.driver-class-name=oracle.jdbc.OracleDriver

spring.datasource.dynamic.datasource.slave_1.url=jdbc\:oracle\:thin\:@39.100.143.84\:1521\:xe
spring.datasource.dynamic.datasource.slave_1.username=zhaohy
spring.datasource.dynamic.datasource.slave_1.password=oracle
spring.datasource.dynamic.datasource.slave_1.driver-class-name=oracle.jdbc.OracleDriver

spring.datasource.dynamic.datasource.master.druid.initial-size=3
spring.datasource.dynamic.datasource.master.druid.max-active=8
spring.datasource.dynamic.datasource.master.druid.min-idle=2
spring.datasource.dynamic.datasource.master.druid.max-wait=-1
spring.datasource.dynamic.datasource.master.druid.min-evictable-idle-time-millis=30000
spring.datasource.dynamic.datasource.master.druid.max-evictable-idle-time-millis=30000
spring.datasource.dynamic.datasource.master.druid.time-between-eviction-runs-millis=0
spring.datasource.dynamic.datasource.master.druid.validation-query=select 1 from dual
spring.datasource.dynamic.datasource.master.druid.validation-query-timeout=-1
spring.datasource.dynamic.datasource.master.druid.test-on-borrow=false
spring.datasource.dynamic.datasource.master.druid.test-on-return=false
spring.datasource.dynamic.datasource.master.druid.test-while-idle=true
spring.datasource.dynamic.datasource.master.druid.pool-prepared-statements=true
spring.datasource.dynamic.datasource.master.druid.filters=stat,wall
spring.datasource.dynamic.datasource.master.druid.share-prepared-statements=true

在springboot启动类上排除原生Druid的快速配置类。

package com.zhaohy.app;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.zhaohy.app.sys.filter.LoginProcessFilter;
import com.zhaohy.app.utils.OnLineCountListener;

@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
@MapperScan("com.zhaohy.app.dao")
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
        System.out.println("springboot started...");
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean myFilterRegistration() {
        FilterRegistrationBean regist = new FilterRegistrationBean(new LoginProcessFilter());
        // 过滤全部请求
        regist.addUrlPatterns("/*");//过滤url
        regist.setOrder(1);//过滤器顺序
        return regist;
    }
    
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public ServletListenerRegistrationBean listenerRegist() {
        ServletListenerRegistrationBean srb = new ServletListenerRegistrationBean();
        srb.setListener(new OnLineCountListener());
        System.out.println("listener====");
        return srb;
    }
}

为什么要排除?

DruidDataSourceAutoConfigure在DynamciDataSourceAutoConfiguration之前,其会注入一个DataSourceWrapper,会在原生的spring.datasource下找url,username,password等。而我们动态数据源的配置路径是变化的。

这样,再次运行,也可以得到同样的结果。

方法二

可以根据实现原理,自己实现一下。
pom.xml注释掉动态框架依赖


        
        
        
        
            com.alibaba
            druid-spring-boot-starter
            1.1.24
        

注释掉applicaton.properties文件里的相关数据源配置

#set druid
#spring.datasource.druid.stat-view-servlet.login-username=admin
#spring.datasource.druid.stat-view-servlet.login-password=123456

#set default datasource
#spring.datasource.dynamic.primary=master
#spring.datasource.dynamic.datasource.master.url=jdbc\:oracle\:thin\:@127.0.0.1\:1521\:xe
#spring.datasource.dynamic.datasource.master.username=name1
#spring.datasource.dynamic.datasource.master.password=xxx
#spring.datasource.dynamic.datasource.master.driver-class-name=oracle.jdbc.OracleDriver
#
#spring.datasource.dynamic.datasource.slave_1.url=jdbc\:oracle\:thin\:@127.0.0.1\:1521\:xe
#spring.datasource.dynamic.datasource.slave_1.username=name2
#spring.datasource.dynamic.datasource.slave_1.password=xxx
#spring.datasource.dynamic.datasource.slave_1.driver-class-name=oracle.jdbc.OracleDriver
#
#spring.datasource.dynamic.datasource.master.druid.initial-size=3
#spring.datasource.dynamic.datasource.master.druid.max-active=8
#spring.datasource.dynamic.datasource.master.druid.min-idle=2
#spring.datasource.dynamic.datasource.master.druid.max-wait=-1
#spring.datasource.dynamic.datasource.master.druid.min-evictable-idle-time-millis=30000
#spring.datasource.dynamic.datasource.master.druid.max-evictable-idle-time-millis=30000
#spring.datasource.dynamic.datasource.master.druid.time-between-eviction-runs-millis=0
#spring.datasource.dynamic.datasource.master.druid.validation-query=select 1 from dual
#spring.datasource.dynamic.datasource.master.druid.validation-query-timeout=-1
#spring.datasource.dynamic.datasource.master.druid.test-on-borrow=false
#spring.datasource.dynamic.datasource.master.druid.test-on-return=false
#spring.datasource.dynamic.datasource.master.druid.test-while-idle=true
#spring.datasource.dynamic.datasource.master.druid.pool-prepared-statements=true
#spring.datasource.dynamic.datasource.master.druid.filters=stat,wall
#spring.datasource.dynamic.datasource.master.druid.share-prepared-statements=true

在src/main/resources下面新建applicationContext.xml




    
      

新建database.xml




    
    
        
        
        
        

        
        
        
        

        
        

        
        
        
        

        
        
        
        

        
        
        
    

    
        
    

    
    
        
        
        
        
        
    


    
        
        
        
        
        
    

    
    

    
    
        
            
                
                
            
        
        
    

新建DynamicDataSource继承AbstractRoutingDataSource重写determineCurrentLookupKey()方法

/*    */ package com.zhaohy.app.utils;
/*    */ 
/*    */ import java.util.Map;
/*    */ import javax.sql.DataSource;
/*    */ import org.slf4j.Logger;
/*    */ import org.slf4j.LoggerFactory;
/*    */ import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/*    */ import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
/*    */ 
/*    */ public class DynamicDataSource
/*    */   extends AbstractRoutingDataSource
/*    */ {
/* 17 */   private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);
/*    */ 
/*    */ 
/*    */   
/*    */   public static final String DATASOURCE_BEAN_CLASS = "com.alibaba.druid.pool.DruidDataSource";
/*    */ 
/*    */   
/*    */   private int readDBSize;
/*    */ 
/*    */ 
/*    */   
/*    */   protected Object determineCurrentLookupKey() {
/* 29 */      String dsKey = DynamicDataSourceContextHolder.getDataSourceRouterKey();
/* 31 */      dsKey = (dsKey == null) ? "master" : dsKey;
/* 34 */     log.info("当前数据源key:{}", dsKey);
/* 35 */     return dsKey;
/*    */   }
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */   
/* 43 */   public void setDataSourceLookup(DataSourceLookup dataSourceLookup) { super.setDataSourceLookup(dataSourceLookup); }
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */   
/* 51 */   public void setDefaultTargetDataSource(Object defaultTargetDataSource) { super.setDefaultTargetDataSource(defaultTargetDataSource); }
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */   
/* 59 */   public void setTargetDataSources(Map targetDataSources) { super.setTargetDataSources(targetDataSources); }
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */   
/* 66 */   public DataSource getTargetDataSource() { return determineTargetDataSource(); }
/*    */ 
/*    */ 
/*    */ 
/*    */   
/* 71 */   public void setReadDBSize(int readDBSize) { this.readDBSize = readDBSize; }
/*    */ 
/*    */ 
/*    */   
/* 75 */   public int getReadDBSize() { return this.readDBSize; }
}

新建DynamicDataSourceContextHolder类:

package com.zhaohy.app.utils;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DynamicDataSourceContextHolder {
    private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

    /**
     * 存储已经注册的数据源的key
     */
    public static List dataSourceIds = new ArrayList<>();

    /**
     * 线程级别的私有变量
     */
    private static final ThreadLocal HOLDER = new ThreadLocal<>();

    public static String getDataSourceRouterKey () {
        return HOLDER.get();
    }

    public static void setDataSourceRouterKey (String dataSourceRouterKey) {
        logger.info("切换至{}数据源", dataSourceRouterKey);
        HOLDER.set(dataSourceRouterKey);
    }

    /**
     * 设置数据源之前一定要先移除
     */
    public static void removeDataSourceRouterKey () {
        HOLDER.remove();
    }

    /**
     * 判断指定DataSrouce当前是否存在
     *
     * @param dataSourceId
     * @return
     */
    public static boolean containsDataSource(String dataSourceId){
        return dataSourceIds.contains(dataSourceId);
    }
}

上面这个类中用到ThreadLocal ,保证线程安全。

新建Datasource注解类

package com.zhaohy.app.sys.annotation;

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

@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Datasource {
    String name();
}

@Target注解里面参数解释:
METHOD 可用于方法上
TYPE 可用于类或者接口上
ANNOTATION_TYPE 可用于注解类型上(被@interface修饰的类型)
CONSTRUCTOR 可用于构造方法上
FIELD 可用于域上
LOCAL_VARIABLE 可用于局部变量上
PACKAGE 用于记录java文件的package信息
PARAMETER 可用于参数上

新建DynamicDataSourceAspect切面类:

package com.zhaohy.app.sys.aspect;

import java.lang.reflect.Method;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import com.zhaohy.app.sys.annotation.Datasource;
import com.zhaohy.app.utils.DynamicDataSourceContextHolder;

/**
 * 定义日志切面
 * 
 * @author zhaohy
 * @Lazy 注解:容器一般都会在启动的时候实例化所有单实例 bean,如果我们想要 Spring 在启动的时候延迟加载 bean,需要用到这个注解
 *       value为true、false 默认为true,即延迟加载,@Lazy(false)表示对象会在初始化的时候创建
 */
@Aspect
@Component
@Lazy(false)
public class DynamicDataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);


    @Before("@annotation(ds)")
    public void begin(JoinPoint joinPoint, Datasource ds) {
        //System.out.println("========" + ds.name());
        // 动态切换数据源
        DynamicDataSourceContextHolder.setDataSourceRouterKey(ds.name());
    }

    @After("@annotation(ds)")
    public void after(JoinPoint point, Datasource ds) {
        //System.out.println("==@After==  finally returning");
        // 数据源重置
        DynamicDataSourceContextHolder.removeDataSourceRouterKey();
    }

    /**
     * 获取方法中声明的注解
     *
     * @param joinPoint
     * @return
     * @throws NoSuchMethodException
     */
    public Datasource getDeclaredAnnotation(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        // 获取方法名
        String methodName = joinPoint.getSignature().getName();
        // 反射获取目标类
        Class targetClass = joinPoint.getTarget().getClass();
        // 拿到方法对应的参数类型
        Class[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
        // 根据类、方法、参数类型(重载)获取到方法的具体信息
        Method objMethod = targetClass.getMethod(methodName, parameterTypes);
        // 拿到方法定义的注解信息
        Datasource annotation = objMethod.getDeclaredAnnotation(Datasource.class);
        // 返回
        return annotation;
    }
}

修改mapper中的注解为自定义@Datasource注解

package com.zhaohy.app.dao;

import java.util.List;
import java.util.Map;

import com.zhaohy.app.sys.annotation.Datasource;

//import com.baomidou.dynamic.datasource.annotation.DS;

public interface TestMapper {

    List> getUserList(Map paramsMap);

    //@DS("slave_1")
    @Datasource(name="slave_1")
    List> getUserList1(Map paramsMap);

}

修改启动类引入applicationContext.xml

package com.zhaohy.app;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportResource;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.zhaohy.app.sys.filter.LoginProcessFilter;
import com.zhaohy.app.utils.OnLineCountListener;

@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
@MapperScan("com.zhaohy.app.dao")
@ImportResource({"classpath:applicationContext.xml"})
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
        System.out.println("springboot started...");
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean myFilterRegistration() {
        FilterRegistrationBean regist = new FilterRegistrationBean(new LoginProcessFilter());
        // 过滤全部请求
        regist.addUrlPatterns("/*");//过滤url
        regist.setOrder(1);//过滤器顺序
        return regist;
    }
    
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public ServletListenerRegistrationBean listenerRegist() {
        ServletListenerRegistrationBean srb = new ServletListenerRegistrationBean();
        srb.setListener(new OnLineCountListener());
        System.out.println("listener====");
        return srb;
    }
}

运行效果如图:


动态数据源测试效果图2

可以看到,同样可以实现动态切换数据源,主要是基于aop在每次访问数据库之前动态指定一下数据源的key,两个连接池在启动的时候就已经加载好了,只是动态的去找哪个池子拿连接而已。

总结

第二种自己实现的方法功能比较简陋,没有做负载均衡策略,追求简单的话,可以直接用第一种别人写好的,原理应该就是第二种的实现方式,具体第一种的源码没去看,原理应该是一样的。

你可能感兴趣的:(springboot动态切换数据源两种方法示例)