关于SpringDataJpa框架的多数据源支持功能实现

现实项目项目中,很多人都有多数据源的需求,其实在JPA中这个也是很容易实现,本文就是来探讨实现的原理及细节!

看过我写的SpringDataJPA的数据源读写分离实现的朋友,其实对于本文就会实现就会有一定思路了。两者原理其实是类似的。

基本原理:

1.初始化默认的数据源;

2.从默认的数据源读取数据源信息,然后进行初始化并放入容器;

3.使用代理代替数据源,并配置数据源路由即可。

具体代码实现:

动态数据源注解:

package vip.efactory.idc.common.dsjpa.annotation;

import java.lang.annotation.*;

/**
 * Description:动态数据源的注解
 *
 * @author dbdu
 * @date 2020-7-16
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DynamicDataSource {

	/**
	 * groupName or specific database name or spring SPEL name.
	 *
	 * @return the database you want to switch
	 */
	String value() default "default";
}

默认使用默认数据源,value有值则使用value的值,当然如果值是#last会取目标方数的最后一个参数作为值,以实现动态数据源的效果!

使用枚举避免硬编码:

package vip.efactory.idc.common.dsjpa.enums;

/**
 * 动态数据源枚举
 */
public enum DynamicDataSourceEnum {
	DEFAULT("default");

	DynamicDataSourceEnum(String name) {
		this.name = name;
	}

	private String name;
}

自定义事务子类,以便观察数据源的选择:

package vip.efactory.idc.common.dsjpa.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.DefaultTransactionStatus;

/**
 * 这个类其实完全可以不存在,主要是为了打日志方便观察动态数据源的使用!
 * 如果不需要观察,直接使用JpaTransactionManager即可!
 */
@SuppressWarnings("serial")
@Slf4j
public class MyJpaTransactionManager extends JpaTransactionManager {

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        log.debug("jpa-transaction:begin-----now dataSource use is [" + DataSourceContextHolder.getDataSource() + "]");
        super.doBegin(transaction, definition);
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        log.debug("jpa-transaction:commit-----now dataSource use is [" + DataSourceContextHolder.getDataSource() + "]");
        super.doCommit(status);
    }
}

自定义的数据源持有者,存储所有的初始化的数据源:

package vip.efactory.idc.common.dsjpa.config;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import vip.efactory.idc.common.core.exception.DataSourceNotFoundException;
import vip.efactory.idc.common.core.util.StringUtils;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * Description: 数据源提供者,管理所有的数据源信息
 *
 * @author dbdu
 * @date 2020-7-16
 */
@Configuration
@Slf4j
public class DynamicDataSourceProvider {

	// 使用一个map来存储我们数据源Key和对应的数据源,把所有数据库都放在dataSourceMap中,
	// 注意key值要和determineCurrentLookupKey()中代码写的一致,否则切换数据源时找不到正确的数据源
	public static Map<Object, Object> dataSourceMap = new HashMap<>();

	// 根据传进来的dataSourceKey决定返回的数据源,不存在则抛出异常
	@SneakyThrows
	public static DataSource getDataSourceByKey(String dataSourceKey) {
		if (dataSourceMap.containsKey(dataSourceKey)) {
			return (DataSource) dataSourceMap.get(dataSourceKey);
		}
		throw new DataSourceNotFoundException("指定的数据源键[" + dataSourceKey + "]不存在!");
	}

	// 初始化的时候用于添加数据源的方法
	@SneakyThrows
	public static void addDataSource(String dataSourceKey, DataSource dataSource) {
		if (StringUtils.isEmpty(dataSourceKey) || dataSource == null) {
			throw new DataSourceNotFoundException("添加数据源时键[" + dataSourceKey + "]不允许为空或者数据源不允许为空!");
		}
		dataSourceMap.put(dataSourceKey, dataSource);
	}

	// 根据传进来的dataSourceKey来删除指定的数据源
	public static void removeDataSourceByKey(String dataSourceKey) {
		if (dataSourceMap.containsKey(dataSourceKey)) {
			dataSourceMap.remove(dataSourceKey);
		}
	}

}

注意此处管理并非真正的管理,不过因为框架内的数据源可以利用它刷新,等效于可管理!

数据源的上下文管理,通过改变这里从而改变使用的数据源。

package vip.efactory.idc.common.dsjpa.config;

import lombok.extern.slf4j.Slf4j;

/**
 * Description:本地线程,数据源上下文切换
 *
 * @author dbdu
 * @date 2020-07-16
 */
@Slf4j
public class DataSourceContextHolder {
	//线程本地环境
	private static final ThreadLocal<String> local = new ThreadLocal<String>();

