springboot动态数据源

本文介绍如何在Spring Boot 2.0中实现多数据源动态注册与切换,包括通过配置控制数据源数量,利用AOP技术进行数据源切换,以及在不同数据源间进行事务控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于spring-boot2.0.0版本实现多数据源注册

功能点:一,可以通过配置控制数据源注册个数,实现事先不知道数据源个数和别名,在不修改任何有关数据库相关代码条件下,仅仅在使用时按照规则添加配置来注册多数据源.可以将其作为基础jar提供公共服务.

二,实现了多数据源中,调用单个数据源的事务控制(同一个方法中只调用一个数据库的事务),同一个方法中调用不同数据源就是分布式事务,以下代码没有实现.

实现代码:

1,继承AbstractRoutingDataSource重写determineCurrentLookupKey方法,得到通过AOP切换之后的数据源名称

import lombok.extern.slf4j.Slf4j;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 功能说明: 通过重写determineCurrentLookupKey得到切换之后的数据源名称<br>
 * 系统版本: v1.0<br>
 * 开发人员: @author liansh<br>
 * 开发时间: 2019年6月25日<br>
 */
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
	private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

	@Override
	protected Object determineCurrentLookupKey() {
		log.debug("数据源为:{}", DynamicDataSource.getDataSource());
		return DynamicDataSource.getDataSource();
	}

	public static void setDataSource(String dataSource) {
		contextHolder.set(dataSource);
	}

	public static String getDataSource() {
		return contextHolder.get();
	}

	public static void clearDataSource() {
		contextHolder.remove();
	}
}

2,添加注解,在方法上声明该注解,指定数据源,不添加注解就会使用默认数据源

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;

/**
 * 功能说明: 数据源<br>
 * 系统版本: v1.0<br>
 * 开发人员: @author liansh<br>
 * 开发时间: 2019年6月25日<br>
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
	String defaultStr = "default";

	String value() default defaultStr;
}

3,读取数据源相关配置,通过数据库别名,动态读取具有相同特征(spring.datasource.xxx.username,xxx作为别名)的数据库相关配置并注册进spring容器,旨在将此类作为jar,不管扩展多少个未知数据源,都可以不用修改数据源相关代码

import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import com.alibaba.druid.pool.DruidDataSource;

/**
 * 功能说明: 动态数据源配置,配合注解可完成数据库自动切换,但切换必须在service以上,不能在mapper层<br>
 * 系统版本: v1.0<br>
 * 开发人员: @author liansh<br>
 * 开发时间: 2019年6月25日<br>
 */
@Slf4j
@Configuration
public class DataSourceConfig {
	@Value("${jdbc.mapper-locations:classpath*:**/*Mapper.xml}")
	private String mappingPath;
	/** 需要注册的数据库别名列表 */
	@Value("${jdbc.target-sources:default,order}")
	private String targetSources = "";
	@Autowired
	private Environment env;

