mybatisに基づく読み取りと書き込みの分離、複数のデータソースの自動ルーティング

目次

1.なぜこれを行うのですか?

2.どうすればよいですか?

第三に、使用される技術

4、使用

1.まず、データベース操作の前にインターセプトするインターセプターを定義します

2.次に、xml構成ファイルでアスペクトを定義します

3. sqlを実行するために現在のスレッドのdaoクラス情報を取得した後、それをThreadLocalオブジェクトに配置し、ルートを選択するときに使用します。


1.なぜこれを行うのですか?

  1. 実際の同時実行性の高いプロジェクトでは、単一のライブラリのプレッシャーは非常に高くなります。このとき、データベースのマスタースレーブ構造を導入する必要があります。(サブデータベースサブテーブルまたはデータベースクラスターの場合は、別の話です)。
  2. マイクロサービスは完全に分割されていないか、単一のアプリケーションではないため、複数のデータにアクセスする必要があります

2.どうすればよいですか?

以前のアプローチは、構成ファイルで複数のデータソースを定義し、追加、削除、変更、およびチェックに基づいてdaoで異なるデータソースを選択することでした。そうすることで、コード内のデータソースを選択するプロセスをハードコーディングする必要があるかもしれません。これは明らかに十分に友好的ではなく、冗長で重複したコードを大量に生成します。

私たちのプログラムがルーティングデータソースを自動的に選択することは可能ですか?私のコードは以前と同じですが、ビジネスロジックのみに関係し、選択方法については、すべてを実装するフレームワークに任されています。これを行うと、コードが一瞬ではるかに明確になったと思いますか?次に、実装を段階的に見ていきましょう。

第三に、使用される技術

  1. Springによって提供されるAbstractRoutingDataSourceクラス。このクラスは複数のデータソースの下にあり、determineCurrentLookupKey()メソッドによって返されるルーティングキーに従ってデータソースを動的に選択します
  2. 春のAOP。sqlメソッドを実行する前にリクエストをインターセプトし、スレッドリクエストのメソッド、クラス名、パラメーター、およびその他の情報をスレッドローカル変数(ThreadLocal)に設定する必要があります。その後、determineCurrentLookupKeyはスレッドデータに従ってデータソースを動的に選択できます。 。
  3. spring-boot-autoConfiguration。構成されたBeanは自動的にロードできます。構成ファイルを手動で書き込む必要はありません

4、使用

1.まず、データベース操作の前にインターセプトするインターセプターを定義します

以下の完全なコードは私のgithubにあります。https//github.com/terry2870/hp-springbootを参照してください。

 

クラスDAOMethodInterceptorHandleを定義して、MethodInterceptorインターフェイスを実装します。完全なコードは次のとおりです。

package com.hp.springboot.database.interceptor;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.hp.springboot.database.bean.DAOInterfaceInfoBean;
import com.hp.springboot.database.bean.DAOInterfaceInfoBean.DBDelayInfo;
import com.hp.springboot.threadprofile.profile.ThreadProfile;

/**
 * 
 * 描述:执行数据库操作之前拦截请求,记录当前线程信息
 * 之所以用抽象类,是因为可以扩展选择持久层框架。可以选择mybatis或jdbcTemplate,又或者hibernate
 * 作者:黄平
 * 时间:2018年4月11日
 */
public abstract class DAOMethodInterceptorHandle implements MethodInterceptor {

	private static Logger log = LoggerFactory.getLogger(DAOMethodInterceptorHandle.class);
	
	/**
	 * 存放当前执行线程的一些信息
	 */
	private static ThreadLocal<DAOInterfaceInfoBean> routeKey = new ThreadLocal<>();
	
	/**
	 * 最大数据库查询时间(超过这个时间,就会打印一个告警日志)
	 */
	private static final long MAX_DB_DELAY_TIME = 10L;
	
	/**
	 * 获取dao操作的对象,方法等
	 * @param invocation
	 * @return
	 */
	public abstract DAOInterfaceInfoBean getDAOInterfaceInfoBean(MethodInvocation invocation);
	
