Springboot+Druid动态切换数据源

2 篇文章 0 订阅
1 篇文章 0 订阅

一、描述

关于数据源的切换,在实际使用中经常出现,本文主要是使用Druid,最近经常使用到,根据以往项目的使用,调整后进行记录,方便自己以后查看,也便于大家一起学习。
特点

  1. 项目启动时已制定一个默认数据源。
  2. 系统不固定数据源,需要在访问时通过自定义的方式去指定或获取。

二、实现

2.1 maven引入jar

只提供关键jar包,主要就是数据库与连接池的。

		<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
             <groupId>tk.mybatis</groupId>
             <artifactId>mapper-spring-boot-starter</artifactId>
             <version>2.1.5</version>
         </dependency>
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>

2.2 数据源上下文DataSourceContextHolder

避免线程的安全问题,设置当前数据源时添加同步锁synchronized 。

package net.cnki.common.datasource;

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
//设置当前线程持有的数据源
    public static synchronized void setDBType(String dbType){
        contextHolder.set(dbType);
    }
//获取当前线程持有的数据源
    public static String getDBType(){
        return contextHolder.get();
    }

    public static void clearDBType(){
        contextHolder.remove();
    }
}

2.3 DynamicDataSource继承AbstractRoutingDataSource抽象类

重写了determineCurrentLookupKey()方法,在多个数据源中确定当前所需要使用的那一个。其实DynamicDataSource本身就是一个线程安全下的单例(单例本想用枚举,但是不可以继承,所以放弃了),dataSourceMap用于存储数据源信息。

package net.cnki.common.datasource;

import java.util.HashMap;
import java.util.Map;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
	private static DynamicDataSource instance;
	private static byte[] lock = new byte[0];
	private static Map<Object, Object> dataSourceMap = new HashMap<Object, Object>();

	@Override
	public void setTargetDataSources(Map<Object, Object> targetDataSources) {
		super.setTargetDataSources(targetDataSources);
		dataSourceMap.putAll(targetDataSources);
		super.afterPropertiesSet();
	}
	public Map<Object, Object> getDataSourceMap() {
		return dataSourceMap;
	}
	public static synchronized DynamicDataSource getInstance() {
		if (instance == null) {
			synchronized (lock) {
				if (instance == null) {
					instance = new DynamicDataSource();
				}
			}
		}
		return instance;
	}
	// 必须实现其方法
	protected Object determineCurrentLookupKey() {
		return DataSourceContextHolder.getDBType();
	}
}

2.4 Druid配置类

与直接使用Druid的差距不大,只是将注入数据源的 @bean 更换成自定义的方式。此处参数可以不在配置文件获取,毕竟参数不少,根据需要就可以。不想有默认数据源去掉参数的配置就行,但还是需要去掉注入。

package net.cnki.common.datasource;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.Servlet;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

@Configuration
public class DruidConfig {
	
	@Value("${spring.datasource.type}")
    private String db_type;
	
	@Value("${spring.datasource.driver-class-name}")
    private String db_driver_name;
	
	@Value("${spring.datasource.url}")
    private String db_url;
	
	@Value("${spring.datasource.username}")
    private String db_user;
	
	@Value("${spring.datasource.password}")
    private String db_pwd;
	
	// 连接池初始化大小 
    @Value("${spring.datasource.initialSize}")
    private int initialSize;

    // 连接池最小值
    @Value("${spring.datasource.minIdle}")
    private int minIdle;

    // 连接池最大 值
    @Value("${spring.datasource.maxActive}")
    private int maxActive;

    // 配置获取连接等待超时的时间 
    @Value("${spring.datasource.maxWait}")
    private int maxWait;

