Separación de lectura y escritura basada en mybatis, enrutamiento automático de múltiples fuentes de datos

Tabla de contenido

1. ¿Por qué hacemos esto?

2. ¿Qué debemos hacer?

En tercer lugar, la tecnología utilizada

Cuatro, usa

1. Primero defina un interceptor para interceptar antes de la operación de nuestra base de datos

2. Luego defina el aspecto en el archivo de configuración xml

3. Después de obtener la información de la clase dao del hilo actual para ejecutar sql, la colocamos en el objeto ThreadLocal y la usamos cuando seleccionamos la ruta.


1. ¿Por qué hacemos esto?

  1. En proyectos reales de alta concurrencia, la presión de una sola biblioteca es muy alta. En este momento, es necesario introducir la estructura maestro-esclavo de la base de datos. (Si es subtabla de sub-base de datos o clúster de base de datos, otra charla).
  2. Debido a que los microservicios no están completamente divididos, o una sola aplicación en absoluto, necesita acceder a múltiples datos

2. ¿Qué debemos hacer?

El enfoque anterior puede ser definir múltiples fuentes de datos en nuestro archivo de configuración y luego seleccionar diferentes fuentes de datos en dao en función de la adición, eliminación, modificación y verificación. Al hacerlo, es posible que necesitemos codificar el proceso de selección de la fuente de datos en el código, lo cual obviamente no es lo suficientemente amigable y generará mucho código redundante y duplicado.

¿Es posible que nuestro programa seleccione automáticamente la fuente de datos de enrutamiento, y mi código sigue siendo el mismo que antes, solo se preocupa por la lógica comercial, en cuanto a cómo elegir, todo queda en manos del marco para implementarlo? Si hace esto, ¿cree que el código es mucho más claro en un instante? Entonces, echemos un vistazo a la implementación paso a paso.

En tercer lugar, la tecnología utilizada

  1. Clase AbstractRoutingDataSource proporcionada por Spring. Esta clase se encuentra en múltiples fuentes de datos, seleccionará dinámicamente la fuente de datos de acuerdo con la clave de enrutamiento devuelta por el método determineCurrentLookupKey ()
  2. primavera AOP. Debe interceptar la solicitud antes de ejecutar el método sql, establecer el método, el nombre de la clase, el parámetro y otra información de la solicitud del hilo en la variable local del hilo (ThreadLocal), y luego determinarCurrentLookupKey puede seleccionar dinámicamente la fuente de datos de acuerdo con los datos del hilo .
  3. Spring-boot-autoConfiguration. Los beans configurados se pueden cargar automáticamente. No es necesario escribir archivos de configuración manualmente

Cuatro, usa

1. Primero defina un interceptor para interceptar antes de la operación de nuestra base de datos

El código completo a continuación está en mi github, consulte: https://github.com/terry2870/hp-springboot

 

Defina la clase DAOMethodInterceptorHandle para implementar la interfaz MethodInterceptor, el código completo es el siguiente

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();
	}
}

Aquí usamos getDAOInterfaceInfoBean para obtener información como el método, los parámetros, la firma, etc., llamado por el hilo actual. Proporciono la realización de mybatis aquí. como sigue

/**
 * 
 */
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. Luego defina el aspecto en el archivo de configuración xml

De esta manera, el interceptor es efectivo

 


3. Después de obtener la información de la clase dao del hilo actual para ejecutar sql, la colocamos en el objeto ThreadLocal y la usamos cuando seleccionamos la ruta.

OK, definimos una ruta automática, los pasos de ejecución son los siguientes

  1. Cuando se inicia el servicio, cargue la información de configuración de la base de datos, lea la información de configuración de la base de datos maestro-esclavo
  2. Toda la información de la fuente de datos se carga en targetDataSources de AbstractRoutingDataSource para una selección dinámica posterior
  3. Antes de ejecutar sql, seleccione dinámicamente el enrutamiento de la fuente de datos