	/**
	 * 获取当前线程的数据源路由的key
	 */
	public static DAOInterfaceInfoBean getRouteDAOInfo() {
		return routeKey.get();
	}
	
	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		//获取dao的操作方法,参数等信息,并设置到线程变量里
		this.setRouteDAOInfo(getDAOInterfaceInfoBean(invocation));
		
		//设置进入查询,记录线程执行时长
		entry();
		Object obj = null;
		try {
			//执行实际方法
			obj = invocation.proceed();
			return obj;
		} catch (Exception e) {
			throw  e;
		} finally {
			//退出查询
			exit();
			
			//避免内存溢出,释放当前线程的数据
			this.removeRouteDAOInfo();
		}
	}
	
	/**
	 * 进入查询
	 */
	private void entry() {
		DAOInterfaceInfoBean bean = getRouteDAOInfo();
		//加入到我们的线程调用堆栈里面,可以统计线程调用时间
		ThreadProfile.enter(bean.getMapperNamespace(), bean.getStatementId());
		DBDelayInfo delay = bean.new DBDelayInfo();
		delay.setBeginTime(System.currentTimeMillis());
		bean.setDelay(delay);
	}
	
	/**
	 * 结束查询
	 */
	private void exit() {
		DAOInterfaceInfoBean bean = getRouteDAOInfo();
		DBDelayInfo delay = bean.getDelay();
		delay.setEndTime(System.currentTimeMillis());
		ThreadProfile.exit();
		//输出查询数据库的时间
		if (delay.getEndTime() - delay.getBeginTime() >= MAX_DB_DELAY_TIME) {
			log.warn("execute db expire time. {}", delay);
		}
		
	}
	
	/**
	 * 绑定当前线程数据源路由的key 使用完成后必须调用removeRouteKey()方法删除
	 */
	private void setRouteDAOInfo(DAOInterfaceInfoBean key) {
		routeKey.set(key);
	}

	/**
	 * 删除与当前线程绑定的数据源路由的key
	 */
	private void removeRouteDAOInfo() {
		routeKey.remove();
	}
}

ここでは、getDAOInterfaceInfoBeanを使用して、現在のスレッドによって呼び出されたメソッド、パラメーター、署名などの情報を取得します。ここでmybatisの実現を提供します。次のように

/**
 * 
 */
package com.hp.springboot.mybatis.interceptor;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.util.ClassUtils;

import com.hp.springboot.database.bean.DAOInterfaceInfoBean;
import com.hp.springboot.database.interceptor.DAOMethodInterceptorHandle;

/**
 * @author huangping
 * Jul 14, 2020
 */
public class MyBatisDAOMethodInterceptorHandle extends DAOMethodInterceptorHandle {

	@Override
	public DAOInterfaceInfoBean getDAOInterfaceInfoBean(MethodInvocation invocation) {
		DAOInterfaceInfoBean bean = new DAOInterfaceInfoBean();
		
		// 获取当前类的信息(由于我们使用的mybatis,这里获取到的是spring的代理类信息)
		Class<?> clazz = invocation.getThis().getClass();
		
		// 这里获取的才是我们定义的dao接口对象
		Class<?>[] targetInterfaces = ClassUtils.getAllInterfacesForClass(clazz, clazz.getClassLoader());
		
		// 获取该类的父类(该操作暂时没用使用到)
		Class<?>[] parentClass = targetInterfaces[0].getInterfaces();
		if (ArrayUtils.isNotEmpty(parentClass)) {
			bean.setParentClassName(parentClass[0]);
		}
		
		// 设置类名信息
		bean.setClassName(targetInterfaces[0]);
		
		// 设置方法的类信息
		bean.setMapperNamespace(targetInterfaces[0].getName());
		
		// 设置方法名
		bean.setStatementId(invocation.getMethod().getName());
		
		// 设置方法参数
		bean.setParameters(invocation.getArguments());
		return bean;
	}
}

