SpringBoot+Mybatis-plus不同用户动态切换数据源(一)

步骤如下

    • 导依赖
    • 主从数据源配置核心代码
    • 读取数据源配置信息(建两个类)
      • 主数据源类
      • 从数据源类
    • 线程安全控制添加
    • 动态切换数据源路由选择器
    • 组建数据源容器
      • 系统默认(主)数据源组装类
      • 创建连接池封装工具类
    • 使用Spring Aop切面拦截进行动态切换
    • 项目使用一段时间回顾一个坑
    • 联系博主方式
    • 扩展(javassist动态修改方法注解名称)

导依赖

        <!--mybatis主从数据源切换依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>2.5.6</version>
        </dependency>
        <!--连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.8</version>
        </dependency>
        <!--数据库链接自定义依赖-->
        <!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jdbc -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
            <version>8.0.1</version>
        </dependency>

主从数据源配置核心代码

#数据源配置
spring:
  #排除DruidDataSourceAutoConfigure(取消druid数据源自动注入)
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  datasource:
    dynamic:
      #设置默认的数据源或者数据源组,默认值即为master
      primary: master
      datasource:
      #为默认数据源起一个名字为 master
        master:
          url: jdbc:mysql://localhost:3306/tfenergy?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
          username: root
          password: root
          driver-class-name: com.mysql.jdbc.Driver
     #为从数据源起一个名字为 other
        other:
          url: jdbc:mysql://192.168.5.9:3306/tfenergy?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
          username: root
          password: root
          driver-class-name: com.mysql.jdbc.Driver

读取数据源配置信息(建两个类)

主数据源类

ConfigurationMaster

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
/**
 1. 默认数据源
 */
@Configuration
@Data
public class ConfigurationMaster {
	@Value("${spring.datasource.dynamic.datasource.master.url}")
	private String url;
	@Value("${spring.datasource.dynamic.datasource.master.username}")
	private String username;
	@Value("${spring.datasource.dynamic.datasource.master.password}")
	private String password;
	@Value("${spring.datasource.dynamic.datasource.master.driver-class-name}")
	private String driverClassName;
}

从数据源类

ConfigurationOther

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
/**
 * 其它数据源
 */
@Configuration
@Data
public class ConfigurationOther {
	@Value("${spring.datasource.dynamic.datasource.other.url}")
	private String url;
	@Value("${spring.datasource.dynamic.datasource.other.username}")
	private String username;
	@Value("${spring.datasource.dynamic.datasource.other.password}")
	private String password;
	@Value("${spring.datasource.dynamic.datasource.other.driver-class-name}")
	private String driverClassName;
}

线程安全控制添加

目的:当一个进程(线程)访问结束之后才进入下一个,提高线程安全

import org.springframework.stereotype.Component;
/**
 * 线程安全控制
 */
@Component
public class DataSourceContext {
	private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
	//参数为字符串表示主从数据源参数名称
	public static void setDataSource(String value) {
		contextHolder.set(value);
	}
	public static String getDataSource() {
		return contextHolder.get();
	}
	public static void clearDataSource() {
		contextHolder.remove();
	}
}

动态切换数据源路由选择器

Spring boot:提供AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源 由此在执行业务之前 即可实现可动态路由的数据源.它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

介绍一下AbstractRoutingDataSource类的部分核心方法
设置数据源(map):主键:数据源名称 value:数据源Datasource对象 setTargetDataSources()
在切面进行查询数据库数据源之后 生成一个新的Datasource对象,然后赋值进入targetDataSources 中去 ,然后重写AbstractRoutingDataSource的子类调用这个方法afterPropertiesSet()即可刷新 紧接着就可以进行切换数据源了

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 切换路由
 */
public class MultiRouteDataSource extends AbstractRoutingDataSource {
	@Override
	protected Object determineCurrentLookupKey() {
		//通过绑定线程的数据源上下文实现多数据源的动态切换
		return DataSourceContext.getDataSource();
	}
}

组建数据源容器

系统默认(主)数据源组装类

我们先将系统默认的数据源先添加入连接池中去 以便后续管理使用
注意:该类中我将重写mysql datasource 的数据源切换模式 目的就是让mysql获取连接时由我自己给它分配连接 从而得到切换数据源效果
属性提供支持类接口

package org.tfcloud.energy.utils;
@SuppressWarnings("all")
public interface ParamValueInstence {
	String DRIVER_CLASS="com.mysql.jdbc.Driver";
	String DRIVER_SERVER_CLASS="com.mysql.cj.jdbc.Driver";

}

主数据源封装类