	private Map<Object, Object> getTargetSource() {
		Map<Object, Object> target = new HashMap<Object, Object>();
		// 遍历数据源名称,读取相关配置,配置格式为spring.datasource.xxx.username
		for (String key : targetSources.split(",")) {
			String point = key.equals(DS.defaultStr) ? "" : ".";
			String pkg = key.equals(DS.defaultStr) ? "" : key;
			DruidDataSource dataSource = new DruidDataSource();
			dataSource.setDriverClassName(env.getProperty("spring.datasource." + pkg + point + "driverClassName"));
			dataSource.setUrl(env.getProperty("spring.datasource." + pkg + point + "jdbc-url"));
			dataSource.setUsername(env.getProperty("spring.datasource." + pkg + point + "username"));
			dataSource.setPassword(env.getProperty("spring.datasource." + pkg + point + "password"));
			dataSource.setTestWhileIdle(true);
			String maxActive = env.getProperty("spring.datasource." + pkg + point + "maxActive", "50");
			String premoveAbandoned = env.getProperty("spring.datasource." + pkg + point + "removeAbandoned", "true");
			String premoveAbandonedTimeout = env.getProperty("spring.datasource." + pkg + point + "removeAbandonedTimeout", "180");
			int poolMaximumActiveConnections = 10, removeAbandonedTimeout = 10;
			boolean removeAbandoned = "true".equalsIgnoreCase(env.getProperty("db.removeAbandoned"));
			if (NumberUtils.isCreatable(maxActive)) {
				poolMaximumActiveConnections = NumberUtils.toInt(maxActive);
			}
			if (StringUtils.isNotBlank(premoveAbandoned)) {
				removeAbandoned = "true".equalsIgnoreCase(premoveAbandoned);
			}
			if (NumberUtils.isCreatable(premoveAbandonedTimeout)) {
				removeAbandonedTimeout = NumberUtils.toInt(premoveAbandonedTimeout);
			}
			dataSource.setRemoveAbandoned(removeAbandoned);
			dataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout);
			dataSource.setMaxActive(poolMaximumActiveConnections);
			target.put(pkg.equals("") ? DS.defaultStr : pkg, dataSource);
			log.info("jdbc.properties加载" + pkg + "数据库配置成功" + "(" + key + ")");
		}
		return target;
	}

	@Bean
	public SqlSessionFactory sqlSessionFactory() throws Exception {
		SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
		// 需要动态指定
		factoryBean.setDataSource(dataSource());
		// 手动实现数据连接无法自动读取xml配置的路径,需要手动指定,若用注解生成的java类sql,无需手动切换
		factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mappingPath));
		return factoryBean.getObject();
	}

	/** 事务:此处虽然Bean只有一个,但不管<单独>使用哪一个数据源,事务都可以接管成功 */
	@Bean
	public DataSourceTransactionManager transactionManager() {
		return new DataSourceTransactionManager(dataSource());
	}

	/** 动态数据源相关配置 */
	@Bean
	public DataSource dataSource() {
		Map<Object, Object> targetDataSources = getTargetSource();
		DynamicDataSource dynamicDataSource = new DynamicDataSource();
		// 默认数据源
		dynamicDataSource.setDefaultTargetDataSource(targetDataSources.get(DS.defaultStr));
		dynamicDataSource.setTargetDataSources(targetDataSources);
		return dynamicDataSource;
	}
}

4,注册AOP拦截,前置切面完成切换数据源,后置切面清空数据源,必须添加@Order注解,使该切面先于事务切面执行,否则会造成开启事务情况下切换数据源失败

package com.lsh.jdbc.dataSource;

import java.lang.reflect.Method;

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.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 功能说明: 数据源切换Aop切面,作用:在方法之前完成数据源的切换<br>
 * 如果该切面不添加@Order注解,那么事务切面将在切换数据源之前执行,导致切换数据源失败<br>
 * 系统版本: v1.0<br>
 * 开发人员: @author liansh<br>
 * 开发时间: 2019年6月25日<br>
 */
@Order(1)
@Aspect
@Component
public class DynamicDataSourceAspect {
	@Before("@annotation(DS)")
	public void beforeSwitchDS(JoinPoint point) {
		// 获得当前访问的class
		Class<?> className = point.getTarget().getClass();
		// 获得访问的方法名
		String methodName = point.getSignature().getName();
		// 得到方法的参数的类型
		Class<?>[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes();
		String dataSource = DS.defaultStr;
		try {
			// 得到访问的方法对
			Method method = className.getMethod(methodName, argClass);
			DS interceptor = className.getAnnotation(DS.class);
			if (interceptor != null) {// 判断类上是否存在@DS注解
				// 取出注解中的数据源名
				dataSource = interceptor.value();
			} else if (method.isAnnotationPresent(DS.class)) {// 判断方法上是否存在@DS注解
				DS annotation = method.getAnnotation(DS.class);
				// 取出注解中的数据源名
				dataSource = annotation.value();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		DynamicDataSource.setDataSource(dataSource);
	}
	

	@After("@annotation(DS)")
	public void afterSwitchDS(JoinPoint point) {
		DynamicDataSource.clearDataSource();
	}
}

完整代码示例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值