2.次に、xml構成ファイルでアスペクトを定義します

このように、インターセプターは効果的です

 


3. sqlを実行するために現在のスレッドのdaoクラス情報を取得した後、それをThreadLocalオブジェクトに配置し、ルートを選択するときに使用します。

OK、自動ルートを定義します。実行手順は次のとおりです。

  1. サービスの開始時に、データベース構成情報をロードし、マスタースレーブデータベース構成情報を読み取ります
  2. すべてのデータソース情報は、後で動的に選択できるように、AbstractRoutingDataSourceのtargetDataSourcesにロードされます。
  3. SQLを実行する前に、データソースルーティングを動的に選択します

最初にどのデータベースを選択し、次にマスターとスレーブを選択します。動的ルーティング手順:

  1. 前のインターセプターによってインターセプトされた現在のスレッド実行のメソッド情報を取得します
  2. daoのclassNameに従って、データソースからデータソースを取得します(つまり、databases.ymlにdao用に構成された別のデータソースがあるかどうか)。
  3. 存在する場合は、指定されたデータソースを使用します。存在しない場合は、デフォルト(つまり、最初のデータソース)のデータソースを使用します。
  4. 以前に取得したのはデータソースのプレフィックスのみであるため、以下では実際のデータソースに対応するキーを取得する必要があります。
  5. クエリメソッドの名前に従って、ライブラリを読み取るか、ライブラリを書き込むかを決定します。(これにより、メソッドの前にForceMasterコメントがあるかどうかが考慮されます)
  6. 最終的な実際のデータソースのキーを返し、実際のデータソースを取得します

対応するコードは次のとおりです。

/**
 * 
 */
package com.hp.springboot.database.datasource;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import com.hp.springboot.database.bean.DAOInterfaceInfoBean;
import com.hp.springboot.database.bean.DatabaseConfigProperties;
import com.hp.springboot.database.bean.DatabaseConfigProperties.DatabaseConfig;
import com.hp.springboot.database.bean.DynamicDatasourceBean;
import com.hp.springboot.database.datasource.pool.AbstConnectionPoolFactory;
import com.hp.springboot.database.enums.ConnectionPoolFactoryEnum;
import com.hp.springboot.database.exception.DataSourceNotFoundException;
import com.hp.springboot.database.exception.DynamicDataSourceRouteException;
import com.hp.springboot.database.interceptor.DAOMethodInterceptorHandle;
import com.hp.springboot.database.interceptor.ForceMasterInterceptor;

/**
 * 描述:动态路由选择数据源
 * 作者:黄平
 * 时间:2018年4月1日
 */
public class DynamicDatasource extends AbstractRoutingDataSource {

	
	private static Logger log = LoggerFactory.getLogger(DynamicDatasource.class);
	
	/**
	 * 存放所有的dao对应的数据源的key
	 * key=dao名称,value=databaseName
	 * 多数据源时,根据database.yml中的配置,先找有没有该dao指定的数据源,如果有,则使用指定的数据源,如果找不到,则使用第一个(也就是主数据源)数据源
	 */
	private static Map<String, String> databaseNameMap = new HashMap<>();
	
	/**
	 * 存放所有的数据源主从的个数
	 * master_databaseName,10
	 * slave_databaseName,20
	 */
	private static Map<String, Integer> databaseIPCountMap = new HashMap<>();
	
	/**
	 * 默认的数据源名称
	 */
	private static String DEFAULT_DATABASE_NAME = "";
	
	/**
	 * master数据源名称的前缀
	 */
	private static final String MASTER_DS_KEY_PREX = "master_";
	
	/**
	 * slave数据源名称的前缀
	 */
	private static final String SLAVE_DS_KEY_PREX = "slave_";
	
	/**
	 * 匹配查询语句
	 */
	private static Pattern select = Pattern.compile("^select.*");
	
	/**
	 * 匹配更新语句
	 */
	private static Pattern update = Pattern.compile("^update.*");
	