    // 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    @Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    // 配置一个连接在池中最小生存的时间,单位是毫秒
    @Value("${spring.datasource.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    // 用来验证数据库连接的查询语句,这个查询语句必须是至少返回一条数据的SELECT语句
    @Value("${spring.datasource.validationQuery}")
    private String validationQuery;

    // 检测连接是否有效
    @Value("${spring.datasource.testWhileIdle}")
    private boolean testWhileIdle;

    // 申请连接时执行validationQuery检测连接是否有效。做了这个配置会降低性能。
    @Value("${spring.datasource.testOnBorrow}")
    private boolean testOnBorrow;

    // 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
    @Value("${spring.datasource.testOnReturn}")
    private boolean testOnReturn;

    // 是否缓存preparedStatement,也就是PSCache。
    @Value("${spring.datasource.poolPreparedStatements}")
    private boolean poolPreparedStatements;
    
    // 指定每个连接上PSCache的大小。
    @Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")
    private int maxPoolPreparedStatementPerConnectionSize;
    
    // 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙  
    @Value("${spring.datasource.filters}")
    private String filters;
    
    // 通过connectProperties属性来打开mergeSql功能;慢SQL记录    
    @Value("${spring.datasource.connectionProperties}")
    private String connectionProperties;
    
    // Druid控制台配置:记录慢SQL 
    @Value("${spring.datasource.logSlowSql}")
    private String logSlowSql;
    
    @Value("${spring.datasource.removeAbandoned}")
    private boolean removeAbandoned;
    
    @Value("${spring.datasource.removeAbandonedTimeout}")
    private int removeAbandonedTimeout;
    
    @Value("${spring.datasource.logAbandoned}")
    private boolean logAbandoned;
	
	@Bean
    public DynamicDataSource druidDataSource() {
		Map<Object,Object> map = new HashMap<>();
		DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
		DruidDataSource defaultDataSource = new DruidDataSource();
		
		defaultDataSource.setDriverClassName(db_driver_name);
		defaultDataSource.setUrl(db_url);
		defaultDataSource.setUsername(db_user);
        defaultDataSource.setPassword(db_pwd);
        defaultDataSource.setInitialSize(initialSize);
		defaultDataSource.setMinIdle(minIdle);
		defaultDataSource.setMaxActive(maxActive);
		defaultDataSource.setMaxWait(maxWait);
		defaultDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
		defaultDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
		defaultDataSource.setValidationQuery(validationQuery);
		defaultDataSource.setTestWhileIdle(testWhileIdle);
		defaultDataSource.setTestOnBorrow(testOnBorrow);
		defaultDataSource.setTestOnReturn(testOnReturn);
		defaultDataSource.setPoolPreparedStatements(poolPreparedStatements);
		defaultDataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
		defaultDataSource.setRemoveAbandoned(removeAbandoned);
		defaultDataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout);
		defaultDataSource.setLogAbandoned(logAbandoned);
        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
        
        map.put("default", defaultDataSource);
        dynamicDataSource.setTargetDataSources(map);
        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
        
       
        return dynamicDataSource;
    }
	
    @Bean
    public ServletRegistrationBean<Servlet>  druid(){
    	// 现在要进行druid监控的配置处理操作
        ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
        // 白名单,多个用逗号分割, 如果allow没有配置或者为空,则允许所有访问
        servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
        // 黑名单,多个用逗号分割 (共同存在时,deny优先于allow)
        //servletRegistrationBean.addInitParameter("deny", "192.168.1.110");
        // 控制台管理用户名
        servletRegistrationBean.addInitParameter("loginUsername", "admin");
        // 控制台管理密码
        servletRegistrationBean.addInitParameter("loginPassword", "admin");
        // 是否可以重置数据源,禁用HTML页面上的“Reset All”功能
        servletRegistrationBean.addInitParameter("resetEnable", "false");

        return servletRegistrationBean;
    }
    
    @Bean
    public FilterRegistrationBean<Filter> filterRegistrationBean() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>() ;
        filterRegistrationBean.setFilter(new WebStatFilter());
        //所有请求进行监控处理
        filterRegistrationBean.addUrlPatterns("/*"); 
        //添加不需要忽略的格式信息
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.css,/druid/*");
        return filterRegistrationBean ;
    }
    
}

2.2 DruidDataSourceUtil实现切换入口

需要进行切换时调用此方法即可,就会查看毛重是否存在,没有的新建,有的直接使用。

package net.cnki.common.datasource;

import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.druid.pool.DruidDataSource;

/**
 * 
 * @author ZhiPengyu
 * @ClassName:    [DruidDataSourceUtil.java]
 * @Description:  [用于查找并切换数据源]
 */
public class DruidDataSourceUtil {
	private static Logger logger = LoggerFactory.getLogger(DruidDataSourceUtil.class);
	