package org.tfcloud.energy.config;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.tfcloud.energy.utils.ParamValueInstence;
import org.tfcloud.energy.utils.PoolDataSourceUtil;
import org.tfcloud.energy.utils.PoolDataSourceUtilTwo;
import java.util.HashMap;
import java.util.Map;
@Configuration
@Component
@SuppressWarnings("all")
public class DataSourceConponent {
	private final String driverClass= ParamValueInstence.DRIVER_SERVER_CLASS;
	@Autowired
	private ConfigurationMaster configurationMaster;

	//默认数据源----->从连接池获得---->为该数据源起名为 master  交由Spring容器管理
	//PoolDataSourceUtilTwo:这个类下面会提供源码即说明
	@Bean(name = "master")
	public DataSource masterDataSource() {
	DataSource dataSource=	PoolDataSourceUtilTwo.getPoolProperties(
		configurationMaster.getUrl(),
		configurationMaster.getUsername(),
		configurationMaster.getPassword());
		return dataSource;
	}	
	//自动装配时当出现多个Bean候选者时 被注解为@Primary的Bean将作为首选者 否则将抛出异常
	//这个就是重写了datasource的默认切换路由器,底层源码有显示 数据源是存在一个map中 所以这里我们
	//也给它赋值我们自己的默认数据源
	@Primary
	@Bean(name = "multiDataSource")
	public MultiRouteDataSource exampleRouteDataSource() throws Exception{
		MultiRouteDataSource multiDataSource = new MultiRouteDataSource();
		Map<Object, Object> targetDataSources = new HashMap<>();
		targetDataSources.put("master", masterDataSource());
		//将map中我们创建的默认数据源赋值
		multiDataSource.setTargetDataSources(targetDataSources);
		//以该默认数据源为默认访问数据库的链接使用
		multiDataSource.setDefaultTargetDataSource(masterDataSource());
		return multiDataSource;
	}
}

注意:
1.创建主数据源时需要重视,我们都知道mysql默认都有一个连接失效自动关闭机制,如果这里处理不当 你今天请求都是正常但是第二天就会出现下面这个问题

nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException:
No operations allowed after connection closed

错误信息说明:意思就是你访问了已经被mysql关闭的连接 所以这个连接mysql不允许访问 需要你重新创建 那么咱们既然是多数据源模式,就少不了对数据源的一个控制及管理,目的就是让连接该释放的释放,该关闭的关闭,这样才能保证系统的高可用性和稳定性 所以才有了上面那个类PoolDataSourceUtilTwo

创建连接池封装工具类

注意:我这个例子中使用的连接池为SpringBoot默认的连接池Tomcat-jdbc
**这里咱们创建两个工具类,为什么呢?
一:1个供默认连接数据源使用 也就是咱们的系统默认库;
二:1个供用户配置的私有数据源使用 **
两个工具类一模一样,你只需要改一下类名即可使用。

package org.tfcloud.energy.utils;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.apache.tomcat.jdbc.pool.PoolProperties;
import org.springframework.stereotype.Component;
import springfox.documentation.annotations.ApiIgnore;

@Component
@SuppressWarnings("all")
@ApiIgnore
public class PoolDataSourceUtil {
	//连接池属性支持
	private static PoolProperties poolProperties = new PoolProperties();
	//驱动替换--
	private static final String DRIVER_CLASS_NAME=ParamValueInstence.DRIVER_SERVER_CLASS;
	//初始化连接池 仅创建一次  这也就是说该连接只有一个连接池供使用  一个连接池就够咱们默认数据源使用了
	private  static DataSource dataSource=new DataSource();
	static {
		poolProperties.setMinIdle(10);
		poolProperties.setMaxActive(1000);
		//如果连接超时等待时间 -单位秒  这里设置为 一分钟
		poolProperties.setMaxWait(60000);
		/**
		 * Mysql  5版本以下默认在url加上autoReconnect=true即可解决 在连接失效时由mysql帮我们完成对连接的判断性和断开与重连问题
		 * 		  5版本以上新增机制默认8小时如果没有请求访问数据库连接 那么连接都将被mysql关闭 如果8小时之后再次访问,
		 * 		  就是访问了一些失效的并且已经关闭的连接 由此在这里配置多长时间进行检验一次连接是否失效-失效即释放以及重新构建连接
		 * 		  单位--毫秒 这里配置15分钟检验一次
		 */
		poolProperties.setTimeBetweenEvictionRunsMillis(1080000);
		//配置一个链接在池中最小生存时间,单位-毫秒
		poolProperties.setMinEvictableIdleTimeMillis(1080000);
		//验证链接是否有效,参数必须设置为非空字符串,下第三项为true即生效
		poolProperties.setValidationQuery("SELECT 1");
		//指明链接是否被空闲链接回收器回收如果有进行检验,如果失败即回收链接
		poolProperties.setTestWhileIdle(true);
		//检验链接是否有效,如果无效舍去重新获取新的链接
		poolProperties.setTestOnBorrow(true);
		//指明是否在归还到连接池中前进行检验
		poolProperties.setTestOnReturn(true);
	}
	//配置数据源链接属性支持
	public static DataSource getPoolProperties(String url,String userName,String password) {
		poolProperties.setUrl(url);
		poolProperties.setUsername(userName);
		poolProperties.setPassword(password);
		poolProperties.setDriverClassName(DRIVER_CLASS_NAME);
		//连接池 依赖属性pool
		dataSource.setPoolProperties(poolProperties);
		return dataSource;
	}
}