	/**
	 * 匹配插入语句
	 */
	private static Pattern insert = Pattern.compile("^insert.*");
	
	/**
	 * 匹配删除语句
	 */
	private static Pattern delete = Pattern.compile("^delete.*");
	
	/**
	 * 数据库配置信息
	 */
	private DatabaseConfigProperties databaseConfigProperties;
	
	public DynamicDatasource() {}
	
	public DynamicDatasource(DatabaseConfigProperties databaseConfigProperties) {
		this.databaseConfigProperties = databaseConfigProperties;
	}
	
	@Override
	public void afterPropertiesSet() {
		//设置targetDataSources 值
		if (databaseConfigProperties == null || CollectionUtils.isEmpty(databaseConfigProperties.getDatabaseConfigList())) {
			// 没有数据库配置信息,直接抛异常,启动失败
			log.error("set DynamicDatasource error. with databaseConfigProperties is null.");
			throw new DynamicDataSourceRouteException("DynamicDatasource route error. with databaseConfigProperties is null");
		}
		try {
			Map<Object, Object> targetDataSources = new HashMap<>();
			
			// 使用哪种类型的连接池(可以dbcp,Druid等等)
			AbstConnectionPoolFactory connectionPool = ConnectionPoolFactoryEnum.getConnectionPoolFactory(databaseConfigProperties.getPoolName());
			DynamicDatasourceBean dynamicDatasourceBean = null;
			String databaseName = null;
			
			// 循环遍历数据库配置信息
			for (DatabaseConfig databaseConfig : databaseConfigProperties.getDatabaseConfigList()) {
				if (databaseConfig.getServers() == null || CollectionUtils.isEmpty(databaseConfig.getServers().getMaster())) {
					// 没有配置数据库ip信息或没有配置主库,直接抛异常,启动失败
					log.error("init database error. with masterUrls is empty.");
					throw new DynamicDataSourceRouteException("masterUrls is empty. with databaseConfig is: " + databaseConfig);
				}
				
				databaseName = databaseConfig.getDatabaseName();
				dynamicDatasourceBean = connectionPool.getDynamicDatasource(databaseConfig);
				
				if (dynamicDatasourceBean == null || CollectionUtils.isEmpty(dynamicDatasourceBean.getMasterDatasource())) {
					log.error("init database error. with masterUrls is empty.");
					throw new DynamicDataSourceRouteException("masterUrls is empty. with databaseConfig is: " + databaseConfig);
				}
				
				//设置master
				for (int i = 0; i < dynamicDatasourceBean.getMasterDatasource().size(); i++) {
					// 设置到自动路由的map中
					targetDataSources.put(buildMasterDatasourceKey(databaseName, i), dynamicDatasourceBean.getMasterDatasource().get(i));
				}
				//设置master有几个数据源
				databaseIPCountMap.put(buildMasterDatasourceKey(databaseName, -1), dynamicDatasourceBean.getMasterDatasource().size());
				
				//设置slave
				if (CollectionUtils.isNotEmpty(dynamicDatasourceBean.getSlaveDatasource())) {
					for (int i = 0; i < dynamicDatasourceBean.getSlaveDatasource().size(); i++) {
						targetDataSources.put(buildSlaveDatasourceKey(databaseName, i), dynamicDatasourceBean.getSlaveDatasource().get(i));
					}
					//设置slave有几个数据源
					databaseIPCountMap.put(buildSlaveDatasourceKey(databaseName, -1), dynamicDatasourceBean.getSlaveDatasource().size());
				}
				
				//默认数据源
				if (StringUtils.isEmpty(DEFAULT_DATABASE_NAME)) {
					// databases.yml的节点 databaseConfigList 下的第一个数据源就是主数据源
					DEFAULT_DATABASE_NAME = databaseName;
				}
				
				//处理dao(这里就是多数据源自动路由使用)
				dealDAOS(databaseConfig.getDaos(), databaseName);
			}
			
			super.setTargetDataSources(targetDataSources);
			super.afterPropertiesSet();
		} catch (Exception e) {
			log.error("deal DynamicDatasource error.", e);
		}
	}