	public static ThreadLocal<String> getLocal() {
		return local;
	}

	/**
	 * 设定使用的数据源标识
	 */
	public static void setDataSource(String dsId) {
		local.set(dsId);
		log.debug("设定使用的数据源标识为:" + dsId);
	}

	/**
	 * 获取使用的数据源标识
	 *
	 * @return
	 */
	public static String getDataSource() {
		log.debug("获取使用的数据源标识为:" + local.get());
		return local.get();
	}

	/**
	 * 清除使用的数据源标识
	 */
	public static void clear() {
		log.debug("删除使用的数据源标识为:" + local.get());
		local.remove();
	}
}

动态数据源注解的切面实现:

package vip.efactory.idc.common.dsjpa.config;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;
import vip.efactory.idc.common.core.util.StringUtils;
import vip.efactory.idc.common.dsjpa.annotation.DynamicDataSource;
import vip.efactory.idc.common.dsjpa.enums.DynamicDataSourceEnum;

/**
 * Description:在service层决定数据源
 * 必须在事务AOP之前执行,所以实现Ordered,order的值越小,越先执行
 * 方法名或者类名上加上动态数据源@DynamicDataSource注解就可以使用对应的数据库!!
 *
 * @author dbdu
 * @date 2020-7-16
 */
@Aspect
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
@Component
public class DataSourceAopInService implements PriorityOrdered {

	private static final String LAST_PREFIX = "#last";

	/**
	 * 设定使用的数据源
	 *
	 * @param dds
	 */
	@Before("@annotation(dds)")
	public void setDynamicDataSource(JoinPoint point, DynamicDataSource dds) {
		String key = dds.value();
		if (!StringUtils.isEmpty(key) && key.startsWith(LAST_PREFIX)) { // 说明是使用方法的最后一个参数作为数据源的key
			Object[] arguments = point.getArgs();
			// 参数值为空及空串时,使用默认的数据源!
			String dynamicKey = (arguments[arguments.length - 1] == null || StringUtils.isEmpty(String.valueOf(arguments[arguments.length - 1]))) ? DynamicDataSourceEnum.DEFAULT.name() : String.valueOf(arguments[arguments.length - 1]);
			DataSourceContextHolder.setDataSource(dynamicKey);
			return;
		}
		DataSourceContextHolder.setDataSource(dds.value());
	}

	@Override
	public int getOrder() {
		/**
		 * 值越小,越优先执行
		 * 要优于事务的执行
		 * 在启动类中加上了@EnableTransactionManagement(order = 10)
		 */
		return 1;
	}

}

从上面的代码中,我们知道注解的默认值为default,即注解的值不写则使用默认数据源;如果写的是#last则取目标方法的最后一个参数作为数据源的key进行查询;如果注解的值不为空且不为#last则使用注解的值。

动态数据源的关键配置,路由配置:

package vip.efactory.idc.common.dsjpa.config;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;

/**
 * Description: 抽象数据源的路由的子类
 * Created at:2020-07-16
 * by dbdu
 */
@Getter
@Setter
@AllArgsConstructor
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {

	/**
	 * 重写父类的功能,如果没有查询key使用默认,
	 * 如果有查询key,但是没有对应的数据源,直接抛异常不允许使用默认的数据源,如果使用,用户会误以为是自己的查询key对应的数据源!!!
	 * 因为父类的属性私有,目前重写不了,变通的方式是检查DynamicDataSourceProvider,这里没有就认为没有
	 */
	@Override
	protected DataSource determineTargetDataSource() {
		Object lookupKey = determineCurrentLookupKey();
		if (!StringUtils.isEmpty(lookupKey)) {
			// 检查持有者是否含有,没有就抛异常
			DataSource dataSource = DynamicDataSourceProvider.getDataSourceByKey((String) lookupKey);
			if (dataSource == null) {
				throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
			}
		}
		return super.determineTargetDataSource();
	}

	/**
	 * 这是AbstractRoutingDataSource类中的一个抽象方法,
	 * 而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,
	 * targetDataSources就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。
	 */
	@Override
	protected Object determineCurrentLookupKey() {
		return DataSourceContextHolder.getDataSource();
	}
}

总配置:

package vip.efactory.idc.generator.config;

