精华内容
下载资源
问答
  • Springboot+Druid动态切换数据源一、描述二、实现2.1 maven引入jar2.2 数据源上下文DataSourceContextHolder2.3 DynamicDataSource继承AbstractRoutingDataSource抽象类2.4 Druid配置类2.2 DruidDataSourceUtil实现...

    一、描述

    关于数据源的切换,在实际使用中经常出现,本文主要是使用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自定义注解
    展开全文
  • SpringBoot集成Druid 动态切换数据源 主要逻辑 通过继承 AbstractRoutingDataSource 类覆写其 determineCurrentLookupKey() 方法来设置数据源,动态切换则基于aop实现。通过自定义一个注解,传入我们需要的数据源...

    SpringBoot集成Druid 动态切换数据源

    本人菜鸟一个,文章也是参考了网上的教程后自己项目用了随便写的笔记,如果有错误还请指正。

    主要逻辑

    通过继承 AbstractRoutingDataSource 类覆写其 determineCurrentLookupKey() 方法来设置数据源,动态切换则基于aop实现。通过自定义一个注解,传入我们需要的数据源名称,在切面中获取到数据源名称并将其存储到当前线程的私有内存(ThreadLocal)中。在调用方法时读取当前线程存储的数据源名称,并根据这个名称去设置数据源.

    依赖:

    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.2.13.RELEASE</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
            
        <dependecies>
             <dependency>
    			<groupId>com.alibaba</groupId>
    			<artifactId>druid-spring-boot-starter</artifactId>
    			<version>1.2.6</version>
    		</dependency>
            <dependency>
    			<groupId>mysql</groupId>
    			<artifactId>mysql-connector-java</artifactId>
    		</dependency>
            <dependency>
    			<groupId>tk.mybatis</groupId>
    			<artifactId>mapper-spring-boot-starter</artifactId>
    			<version>2.0.0</version>
    		</dependency>
        </dependcies>
    
    

    yml配置文件

    server:
      port: 9192
      servlet:
        context-path: /
    
    spring:
      application:
        name: test
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        druid:
          # 主库数据源
          master:
            url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
            username: root
            password: 123456
          slave:
            url: jdbc:mysql://xxxxxxxxx:xxxx/xxx?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
            username: root
            password: 123456
            enabled: true
          # 初始连接数
          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: root
            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
    
    

    详细过程

    自定义注解,用于声明所使用的数据源

    /**
     * 标识数据源
     * @author Administrator
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DataSource {
    
        /**
         * 数据源
         */
        public String value();
    }
    

    该注解应该标记在方法上,value的值就是需要使用的数据源名称,为了方便和统一名称,再定义一个枚举类用于记录所有数据源

    /**
     * @author hzw
     * @Description
     * @date 2021-07-13 17:49
     */
    public enum  DataSourceEnum {
        /**
         * 主数据源
         */
        MASTER,
    
        /**
         * 从数据源
         */
        SLAVE;
    }
    

    接着可以先去定义一个中间类,用于动态的获取当前线程设置的数据源,这个类中定义一个ThreadLocal常量,ThreadLocal的值就是当前线程所使用的数据源名称

    /**
     * @author hzw
     * @Description
     * @date 2021-07-13 17:59
     */
    public class DataSourceHolder {
    
        public static final ThreadLocal<String> DATA_SOURCE = new ThreadLocal<>();
    
        public static void setDataSource(String name) {
            DATA_SOURCE.set(name);
        }
    
        public static void clear() {
            DATA_SOURCE.remove();
        }
    
        public static String getDataSourceName() {
            return DATA_SOURCE.get();
        }
    
    }
    

    setDataSource()getDataSourceName()用于设置和获取数据源名称。clear()方法用于在每次调用方法结束后清空当前线程设置的数据源名称。

    接着就是关键,定义一个类去继承AbstractRoutingDataSource类,这个类的介绍如下:

     * Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
     * calls to one of various target DataSources based on a lookup key. The latter is usually
     * (but not necessarily) determined through some thread-bound transaction context.
     *
    

    意思就是一个抽象的DataSource实现,可以通过lookup key来去寻找一个合适的数据源。

    具体去寻找的方法是determineTargetDataSource(),这个方法根据一个给定键去寻找目标数据源,而这个键就是通过determineCurrentLookupKey()方法来给定,但是determineCurrentLookupKey()这个方法在这个类中没有具体实现,于是我们要去实现这个方法:

    /**
     * @author hzw
     * @Description
     * @date 2021-07-13 19:24
     */
    public class DataSourceRouting extends AbstractRoutingDataSource {
    
        public DataSourceRouting(DataSource defaultDataSource, Map<Object, Object> dataSource) {
            super.setDefaultTargetDataSource(defaultDataSource);
            super.setTargetDataSources(dataSource);
            super.afterPropertiesSet();
        }
    
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceHolder.getDataSourceName();
    
        }
    }
    
    

    实现的方式也很简单,就是通过DataSourceHolder去把当前线程设置的数据源名称返回就行了。至于具体的怎么去根据返回的数据源名称去设置我们要的数据源,简单来说就是在这个DataSourceRouting的构造方法中,第一个参数传入的是默认的数据源,当ThreadLocal中没有数据源名称时就会采用默认数据源。第二个参数是一个Map,里面的键就是数据源名称,值就是数据源对象DataSourcedetermineTargetDataSource()就是在这个Map里去找有没有key是这个数据源名称的数据源。有兴趣可以去看看源码,这个类的方法不多。

    接下来就是Durid数据源的配置类,区别与单数据源,多数据源需要构造多个DataSource对象以便进行切换(反正交给Spring就完事了),此处构造DataSource对象时采用一个外部配置类接收yml文件中的配置并提供实例化方法。

    配置如下:

    /**
     * @author hzw
     * @Description
     * @date 2021-07-13 20:06
     */
    @Component
    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.setMaxActive(maxActive);
            datasource.setMinIdle(minIdle);
    
            /** 配置获取连接等待超时的时间 */
            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;
        }
    }
    
    /**
     * @author hzw
     * @Description Druid连接连接池配置
     * @date 2021-07-13 19:38
     */
    @Configuration
    public class DruidDataSourceConfig {
    
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.druid.master")
        public DataSource masterDataSource(DruidProperties druidProperties) {
            DruidDataSource build = DruidDataSourceBuilder.create().build();
            return druidProperties.dataSource(build);
        }
    
        @Bean
        @ConfigurationProperties("spring.datasource.druid.slave")
        @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
        public DataSource slaveDataSource(DruidProperties druidProperties) {
            DruidDataSource build = DruidDataSourceBuilder.create().build();
            return druidProperties.dataSource(build);
        }
    
        @Bean(name = "dynamicDataSource")
        @Primary
        public DataSourceRouting dataSourceRouting(DataSource masterDataSource) {
            Map<Object, Object> targetDataSources = new HashMap<>(5);
            setDataSource(targetDataSources, DataSourceEnum.MASTER.name(),"masterDataSource");
            setDataSource(targetDataSources, DataSourceEnum.SLAVE.name(),"slaveDataSource");
            return new DataSourceRouting(masterDataSource, targetDataSources);
        }
    
        public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
            targetDataSources.put(sourceName, SpringUtils.getBean(beanName));
        }
    
    }
    

    前面两个方法分别返回主从数据源实例添加到Spring容器中进行管理,第三个方法是将我们刚刚编写的DataSourceRouting类添加到Spring容器中,在方法中把主从数据源的实例都通过构造函数传递进去,这样后续就可以对这两个实例进行切换。

    SpringUtils代码如下:

    @Component
    public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware {
        /**
         * Spring应用上下文环境
         */
        private static ConfigurableListableBeanFactory beanFactory;
    
        private static ApplicationContext applicationContext;
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            SpringUtils.beanFactory = beanFactory;
        }
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            SpringUtils.applicationContext = applicationContext;
        }
    
        /**
         * 获取对象
         *
         * @param name
         * @return Object 一个以所给名字注册的bean的实例
         * @throws org.springframework.beans.BeansException
         */
        @SuppressWarnings("unchecked")
        public static <T> T getBean(String name) throws BeansException {
            return (T) beanFactory.getBean(name);
        }
    
        /**
         * 获取类型为requiredType的对象
         *
         * @param clz
         * @return
         * @throws org.springframework.beans.BeansException
         */
        public static <T> T getBean(Class<T> clz) throws BeansException {
            T result = (T) beanFactory.getBean(clz);
            return result;
        }
    }
    

    重点:在Spring启动类上添加:

    @Import({DruidDataSourceConfig.class})
    @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
    

    作用是排除掉Springboot自带的数据源自动配置,如果不添加这些,就会在启动的时候报循环依赖的错误。(折腾了我三个小时 -.-)

    最后编写切面类

    /**
     * @author hzw
     * @Description 数据源切面
     * @date 2021-07-13 17:53
     */
    @Aspect
    @Component
    public class DataSourceAspect {
    
        @Pointcut("@annotation(com.terabits.datainteraction.config.DataSource)"
                + "|| @within(com.terabits.datainteraction.config.DataSource)")
        public void pointCut() {};
    
        @Around("pointCut()")
        public Object changeDataSource(ProceedingJoinPoint pjp) {
            DataSource dataSource = getDataSource(pjp);
            String value = dataSource.value();
            DataSourceHolder.setDataSource(value);
            Object proceed = null;
            try {
                proceed = pjp.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            } finally {
                //清空数据源
                DataSourceHolder.clear();
            }
            return proceed;
    
        }
    
        /**
         * 获取需要切换的数据源
         */
        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注解修饰的方法执行前,首先先获取注解上的数据源名称,并通过DataSourceHolder将这个名称保存到当前线程的ThreadLocal中。在与数据库建立连接时,会先执行我们自定义的DataSourceRouting中的方法进行数据源的动态选择,于是,执行方法时通过数据源名称去选择的数据源就是注解上所设置的数据源。注意要在finally中对ThreadLocal中的数据源进行清空,相当于将数据源置为默认值。

    补充一下遇到的问题解决方式:

    在一个类中调用同类中一个带有自定义数据源注解(@DataSource)的方法,无法触发切面切换数据源
    这个问题的原因是spring注入的bean是有代理对象和原始对象这个区别的,简单来说就是遇到这种情况下,当前类的注入的bean并不是原始对象而是代理对象。详情可参照https://segmentfault.com/a/1190000008379179。解决方法就是文章里面说的,在当前类中把当前类在注入进来(我用我自己),然后用这个注入的对象去调用带数据源注解的方法。

    切面触发晚于数据源切换
    按上面的方式去解决无法触发切面的问题时,如果当前类或者当前方法上带有@Transactional注解,那么切面的触发时间会在切换数据源之后,这样就导致根本没办法把数据源设置为当前注解上的值。
    原因是因为@Transactional本身也是通过aop实现的,而且他的执行顺序默认比我们设置的切面要早。解决方式就是在自定义的切面上增加一个@Order(1)的注解,这样就会保证执行顺序早于@Transactional了。
    ps:但我不知道有啥后遗症

    展开全文
  • 操作数据一般都是在DAO层进行处理, 1、可以选择直接使用JDBC进行编程 2、使用多个DataSource 然后创建多个SessionFactory,在使用Dao层的时候通过不同的SessionFactory进行处理,不过这样的入侵性比较明显,一般的...

    操作数据一般都是在DAO层进行处理,

    1、可以选择直接使用JDBC进行编程
    2、使用多个DataSource 然后创建多个SessionFactory,在使用Dao层的时候通过不同的SessionFactory进行处理,不过这样的入侵性比较明显,一般的情况下我们都是使用继承HibernateSupportDao进行封装了的处理,如果多个SessionFactory这样处理就是比较的麻烦了,修改的地方估计也是蛮多的
    3、使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源,这样的入侵性较低,非常好的满足使用的需求。比如我们希望对于读写分离或者其他的数据同步的业务场景

     

     

     

     

    单数据源的场景(一般的Web项目工程这样配置进行处理,就已经比较能够满足我们的业务需求)
    多数据源多SessionFactory这样的场景,估计作为刚刚开始想象想处理在使用框架的情况下处理业务,配置多个SessionFactory,然后在Dao层中对于特定的请求,通过特定的SessionFactory即可处理实现这样的业务需求,不过这样的处理带来了很多的不便之处,所有很多情况下我们宁愿直接使用封装的JDBC编程,或者使用Mybatis处理这样的业务场景

    ●使用AbstractRoutingDataSource 的实现类,进行灵活的切换,可以通过AOP或者手动编程设置当前的DataSource,不用修改我们编写的对于继承HibernateSupportDao的实现类的修改,这样的编写方式比较好,至于其中的实现原理,让我细细到来。我们想看看如何去应用,实现原理慢慢的说!

    ●编写AbstractRoutingDataSource的实现类,HandlerDataSource就是提供给我们动态选择数据源的数据的信息,我们这里编写一个根据当前线程来选择数据源,然后通过AOP拦截特定的注解,设置当前的数据源信息,也可以手动的设置当前的数据源,在编程的类中。

     

     

     

    Spring2.x的版本中采用Proxy模式,就是我们在方案中实现一个虚拟的数据源,并且用它来封装数据源选择逻辑,这样就可以有效地将数据源选择逻辑从Client中分离出来。Client提供选择所需的上下文(因为这是Client所知道的),由虚拟的DataSource根据Client提供的上下文来实现数据源的选择。 
    具体的实现就是,虚拟的DataSource仅需继承AbstractRoutingDataSource实现determineCurrentLookupKey()在其中封装数据源的选择逻辑

    一、原理

    首先看下AbstractRoutingDataSource类结构,继承了AbstractDataSource:

     

     

     

    既然是AbstractDataSource,当然就是javax.sql.DataSource的子类,于是我们自然地回去看它的getConnection方法:

    原来关键就在determineTargetDataSource()里:

     

    这里用到了我们需要进行实现的抽象方法determineCurrentLookupKey(),该方法返回需要使用的DataSource的key值,然后根据这个key从resolvedDataSources这个map里取出对应的DataSource,如果找不到,则用默认的resolvedDefaultDataSource。

     

    所以我们需要继承AbstractRoutingDataSource实现determineCurrentLookupKey方法来决定使用哪个数据连接池。

     

     

    具体详细步骤可参考

    spring-boot整合mybatis和druid连接池(动态数据源——读写分离)

    参考:https://blog.csdn.net/qq_27840695/article/details/83543968 
     

    展开全文
  • 需要修改 druid 的DruidPooledStatement 因为: druid 没有给conn属性写set方法,而是做成了受保护的类型,无法切换,需要提供get set 方法
  • DruidDataSource动态切换数据源

    千次阅读 2018-04-12 08:59:17
    druidDataSource = (DruidDataSource)DruidDataSourceFactory.createDataSource(properties);   } catch (Exception e) {   e.printStackTrace();   }  dbPoolConnection = new DBPoolConnection();   ...
    package com.gw.wx.util;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.sql.SQLException;
    import java.util.Date;
    import java.util.Map;
    import java.util.Properties;
    import com.alibaba.druid.pool.DruidDataSource;
    import com.alibaba.druid.pool.DruidDataSourceFactory;
    import com.alibaba.druid.pool.DruidPooledConnection;
    import com.gw.entity.Wxbridgetoken;


    public class DBPoolConnection {

    private static DBPoolConnection dbPoolConnection = null;
        private static  DruidDataSource druidDataSource = null;
        
        /**
         * 数据库连接池单例
         * @return
         */
        public static synchronized DBPoolConnection getInstance(String url ,String username, String password){
        Properties properties = loadPropertiesFile("/db_server.properties");
        properties.setProperty("url", url);
        properties.setProperty("username", username);
        properties.setProperty("password", password);
       
        try {
        druidDataSource = (DruidDataSource)DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
        e.printStackTrace();
        }
            dbPoolConnection = new DBPoolConnection();
            
            return dbPoolConnection;
        }


        /**
         * 返回druid数据库连接
         * @return
         * @throws SQLException
         */
        public static DruidPooledConnection getConnection(Wxbridgetoken bean) throws SQLException{
        DBPoolConnection dbp = new DBPoolConnection();
        Map<String, Object> dbMap = Application.getDBMap();
        dbp =  (DBPoolConnection) dbMap.get(bean.getAPPID());
        if(dbp == null ){
        dbp = DBPoolConnection.getInstance(bean.getDBUrl(), bean.getUser(), bean.getPassword());
        dbMap.put(bean.getAPPID(), dbp);
        }
            return dbp.druidDataSource.getConnection();
        }
        /**
         * @param string 配置文件名
         * @return Properties对象
         */
           private static Properties loadPropertiesFile(String fullFile) {
               Properties props = new Properties();
    try(InputStream ins = ReadConfig.class.getResourceAsStream(fullFile)) {
    props.load(ins);
    return props;
    } catch (IOException ex) {
    // ex.printStackTrace();
    return null;

        }

    }


    -----------------------------------------------------------------------------------------------------------------------------


    package com.gw.wx.util;


    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Properties;


    /**
     * 通过读取sysConfig.properties中的配置信息
     * 
     */
    public class ReadConfig {
    private static String value;
    private static String configFilePath = "/db_server.properties";


    /**
    * 根据key读取value
    * @param key
    * @return
    */
    public static String getInfo(String key) {
    Properties props = new Properties();
    try(InputStream ins = ReadConfig.class.getResourceAsStream(configFilePath);) {
    props.load(ins);
    if (props.containsKey(key)) {
    value = props.getProperty(key).trim();
    } else {
    return "";
    }
    } catch (IOException ex) {
    // ex.printStackTrace();
    return "";
    }
    return value;
    }
    }

    展开全文
  • Spring Boot2.x + Druid动态数据源切换 1、数据源配置 <!-- alibaba的druid数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-...
  • spring + druid 配置动态配置数据源以及多数据源切换功能实现 数据源连接池使用druid 其他的数据源基本原理相同 spring中配置默认数据源连接池如下: <!-- 数据源配置, 使用 BoneCP 数据库连接池 --> &...
  • druid多数据源 AOP注解切换数据源 GITEE代码
  • 1. **多DataSource + 多SqlSessionFactory** 在使用Dao层的...2. **使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源**, 基于这种方式,不仅可是实现真正意义上的`多数据源
  • Spring Boot + Spring JPA + JDBC + Druid实现动态数据源切换 目录 Spring Boot + Spring JPA + JDBC + Druid实现动态数据源切换 AbstractRoutingDataSource源码分析 需求代码实现 DynamicDataSource ...
  • 首先Springboot+Mybatis+druid动态数据源的配置是这样的 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @MapperScan(basePackages = "com.xxx.xxx.mapper") @Import({...
  • version} org.projectlombok lombok 1.18.12 1.1 yml文件配置数据源的实例 spring: datasource: druid: complex: platform: mysql url: jdbc:mysql://localhost:3306/complex_bus?useUnicode=true&characterEncoding...
  • 项目中经常会有集成其他数据库的情况,我们项目是使用spring Boot+Druid+Mybatis Plus开发,本文简述在项目通过AOP的方式动态切换数据库 版本号 框架 版本号 maven依赖 druid 1.1.10 <groupId>...
  • /** * 设置默认数据源和其他数据源 * @param defaultTargetDataSource * @param targetDataSources */ public DynamicDataSource(DruidDataSource defaultTargetDataSource, Map, Object> targetDataSources) { ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 8,682
精华内容 3,472
关键字:

druid动态切换数据源