	public static void addOrChangeDataSource(String key,String dbip,String dbname,String dbuser,String dbpwd){
		DataSourceContextHolder.setDBType("default");

		/**
		 * 创建动态数据源
		 */
		Map<Object, Object> dataSourceMap = DynamicDataSource.getInstance().getDataSourceMap();
		if(!dataSourceMap.containsKey(key+"master") && null != key){
			logger.info("插入新数据库连接信息为:jdbc:mysql://"+dbip+":3306/"+dbname+"?serverTimezone=Hongkong&characterEncoding=UTF-8&useSSL=false");
			DruidDataSource dynamicDataSource = new DruidDataSource();
			dynamicDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
			
			dynamicDataSource.setUsername(dbuser);
			dynamicDataSource.setUrl("jdbc:mysql://"+dbip+":3306/"+dbname+"?serverTimezone=Hongkong&characterEncoding=UTF-8&useSSL=false&nullCatalogMeansCurrent=true&allowMultiQueries=true"
					);
			dynamicDataSource.setPassword(dbpwd);

			dynamicDataSource.setInitialSize(50);
			dynamicDataSource.setMinIdle(5);
			dynamicDataSource.setMaxActive(1000);
			dynamicDataSource.setMaxWait(5000);
			dynamicDataSource.setTimeBetweenEvictionRunsMillis(60000);
			dynamicDataSource.setMinEvictableIdleTimeMillis(300000);
			dynamicDataSource.setValidationQuery("SELECT 1 FROM DUAL");
			dynamicDataSource.setTestWhileIdle(true);
			dynamicDataSource.setTestOnBorrow(false);
			dynamicDataSource.setTestOnReturn(false);
			dynamicDataSource.setPoolPreparedStatements(true);
			dynamicDataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
			
			dynamicDataSource.setRemoveAbandoned(true);
			dynamicDataSource.setRemoveAbandonedTimeout(180);
			dynamicDataSource.setLogAbandoned(true);

			dataSourceMap.put(key+"master", dynamicDataSource);
			DynamicDataSource.getInstance().setTargetDataSources(dataSourceMap);
			//切换为动态数据源实例
			DataSourceContextHolder.setDBType(key+"master");
		}else{
			//切换为动态数据源实例
			DataSourceContextHolder.setDBType(key+"master");
		}
	}
	
}

三、切换

实现切换代码:

DruidDataSourceUtil.addOrChangeDataSource(key,dbip,dbname,dbuser,dbpwd);

我这里目前用到的实现有两种,原理一样,都是事先拦截,切换后执行。拦截有两种,用于不同情况。

3.1 全局切换

全局切换也就是每次访问都会切换数据源,不需要考虑到底是哪些接口。
继承OncePerRequestFilter,进行访问拦截
访问过滤-用于访问校验jwt、过滤url、切换数据源等等,我的项目使用的springsecurity并前后端分离处理,使用jwt替换了原session会话,所以处理内容较多,参考就可。至于用到的工具包全都是自己写的,没啥特殊的都是一些封装的,实在不知道的在留言吧

package net.cnki.security.filter;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import com.alibaba.fastjson.JSONObject;

import net.cnki.api.cnki.bean.CasDbBean;
import net.cnki.common.returned.ResponseUtil;
import net.cnki.common.returned.ResultCode;
import net.cnki.common.returned.ResultGenerator;
import net.cnki.common.datasource.DruidDataSourceUtil;
import net.cnki.common.redis.JedisUtils;
import net.cnki.common.redis.RedisConstants;
import net.cnki.security.jwt.JwtTokenUtil;
import net.cnki.util.AESUtil;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
	Logger logger = LoggerFactory.getLogger(this.getClass());
	
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
	ResultGenerator resultGenerator;
    @Autowired
    private PathMatcher pathMatcher;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
	JedisUtils jedisUtils;
    
