现实项目项目中,很多人都有多数据源的需求,其实在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) {
...省略方法自己的业务逻辑...
}