	@Override
	protected Object determineCurrentLookupKey() {
		// 获取当前线程的信息
		DAOInterfaceInfoBean daoInfo = DAOMethodInterceptorHandle.getRouteDAOInfo();
		if (daoInfo == null) {
			//如果没有获取到拦截信息,则取主数据库
			log.warn("determineCurrentLookupKey error. with daoInfo is empty.");
			//return null;
			// 由于上面没有设置defaultTargetDataSource,所以这里需要new一个对象出来空对象,下面会自动在主库中随机选择一个
			daoInfo = new DAOInterfaceInfoBean();
		}
		
		//按照dao的className,从数据源中获取数据源
		String mapperNamespace = daoInfo.getMapperNamespace();
		String databaseName = databaseNameMap.get(mapperNamespace);
		if (StringUtils.isEmpty(databaseName)) {
			//如果没有,则使用默认数据源
			databaseName = DEFAULT_DATABASE_NAME;
		}
		
		// 根据数据源的key前缀,获取真实的数据源的key
		// 这里考虑了在代码里面的注解(ForceMaster)
		String result = getDatasourceByKey(databaseName, getForceMaster(daoInfo));
		log.debug("-------select route datasource with statementId={} and result is {}", (daoInfo.getMapperNamespace() + "." + daoInfo.getStatementId()), result);
		return result;
	}
	
	/**
	* @Title: getForceMaster  
	* @Description: 获取是否  forceMaster
	* @param daoInfo
	* @return
	 */
	private boolean getForceMaster(DAOInterfaceInfoBean daoInfo) {
		if (ForceMasterInterceptor.getForceMaster()) {
			//有设置forceMaster
			return true;
		}
		
		return getMasterOrSlave(daoInfo);
	}
	
	/**
	* @Title: getMasterOrSlave  
	* @Description: 根据方法名,判断走读库还是写库
	* 这里约定我们的dao里面的方法命名
	* 查询方法:selectXXX
	* 新增方法:insertXXX
	* 更新方法:insertXXX
	* 删除方法:deleteXXX
	* 如果不符合这个规范,则默认路由到master库
	* @param daoInfo
	* @return
	 */
	private boolean getMasterOrSlave(DAOInterfaceInfoBean daoInfo) {
		//根据方法名称去判断
		boolean fromMaster = false;
		//获取用户执行的sql方法名
		String statementId = daoInfo.getStatementId();
		if (StringUtils.isEmpty(statementId)) {
			//没有获取到方法,走master
			return true;
		}
		statementId = statementId.toLowerCase();
		if (select.matcher(statementId).matches()) {
			// 如果是查询语句,这里,随机取主从
			int i = RandomUtils.nextInt(0, 2);
			fromMaster = BooleanUtils.toBoolean(i);
		} else if (update.matcher(statementId).matches() || insert.matcher(statementId).matches() || delete.matcher(statementId).matches()) {
			// 更新,插入,删除使用master数据源
			fromMaster = true;
		} else {
			//如果statemenetId不符合规范,则告警,并且使用master数据源
			log.warn("statement id {}.{} is invalid, should be start with select*/insert*/update*/delete*. ", daoInfo.getMapperNamespace(), daoInfo.getStatementId());
			fromMaster = true;
		}
		return fromMaster;
	}
	