Primero elija qué base de datos, luego elija el maestro y el esclavo. Pasos de enrutamiento dinámico:

  1. Obtener la información del método de la ejecución del hilo actual interceptado por el interceptor anterior
  2. Según el className de dao, obtenga la fuente de datos de la fuente de datos (es decir, si hay una fuente de datos separada configurada para el dao enbases de datos.yml)
  3. Si la hay, use la fuente de datos especificada; si no, use la fuente de datos predeterminada (es decir, la primera fuente de datos)
  4. Dado que lo que obtuvimos anteriormente es solo el prefijo de la fuente de datos, a continuación tenemos que obtener la clave correspondiente a la fuente de datos real
  5. De acuerdo con el nombre del método de consulta, determine si leer o escribir la biblioteca. (Esto considerará si hay un comentario de ForceMaster antes del método)
  6. Devuelva la clave de la fuente de datos real final y obtenga la fuente de datos real

El código correspondiente es el siguiente:

/**
 * 
 */
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();
	}

}

De esta manera, enrutamos automáticamente a la base de datos correspondiente según el nombre del método. Nuestro código comercial todavía está escrito como antes, y es casi no invasivo para nuestro código comercial.

 

¿Crees que es eso? Quizás lo anterior no sea un problema en la mayoría de los casos. Sin embargo, golpeando la pizarra, golpeando la pizarra, no hemos considerado los asuntos de la situación.

Normalmente, si queremos agregar transacciones, agregaremos anotaciones @Transactional antes del método. Como sigue:

Dado que Transactional se agrega al método, Spring debe seleccionar la fuente de datos antes de ingresar al método test (), pero nuestro interceptor DAOMethodInterceptorHandle intercepta el método DAO, y el método dao está en el servicio, por lo que esta vez encontrará que el resorte ejecuta el método determineCurrentLookupKey () no obtendrá la fuente de datos (por supuesto, escribí aquí, la primera fuente de datos se devuelve por defecto), porque no hemos ejecutado el interceptor para establecer la variable del hilo. Por tanto, si vuelve a ejecutar SQL en este momento, se informará de un error (para ser precisos, se informará de un error al ejecutar SQL en una base de datos no predeterminada).

En este momento, si queremos usar una base de datos no predeterminada y tener control de transacciones, aquí se informará un error. El problema está aquí, ¿cómo solucionarlo?

Cuando usamos el enrutamiento automático normalmente, primero ingresamos al interceptor para establecer las variables del hilo y luego ingresamos el enrutamiento dinámico para seleccionar la fuente de datos. Pero después de agregar la anotación transaccional, el paso de seleccionar el enrutamiento dinámico se vio obligado a avanzar al método de servicio. Por lo tanto, las variables de subproceso no se pueden obtener en este momento. Por lo tanto, si se ejecuta una base de datos no predeterminada y se requiere el control de transacciones, se informará un error.

Ahora que conocemos la lógica y la secuencia de ejecución, el problema está resuelto. Siempre que agreguemos otra anotación (paralela a Transaccional), la base de datos es obligatoria. (No intente consultar diferentes bases de datos en una transacción)

Bien, agreguemos una anotación @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() ;
}

Analizar la anotación 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();
	}
}

La clase de interceptor para analizar el método agregamos anotaciones UseDatabase y establecemos el nombre de la base de datos en la variable del hilo

Tenga en cuenta que aquí se agrega

Se agregó la anotación de orden y se estableció el orden de las anotaciones con la máxima prioridad para establecer el orden de ejecución del interceptor. De lo contrario, si este interceptor se ejecuta detrás del interceptor transaccional, será un problema.

Bien, entonces el método determineCurrentLookupKey de nuestro enrutamiento automático necesita ser modificado

Por supuesto, podemos empaquetarlos en un arrancador de resorte, de modo que si lo usa afuera, puede confiar directamente en maven.

ok ya terminaste! También resuelve problemas comerciales. Pero una cosa a la que debe prestar especial atención, en el método de anotación @Transactional, no visite diferentes bases de datos.

Finalmente, se adjunta el archivo correspondientebases.yml, que se coloca en src / main / resources por defecto.

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

 

 

El código completo se puede encontrar en mi github: https://github.com/terry2870/hp-springboot

Es la primera vez que escribo un blog. Si tienes alguna mala redacción o sugerencias, ¡no dudes en dejarme un mensaje!

 

Supongo que te gusta

Origin blog.csdn.net/terry2870/article/details/112303053
Recomendado
Clasificación