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 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 local = new ThreadLocal();

	public static ThreadLocal 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 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 dataSourceConfs = (List) 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) {
        ...           ...
    }