SpringBoot+Mybatis-plus不同用戶動態切換數據源(一)

導依賴

        <!--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(“數據源”)即可。

聯繫博主方式

到此根據用戶動態切換數據源已經完成,如果對以上代碼有任何疑問都可以聯繫我
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天的體會,網上的資料基本都是測試環境 所以在此告訴大家少走彎路,~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章