动态切换数据源,无非是继承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
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
新建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;
}
}
运行效果如图:
可以看到,同样可以实现动态切换数据源,主要是基于aop在每次访问数据库之前动态指定一下数据源的key,两个连接池在启动的时候就已经加载好了,只是动态的去找哪个池子拿连接而已。
总结
第二种自己实现的方法功能比较简陋,没有做负载均衡策略,追求简单的话,可以直接用第一种别人写好的,原理应该就是第二种的实现方式,具体第一种的源码没去看,原理应该是一样的。