import com.alibaba.druid.pool.DruidDataSource;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.hibernate.tool.schema.Action;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import vip.efactory.idc.common.dsjpa.config.DynamicDataSourceProvider;
import vip.efactory.idc.common.dsjpa.config.MyAbstractRoutingDataSource;
import vip.efactory.idc.common.dsjpa.config.MyJpaTransactionManager;
import vip.efactory.idc.common.dspublic.config.DataSourceProperties;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
@EnableConfigurationProperties({JpaProperties.class})
@EnableTransactionManagement
// @EnableJpaRepositories(basePackages = {"vip.efactory"})
@Slf4j
@AllArgsConstructor
public class DynamicDataSourceJpaConfiguration {
	private final JpaProperties jpaProperties; // yml文件中的jpa配置
	private DataSourceProperties dataSourceProperties;

	/**
	 * 初始化默认的数据源
	 */
	@Bean
	public DataSource defaultDataSource() {
		// 先初始化默认数据源,然后其他的数据源然后再进行初始化,详见:DataSourceBeanPostProcessor类
		DruidDataSource defaultDataSource = new DruidDataSource();
		defaultDataSource.setUsername(dataSourceProperties.getUsername());
		defaultDataSource.setPassword(dataSourceProperties.getPassword());
		defaultDataSource.setUrl(dataSourceProperties.getUrl());
		defaultDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
		try {
			defaultDataSource.init();
		} catch (SQLException e) {
			log.error("默认数据源初始化异常!", e);
			log.error("系统即将退出!");
			System.exit(1);
		}
		return defaultDataSource;
	}

	/**
	 * 把所有数据库都放在路由中
	 * 重点是roundRobinDataSouceProxy()方法,它把所有的数据库源交给AbstractRoutingDataSource类,
	 * 并由它的determineCurrentLookupKey()进行决定数据源的选择。
	 *
	 * @return
	 */
	@Bean(name = "roundRobinDataSouceProxy")
	public AbstractRoutingDataSource roundRobinDataSouceProxy(DataSource defaultDataSource) {
		MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
		proxy.setTargetDataSources(DynamicDataSourceProvider.dataSourceMap);
		//默认库
		//proxy.setDefaultTargetDataSource(DynamicDataSourceProvider.dataSourceMap.get(DynamicDataSourceEnum.DEFAULT.name()));
		proxy.setDefaultTargetDataSource(defaultDataSource);
		return proxy;
	}

	@Bean
	public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(AbstractRoutingDataSource roundRobinDataSouceProxy) {
		Map<String, Object> hibernateProps = new LinkedHashMap<>();
		hibernateProps.putAll(this.jpaProperties.getProperties());
		hibernateProps.put(Environment.DIALECT, "org.hibernate.dialect.MySQL8Dialect");
		hibernateProps.put(Environment.PHYSICAL_NAMING_STRATEGY, "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy"); // 属性及column命名策略
		hibernateProps.put(Environment.HBM2DDL_AUTO, Action.UPDATE); // 自动更新表结构,仅默认数据源有效且控制台会报警告可以不用管!
		// hibernateProps.put(Environment.SHOW_SQL, true); // 显示SQL,如果需要可以打开
		// hibernateProps.put(Environment.FORMAT_SQL, true); // 格式化SQL,如果需要可以打开

		// No dataSource is set to resulting entityManagerFactoryBean
		LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();
		result.setPackagesToScan("vip.efactory");
		result.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
		result.setJpaPropertyMap(hibernateProps);
		// 这个数据源设置为代理的数据源,----这是关键性配置!!!
		result.setDataSource(roundRobinDataSouceProxy);

		return result;
	}

	@Bean
	@Primary // 注意我们自己定义的bean,最好都加此注解,防止与自动配置的重复而不知道如何选择
	public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
		return entityManagerFactoryBean.getObject();
	}

	@Bean(name = "transactionManager")
	@Primary // 注意我们自己定义的bean,最好都加此注解,防止与自动配置的重复而不知道如何选择
	public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory, AbstractRoutingDataSource roundRobinDataSouceProxy) {
		SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
		// 此处在SpringDataJpa中不使用hibernate的事务管理,否则可能导致log持久层save方法不写数据库的问题
		// HibernateTransactionManager result = new HibernateTransactionManager();
		// result.setAutodetectDataSource(false); // 不自动检测数据源
		// result.setSessionFactory(sessionFactory);
		// result.setRollbackOnCommitFailure(true);
		// return result;

		// JpaTransactionManager txManager = new JpaTransactionManager(); // 如果不想观察数据源的选择,请使用本行
		JpaTransactionManager txManager = new MyJpaTransactionManager(); // 使用自定义的子类是为了更好的观察多数据源切换
		// 这个数据源设置为代理的数据源,----这是关键性配置!!!
		txManager.setDataSource(roundRobinDataSouceProxy);
		txManager.setEntityManagerFactory(entityManagerFactory);
		return txManager;
	}
}