使用Spring Aop切面拦截进行动态切换

Ordered这个接口主要为如果你的切面不仅仅要拦截一层的话 那么你就可以根据getOrder()方法进行设置优先级切面执行顺序 返回值越小 优先级越高

package org.tfcloud.energy.config;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springblade.core.secure.utils.SecureUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.entity.Tenant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.tfcloud.energy.mapper.DataSourceMapper;
import org.tfcloud.energy.mongo.AbstractMongoDbConfig;
import org.tfcloud.energy.mongo.AbstractMongoDbConfigTwo;
import org.tfcloud.energy.utils.ParamValueInstence;
import org.tfcloud.energy.utils.PoolDataSourceUtil;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;


//注入Spring 容器
@Component
//开启切面拦截
@Aspect
@Configuration
public class DataSourceAspect implements Ordered {
	private static DataSource dataSource = null;
	@Autowired
	private DataSourceContext dataSourceContext;
	//引入我们重写的切换数据源路由器
	@Autowired
	//这个注解意思就是:只引入我们定义的那个@Bean名称为multiDataSource的路由
	@Qualifier(value = "multiDataSource")
	private MultiRouteDataSource multiRouteDataSource;
	@Autowired
	private DataSourceMapper dataSourceMapper;

	/**
	 * 以controller为切面
	 */
	@Pointcut("execution(* org.tfcloud.energy.controller..*(..)))")
	public void dataSourcePointcut() {
	}

	/**
	 * 业务执行前拦截处理切换数据源
	 * Mysql 和Mongodb
	 *
	 * @param joinPoint
	 * @throws Exception
	 */
	@SuppressWarnings("all")
	@Before(value = "dataSourcePointcut()")
	public void before(JoinPoint joinPoint) throws Exception {
		//获得当前租户信息-->注意这里我是封装的工具类获得当前用户的,其它你自己结合业务写
		String tenantCode = SecureUtil.getUser().getTenantCode();
		//数据库查询该用户是否启用私有库
		Tenant tenant = dataSourceMapper.findDataSourceByTenant(tenantCode);
		System.out.println("租户信息打印" + tenantCode.toString());
		//如果开启私有库
		if (Func.isNotEmpty(tenant.getIsPrivateDb()) && tenant.getIsPrivateDb() == 1) {
			Map<Object, Object> map = new HashMap<>();
			//组装访问数据库的url
			String url = "jdbc:mysql://" + tenant.getPrivateDbAddr() + ":" + tenant.getMysqlPort() + "/tfenergy?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true";
			//拿着数据源核心信息去我们创建的链接池中获取链接
			dataSource = PoolDataSourceUtil.getPoolProperties(url, tenant.getMysqlUser(), tenant.getMysqlPassword());
			map.put("other", dataSource);
			//放入路由选择器中
			multiRouteDataSource.setTargetDataSources(map);
			//更新动态切换数据源底层实现 手动注入
			multiRouteDataSource.afterPropertiesSet();
			//设置私有库数据源
			dataSourceContext.setDataSource("other");
		} else {
			dataSourceContext.setDataSource("master");
		}
	}
	/**
	 * 执行成功清除缓存
	 *
	 * @param joinPoint
	 * @throws Exception
	 */
	@SuppressWarnings("all")
	@After(value = "dataSourcePointcut()")
	public void after(JoinPoint joinPoint) throws Exception {
	//每个链接请求结束后,切面后置处理,如果该连接池不为空,回收刚才的链接
		if (Func.isNotEmpty(dataSource)) {
			dataSource.getConnection().close();
		}
		//清除缓存
		dataSourceContext.clearDataSource();
	}
	/**
	 * 定义优先执行顺序  数值越小 最先执行
	 *
	 * @return
	 */
	@Override
	public int getOrder() {
		return 1;
	}
}

扩展:如果你在业务中处理时 需要访问默认库的时候,因为切面执行完毕之后就已经对该用户是否切换私有库访问已经做了处理 所以后面如果业务特殊 需要默认库和私有库两个一块儿查询,这个时候我告诉大家解决办法如下
只要在业务中注入DataSourceContext这个类 即可
切换时 直接调用方法 dataSourceContext.setDataSource(“数据源”)即可。