    @Value("${jwt.token.header}")
    private String token_header;
    @Value("${jwt.token.type}")
    private String token_type;
    @Value("${jwt.token.passUrl}")
    private List<String> passUrl;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 
    		throws ServletException, IOException {
        String requestUrl = request.getRequestURI();
    	logger.info("["+requestUrl+"]访问校验jwt,并将用户角色信息写入内存!");
    	//判断URL是否需要验证
        Boolean flag = true;
        for(String url : passUrl){
            if(pathMatcher.match(url, requestUrl)){
                flag = false;
                break;
            }
        }
        
        //根据判断结果执行校验
        if (flag) {
        	String authHeader = request.getHeader(this.token_header);
            if (authHeader != null && authHeader.startsWith(this.token_type)) {
            	//获取token
                String authToken = authHeader.substring(this.token_type.length());
                if (!jwtTokenUtil.isTokenExpired(authToken)) {//无效token去更新
                	//根据token获取用户名
                    String username = jwtTokenUtil.getUserNameFromToken(authToken);
                    if (username != null) {
                    	String retoken = jedisUtils.get(username, RedisConstants.datebase1);
                    	if (StringUtils.isEmpty(retoken)) {
                    		logger.error("用户:"+username+" 访问url:["+requestUrl+"]校验失败,未登录!");
                            ResponseUtil.out(response, 402, resultGenerator.getFreeResult(ResultCode.LOGIN_NO).toString());
                            return;
    					}
                    	//获取用户对应数据源
                    	String dbStr = jedisUtils.get(username,RedisConstants.datebase2);
                    	if (dbStr != null) {
                    		String dbinfo = AESUtil.decryptPwd(dbStr);
                    		CasDbBean casDbBean = JSONObject.parseObject(dbinfo, CasDbBean.class);
                    		DruidDataSourceUtil.addOrChangeDataSource(casDbBean.getSchoolId(),casDbBean.getDbIp(),casDbBean.getDbName(),casDbBean.getDbUser(),casDbBean.getDbPassword());
    					}
                    	
                        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                        if (jwtTokenUtil.validateToken(authToken, userDetails) && !StringUtils.isEmpty(retoken)) {
                            //验证token是否有效
                            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                            SecurityContextHolder.getContext().setAuthentication(authentication);
                            
                            chain.doFilter(request, response);
                            return;
                        }
                    }
                } 
            }
		}else {//无需校验直接通过
			chain.doFilter(request, response);
            return;
		}
        logger.error("访问url:["+requestUrl+"]校验失败,无权访问!");
        ResponseUtil.out(response, 403, resultGenerator.getFreeResult(ResultCode.NO_PERMISSION).toString());
    }
    
}

3.2 局部切换

局部切换就是多数接口采用默认数据源或全局切换下的数据源,但是仍有部分接口或部分功能模块处理不同数据源的数据。
其实现也是有三种:

  1. 一种就像全局切换那样,配置指定的url路径咋子单独配置,适用功能模块的处理,接口较多,统一处理了。
  2. 其次就是,直接在接口里使用,旨在需要切换的时候执行就可以。但是需要注意,如果controller接口配置事务,不可采用此方法。
  3. 还有一种,也是此处要说的,也就是自定义注解,只需要在接口添加就行的。
    参考我的另一篇文章:SpringBoot自定义注解
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
好的,你的问题是关于如何在Spring Boot应用中实现多数据源动态切换,使用的技术包括Spring Boot、MyBatis、MySQL、Oracle、Druid数据源连接池、自定义注解和切面,并且配置文件使用application.yml格式。 首先,需要在pom.xml文件中添加相应的依赖: ```xml <!--Spring Boot--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--MyBatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <!--MySQL--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version> </dependency> <!--Oracle--> <dependency> <groupId>com.oracle.database.jdbc</groupId> <artifactId>ojdbc8</artifactId> <version>19.3.0.0</version> </dependency> <!--Druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> ``` 接下来,需要在application.yml文件中配置数据源和MyBatis相关的属性,例如: ```yaml spring: datasource: druid: # 数据源1 db1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/db1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root # 数据源2 db2: driver-class-name: oracle.jdbc.OracleDriver url: jdbc:oracle:thin:@localhost:1521:ORCL username: scott password: tiger # 默认数据源 url: jdbc:mysql://localhost:3306/db1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity ``` 然后,需要定义一个自定义注解,用于标识哪些方法需要使用哪个数据源: ```java @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String value() default "db1"; } ``` 在数据源切换的时候,我们需要获取注解上指定的数据源名称,因此需要定义一个切面: ```java @Aspect @Component public class DataSourceAspect { @Around("@annotation(ds)") public Object around(ProceedingJoinPoint point, DataSource ds) throws Throwable { String dataSourceName = ds.value(); DynamicDataSource.setDataSource(dataSourceName); try { return point.proceed(); } finally { DynamicDataSource.clearDataSource(); } } } ``` 最后,需要定义一个动态数据源,用于实现数据源切换: ```java public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<>(); @Override protected Object determineCurrentLookupKey() { return dataSourceHolder.get(); } public static void setDataSource(String dataSourceName) { dataSourceHolder.set(dataSourceName); } public static void clearDataSource() { dataSourceHolder.remove(); } } ``` 至此,多数据源动态切换的配置就完成了。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值