	/**
	* @Title: getDatasourceByKey  
	* @Description: 随机获取路由
	* @param databaseName
	* @param fromMaster
	* @return
	 */
	private String getDatasourceByKey(String databaseName, boolean fromMaster) {
		String datasourceKey = null;
		Integer num = null;
		if (fromMaster) {
			datasourceKey = buildMasterDatasourceKey(databaseName, -1);
			num = databaseIPCountMap.get(datasourceKey);
			if (num == null) {
				//没找到,直接抛出异常
				log.error("datasource not found with databaseName= {}", databaseName);
				throw new DataSourceNotFoundException(databaseName);
			}
		} else {
			datasourceKey = buildSlaveDatasourceKey(databaseName, -1);
			num = databaseIPCountMap.get(datasourceKey);
			if (num == null) {
				//没有配置从库,则路由到主库
				return getDatasourceByKey(databaseName, true);
			}
		}
		
		int random = 0;
		if (num == 1) {
			//如果就只有一个数据源,则就选择它
			random = 0;
		} else {
			//随机获取一个数据源
			random = RandomUtils.nextInt(0, num);
		}
		return fromMaster ? buildMasterDatasourceKey(databaseName, random) : buildSlaveDatasourceKey(databaseName, random);
	}
	
	/**
	* @Title: dealDAOS  
	* @Description: dao处理
	* @param daoList
	* @param databaseName
	 */
	private void dealDAOS(List<String> daoList, String databaseName) {
		if (CollectionUtils.isEmpty(daoList)) {
			return;
		}
		for (String dao : daoList) {
			databaseNameMap.put(dao, databaseName);
		}
	}

	/**
	* @Title: buildMasterDatasourceKey  
	* @Description: 获取主数据源的key
	* @param databaseName
	* @param index
	* @return
	 */
	private String buildMasterDatasourceKey(String databaseName, int index) {
		StringBuilder sb = new StringBuilder(MASTER_DS_KEY_PREX).append(databaseName);
		if (index >= 0) {
			sb.append("_").append(index);
		}
		return sb.toString();
	}
	
	/**
	 * 获取从数据源的key
	 * @param databaseName
	 * @param index
	 * @return
	 */
	private String buildSlaveDatasourceKey(String databaseName, int index) {
		StringBuilder sb = new StringBuilder(SLAVE_DS_KEY_PREX).append(databaseName);
		if (index >= 0) {
			sb.append("_").append(index);
		}
		return sb.toString();
	}

}

このようにして、メソッド名に基づいて対応するデータベースに自動的にルーティングします。私たちのビジネスコードは以前と同じように書かれており、ビジネスコードにほとんど影響を与えません。

 

それだけだと思いますか?おそらく、上記はほとんどの場合問題ありません。しかし、黒板をノックする、黒板をノックする、状況の事情を考慮していません

通常、トランザクションを追加する場合は、メソッドの前に@Transactionalアノテーションを追加します。次のように:

Transactionalがメソッドに追加されているため、springはtest()メソッドに入る前にデータソースを選択する必要がありますが、DAOMethodInterceptorHandleインターセプターがDAOメソッドをインターセプトし、daoメソッドがサービス中であるため、今回はSpringが見つかります。スレッド変数を設定するためのインターセプターを実行していないため、determineCurrentLookupKey()メソッドを実行してもデータソースは取得されません(もちろん、ここで記述しました。最初のデータソースがデフォルトで返されます)。したがって、この時点でSQLを再度実行すると、エラーが報告されます(正確には、デフォルト以外のデータベースでSQLを実行するとエラーが報告されます)。

現時点で、デフォルト以外のデータベースを使用してトランザクションを制御する場合は、ここでエラーが報告されます。問題はここにあります、それをどのように解決するのですか?

通常、自動ルーティングを使用する場合、最初にインターセプターを入力してスレッド変数を設定し、次に動的ルーティングを入力してデータソースを選択します。しかし、Transactionalアノテーションを追加した後、動的ルーティングを選択するステップは、サービスメソッドに進むことを余儀なくされました。したがって、現時点ではスレッド変数を取得できません。したがって、デフォルト以外のデータベースが実行され、トランザクション制御が必要な場合、エラーが報告されます。

実行ロジックとシーケンスがわかったので、問題は解決しました。別の注釈を追加する限り(トランザクションと並行して)、データベースは必須です。(1つのトランザクションで異なるデータベースにクエリを実行しようとしないでください)

さて、アノテーション@UseDatabaseを追加しましょう