初始化数据表里的数据源:

package vip.efactory.idc.generator.config;

import com.alibaba.druid.pool.DruidDataSource;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import vip.efactory.idc.common.dsjpa.config.DynamicDataSourceProvider;
import vip.efactory.idc.common.dsjpa.enums.DynamicDataSourceEnum;
import vip.efactory.idc.generator.domain.GenDataSourceConf;
import vip.efactory.idc.generator.service.GenDataSourceConfService;

import javax.annotation.PostConstruct;
import java.sql.SQLException;
import java.util.List;

/**
 * 初始化数据表中定义的数据源
 */
@AllArgsConstructor
@Component
@Slf4j
public class DataSourceBeanPostProcessor {
	private DruidDataSource defaultDataSource;  //自动配置创建的druid数据源
	private GenDataSourceConfService genDataSourceConfService;
	private final StringEncryptor stringEncryptor; // 密码密文解码
	private AbstractRoutingDataSource roundRobinDataSouceProxy;


	@PostConstruct
	public void init() {
		// 先将默认的数据源也放在容器里
		DynamicDataSourceProvider.addDataSource(DynamicDataSourceEnum.DEFAULT.name(), defaultDataSource);  // 放入数据源集合中
		log.info("动态数据源初始化开始...");
		List<GenDataSourceConf> dataSourceConfs = (List<GenDataSourceConf>) genDataSourceConfService.findAll();
		// 初始化所有租户的数据源
		if (dataSourceConfs != null && dataSourceConfs.size() > 0) {
			dataSourceConfs.forEach(dataSourceConf -> {
				try {
					DruidDataSource newDataSource = defaultDataSource.cloneDruidDataSource();  // 克隆已有的数据源进行修改
					// 设定新的数据源的重要参数
					newDataSource.setUsername(dataSourceConf.getUserName());
					newDataSource.setPassword(stringEncryptor.decrypt(dataSourceConf.getPwd()));
					newDataSource.setUrl(dataSourceConf.getJdbcUrl());
					// newDataSource.setDriverClassName(dataSourceConf.getDriverClassName());  // 其实也可以默认
					newDataSource.init(); // 初始化数据源
					DynamicDataSourceProvider.addDataSource(dataSourceConf.getName(), newDataSource);  // 放入数据源集合中
					log.info("数据源{}初始化完成!", dataSourceConf.getName());
				} catch (SQLException throwables) {
					log.error("数据源{}初始化失败!异常内容:{}", dataSourceConf.getName(), throwables.getMessage());
					throwables.printStackTrace();
				}
			});
			// 再次设定路由的数据源,否则用key查不到就会走默认数据源,因为初次是空集合。
			roundRobinDataSouceProxy.setTargetDataSources(DynamicDataSourceProvider.dataSourceMap);
			roundRobinDataSouceProxy.afterPropertiesSet();  // 重新处理加入的数据源集合,如果不调用此方法就算加入了也不会被处理,等于没有加入!!
		}
		log.info("动态数据源初始化结束");
	}

	/**
	 * 在数据源管理界面如果动态增加新的数据源,可以注入当前对象,调用此方法!
	 *
	 * @param dataSourceConf
	 */
	public void addNewDataSource(GenDataSourceConf dataSourceConf) {
		try {
			DruidDataSource newDataSource = defaultDataSource.cloneDruidDataSource();  // 克隆已有的数据源进行修改
			// 设定新的数据源的重要参数
			newDataSource.setUsername(dataSourceConf.getUserName());
			newDataSource.setPassword(stringEncryptor.decrypt(dataSourceConf.getPwd()));
			newDataSource.setUrl(dataSourceConf.getJdbcUrl());
			// newDataSource.setDriverClassName(dataSourceConf.getDriverClassName());  // 其实也可以默认
			newDataSource.init(); // 初始化数据源
			DynamicDataSourceProvider.addDataSource(dataSourceConf.getName(), newDataSource);  // 放入数据源集合中
			// 刷新所有的数据源
			roundRobinDataSouceProxy.setTargetDataSources(DynamicDataSourceProvider.dataSourceMap);
			roundRobinDataSouceProxy.afterPropertiesSet();
			log.info("数据源{}初始化完成!", dataSourceConf.getName());
		} catch (SQLException throwables) {
			log.error("数据源{}初始化失败!异常内容:{}", dataSourceConf.getName(), throwables.getMessage());
			throwables.printStackTrace();
		}
	}
}

使用案例:

   @Override
	@DynamicDataSource("#last")
    public Object getTables(String name, int[] startEnd, String dsName) {
        ...省略方法自己的业务逻辑...
    }

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值