项目使用一段时间回顾一个坑

如果是新增或修改得业务,咱们肯定要考虑事务吧,那么这个时候我得场景就来了.往下看

执行流程:例如当前用户开启私有库进来,由我们得切面直接进行切换私有库,走到业务层,方法上肯定我们会添加@Transactional注解来支持事务回衮保障数据安全性,那么这个时候你再在这里面去任意切换私有或公有库得时候,就会出问题了(你会发现所有得切换都没生效,一直走得数据源就是从切面过来时切换得那个数据源)

  • 正确切换?
    我们直接在controller进行处理,直接进行切换即可,
    例:

    SpringBoot+Mybatis-plus不同用户动态切换数据源(一)_第1张图片
    想必大家会问了,比如我在业务里面也需要两个源来回切换呢?怎么处理呢?

我的实例解决办法:因为我们是分布式开发,一般以服务为标识,所以涉及到基础库或业务库的东西我们都是分开的,我这里是使用Feign远程调用执行的方式进行解决,feign调用相当于调了一个http请求,有它自己的url,切面不会走,那么数据源也肯定不会重,每个服务都对应了一个连接池及数据源,所以我这种方式可行并且我在项目中也已经使用一段时间了

其它方式解决办法呢?

要说声抱歉,我目前没有发现其它办法,我对源码的阅读及理解能力还是比较欠缺的,大家可以去参考一下资料解决,当然,如果你是微服务直接开箱即用即可~~

联系博主方式

到此根据用户动态切换数据源已经完成,如果对以上代码有任何疑问都可以联系我
QQ:2509647976
微信: x331191249

扩展(javassist动态修改方法注解名称)

例如我要拦截的是service层的在方法上面的注解
代码如下

@Service
@AllArgsConstructor
public class CompileShowServiceImpl  implements CompileShowService  {
    private JdbcTemplate jdbcTemplate;
    /**
     * 我要修改的就是这个注解值,那么肯定有人问你为什么要修改这个注解值呢?下面我会介绍
     * @param
     * @return
     */
    @DS("master")
    public String findAtimerByName() {//54
        System.out.println("执行");
        String sql="select meter_name  from e_dayenergy where id =1";
        return jdbcTemplate.queryForObject(sql,String.class);
    }
}

代码添加依然在aop切面的@Before()方法体里面进行业务处理

 Object target = joinPoint.getTarget();
        // String name = joinPoint.getSignature().getName();
        //.forName("com.itxwl.getoutserver.service.impl.CompileShowServiceImpl")
        Class<?> aClass1 = target.getClass();
        //新建类
        ClassPool classPool=ClassPool.getDefault();
        CtClass ctClass = classPool.get(aClass1.getName());
        ClassFile classFile = ctClass.getClassFile();
        List methods1 = classFile.getMethods();

        String name = aClass1.getName();
        System.out.println("类路径" + name);
        Class aClass = aClass1.getClass().forName(name);
        //得到类下得所有方法

        Method[] methods = aClass.getDeclaredMethods();
        for (Method method : methods) {
            //获得注解
            DS ds = method.getAnnotation(DS.class);
            //修改属性值起
            InvocationHandler invocationHandler = Proxy.getInvocationHandler(ds);
            Field f = invocationHandler.getClass().getDeclaredField("memberValues");
            //设置可修改权限
            f.setAccessible(true);
            Map<String, Object> memberValues = (Map<String, Object>) f.get(invocationHandler);
            //获得注解属性~
            String val = (String) memberValues.get("value");
            System.out.println("改变前:" + val);
            if (val.equals("other")) {
                memberValues.put("value", "mather");
            } else {
                //覆盖之前属性值进行修改
                memberValues.put("value", "other");
            }
            System.out.println("改变后" + memberValues.get("value"));
            Method[] declaredMethods = ds.annotationType().getDeclaredMethods();
            for (Method method1 : declaredMethods) {
                String invoke = (String) method1.invoke(ds, null);
                System.out.println(invoke);
            }
        }

最初我的想法是利用aop拦截这个@DS注解进行动态切换数据源,后来试了3天 进坑了三天没有一点进展,
之后发现,当你切面拦截这个@DS注解值,java的机制就已经将你这个类进行类加载了(也就是说将这个类的全部的代码转换字节码添加入jvm内存当中),所以即使我在切面进行修改注解值成功 那么service层该方法运行时依然不会改变最初的那个注解值,这也就是我这3天的体会,网上的资料基本都是测试环境 所以在此告诉大家少走弯路,~~

你可能感兴趣的:(微服务)