package com.hp.springboot.database.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 描述:强制使用数据库
 * 作者:黄平
 * 时间:2021-1-7
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface UseDatabase {

	/**
	* @Title: value  
	* @Description: 数据名称
	* @return
	 */
	String value() ;
}

UseDatabaseアノテーションを解析します:

package com.hp.springboot.database.interceptor;

import java.lang.reflect.Method;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

import com.hp.springboot.database.annotation.UseDatabase;
import com.hp.springboot.database.exception.DatabaseNotSetException;

/**
 * 描述:强制使用数据
 * 作者:黄平
 * 时间:2021-1-7
 */
@Aspect
@Order(Ordered.HIGHEST_PRECEDENCE)
public class UseDatabaseInterceptor {

	private static Logger log = LoggerFactory.getLogger(UseDatabaseInterceptor.class);
	
	private static final ThreadLocal<String> USE_DATABASE = new InheritableThreadLocal<>();
	
	/**
	* @Title: around  
	* @Description: 设置强制走的数据库
	* @param joinPoint
	* @return
	* @throws Throwable
	 */
	@Around("@annotation(com.hp.springboot.database.annotation.UseDatabase)")
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		log.debug("start before");
		
		//方法签名
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		
		Method method = methodSignature.getMethod();
		
		// 获取UseDatabase注解
		UseDatabase useDatabase = method.getAnnotation(UseDatabase.class);
		
		// 设置的数据库名称
		String databaseName = useDatabase.value();
		
		if (StringUtils.isEmpty(databaseName)) {
			// 没有指定数据库名称,报错
			log.error("UseDatabase error. with databaseName is empty");
			throw new DatabaseNotSetException();
		}
		
		// 设置到线程变量中
		USE_DATABASE.set(databaseName);
		Object obj = null;
		try {
			obj = joinPoint.proceed();
			return obj;
		} catch (Exception e) {
			throw e;
		} finally {
			log.debug("start after");
			USE_DATABASE.remove();
		}
	}
	
	/**
	* @Title: getForceMaster  
	* @Description: 获取设置的哪个数据库
	* @return
	 */
	public static String getDatabaseName() {
		return USE_DATABASE.get();
	}
}

UseDatabaseアノテーションを追加したメソッドを解析し、データベース名をスレッド変数に設定するインターセプタークラス

ここに追加されることに注意してください

Orderアノテーションを追加し、アノテーションの順序を最高の優先度に設定して、インターセプターの実行順序を設定します。そうしないと、このインターセプターがトランザクションインターセプターの背後で実行される場合、問題が発生します。

では、自動ルーティングのdetermineCurrentLookupKeyメソッドを変更する必要があります

もちろん、これらをSpringboot-starterにパックできるので、屋外で使用する場合は、Mavenに直接依存できます。

完了しました。また、ビジネス上の問題を解決します。ただし、@ Transactionalアノテーションメソッドでは、別のデータベースにアクセスしないように特に注意する必要があります。

最後に、対応するdatabases.ymlファイルが添付されます。これは、デフォルトでsrc / main / resourcesの下に配置されます。

hp:
  springboot:
    database:
      expression: "execution(* com.test.dal..*.*(..))"
      poolName: DBCP
      databaseConfigList:
        - databaseName: test
          servers:
            master:
              - ${database.test.master.url}
            slave:
              - 127.0.0.22:3307
              - 127.0.0.33:3308
          username: root
          password: 123456
    
        - databaseName: sys
          servers:
            master:
              - 127.0.0.1:3306
            slave:
              - 127.0.0.2:3301
              - 127.0.0.3:3302
          username: root
          password: 123456
          daos:
            - com.test.dal.ISysConfigDAO

 

 

完全なコードは私のgithubにあります:https//github.com/terry2870/hp-springboot

ブログを書くのは初めてです。悪い文章や提案があれば、遠慮なくメッセージを残してください!

 

おすすめ

転載: blog.csdn.net/terry2870/article/details/112303053