Spring boot +jpa环境下,多数据源处理,并且在control进行切换数据源

2022-06-20 11:39:41

背景:在以往的项目中,存在这样的一个需求:没有实体(entity、pojo)的情况下,进行表的操作,比如查询某某表数据;以往的处理方式,只需要数据源的切换即可;现在的spring boot +jpa项目中,遇到一个问题:项目中大多数采用的是分包的方式进行数据源的注入(扫包方式),对于没有实体的数据源,便无法进行curd操作,原因是切换数据源失败;为了解决这个切换数据的问题,我们研究了网上不少案例,发现jpa是高度封装的一种多功能工具,切换数据源这伙,臣真的办不到啊(至少现在找不到办法),于是退而研究Jdbc Template,原因是jpa的底层也无非是对JdbcTemplate进行了高度封装处理。通过动态修改JdbcTemplate进行切换数据源的目的。具体步骤如下:

第一步:数据源整理

package com.app.wii.datasources;

import java.util.Map;

import javax.persistence.EntityManager;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.app.base.framework.jpa.support.BaseJpaRepositoryFactoryBean;

/**
 * base的数据和事物配置
 *
 * @Date 2018年8月1日
 * @author pangxianhe
 *
 */
@Configuration // 声名为配置类
@EnableTransactionManagement
@EnableJpaRepositories(
		// 指定EntityManager的创建工厂Bean
		entityManagerFactoryRef = "entityManagerFactoryBase",
		// 指定事物管理的Bean
		transactionManagerRef = "transactionManagerBase",
		// 设置Repository所在位置
		basePackages = { "com.app.base.modules" },
		// 覆盖SpringBoot提供的默认配置
		repositoryFactoryBeanClass = BaseJpaRepositoryFactoryBean.class)
public class BaseSourceConfig {

	@Autowired
	@Qualifier("baseDataSource")
	private DataSource baseDataSource;

	@Primary
	@Bean(name = "entityManagerBase")
	public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
		return entityManagerFactoryBase(builder).getObject().createEntityManager();
	}

	/**
	 * 配置实体管理工厂Bean
	 * @param builder
	 * @return
	 * @author pangxianhe
	 * @date 2018年8月2日
	 */
	@Primary
	@Bean(name = "entityManagerFactoryBase")
	public LocalContainerEntityManagerFactoryBean entityManagerFactoryBase(EntityManagerFactoryBuilder builder) {
		return builder.dataSource(baseDataSource)
				.properties(getVendorProperties())
				// 设置实体类所在位置
				.packages("com.app.base.modules")
				.persistenceUnit("basePersistenceUnit")
				.build();
	}

	@Autowired
	private JpaProperties jpaProperties;

	/**
	 * 拿到hibernate的通用配置
	 * @return
	 * @author pangxianhe
	 * @date 2018年8月2日
	 */
	private Map<String, Object> getVendorProperties() {
		return jpaProperties.getHibernateProperties(new HibernateSettings());
	}

	/**
	 * 配置事物管理的Bean
	 * @param builder
	 * @return
	 * @author pangxianhe
	 * @date 2018年8月2日
	 */
	@Primary
	@Bean(name = "transactionManagerBase")
	public PlatformTransactionManager transactionManagerBase(EntityManagerFactoryBuilder builder) {
		return new JpaTransactionManager(entityManagerFactoryBase(builder).getObject());
	}
	
	/**
	 *  向spring容器base数据源JdbcTemplate
	 * @param dataSource
	 * @return 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	@Bean(name = "baseDataSourceJdbcTemplate")
	public JdbcTemplate primaryJdbcTemplate(@Qualifier("baseDataSource") DataSource dataSource) {
		return new JdbcTemplate(dataSource);
	}
}

base数据,为主数据源,其中需要注意的是这个方法加载进容器中:primaryJdbcTemplate

package com.app.wii.datasources;

import java.util.Map;

import javax.persistence.EntityManager;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.app.base.framework.jpa.support.BaseJpaRepositoryFactoryBean;

/**
 * wii的数据和事物配置
 * @Date 2018年8月1日
 * @author pangxianhe
 */
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
		// 指定EntityManager的创建工厂Bean
		entityManagerFactoryRef = "entityManagerFactoryWii",
		// 指定事物管理的Bean
		transactionManagerRef = "transactionManagerWii",
		// 设置Repository所在位置
		basePackages = { "com.app.wii.modules" },
		// 覆盖SpringBoot提供的默认配置
		repositoryFactoryBeanClass = BaseJpaRepositoryFactoryBean.class)
public class WiiSourceConfig {

	@Autowired
	@Qualifier("wiiDataSource")
	private DataSource wiiDataSource;

	@Bean(name = "entityManagerWii")
	public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
		return entityManagerFactoryWii(builder).getObject().createEntityManager();
	}

	/**
	 * 配置实体管理工厂Bean
	 * @param builder
	 * @return 
	 * @author pangxianhe
	 * @date 2018年8月2日
	 */
	@Bean(name = "entityManagerFactoryWii")
	public LocalContainerEntityManagerFactoryBean entityManagerFactoryWii(EntityManagerFactoryBuilder builder) {
		return builder.dataSource(wiiDataSource)
				.properties(getVendorProperties())
				.packages("com.app.wii.modules") // 设置实体类所在位置
				.persistenceUnit("wiiPersistenceUnit").build();
	}

	@Autowired
	private JpaProperties jpaProperties;

	/**
	 * 拿到hibernate的通用配置
	 * @return 
	 * @author pangxianhe
	 * @date 2018年8月2日
	 */
	private Map<String, Object> getVendorProperties() {
		return jpaProperties.getHibernateProperties(new HibernateSettings());
	}

	/**
	 * 配置事物管理的Bean
	 * @param builder
	 * @return 
	 * @author pangxianhe
	 * @date 2018年8月2日
	 */
	@Bean(name = "transactionManagerWii")
	PlatformTransactionManager transactionManagerWii(EntityManagerFactoryBuilder builder) {
		return new JpaTransactionManager(entityManagerFactoryWii(builder).getObject());
	}
	/**
	 * 向spring容器wii数据源JdbcTemplate
	 * @param dataSource
	 * @return 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	@Bean(name = "wiiDataSourceJdbcTemplate")
	public JdbcTemplate primaryJdbcTemplate(@Qualifier("wiiDataSource") DataSource dataSource) {
		return new JdbcTemplate(dataSource);
	}
}

wii数据源

package com.app.wii.datasources;

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

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import com.alibaba.druid.pool.DruidDataSource;
import com.app.wii.properties.ActivitiDbProperties;
import com.app.wii.properties.BaseDbProperties;
import com.app.wii.properties.WiiDbProperties;

/**
 * 数据库连接属性配置
 * @date 2018年10月24日
 * @author pangxianhe
 * 
 */
@Configuration
@ServletComponentScan
public class DruidDataSourceConfig {

	@Autowired
	private BaseDbProperties baseDbProperties;
	@Autowired
	private WiiDbProperties wiiDbProperties;
	@Autowired
	private ActivitiDbProperties activitiDbProperties;

	/**
	 * base的配置
	 */
	@Bean
	@ConfigurationProperties(prefix = "spring.datasource.base") // 取application.yml文件内配置数据源的前缀
	public BaseDbProperties baseDbProperties() {
		return new BaseDbProperties();
	}

	/**
	 * wii的配置
	 */
	@Bean
	@ConfigurationProperties(prefix = "spring.datasource.wii")
	public WiiDbProperties wiiDbProperties() {
		return new WiiDbProperties();
	}

	/**
	 * activiti的配置
	 */
	@Bean
	@ConfigurationProperties(prefix = "spring.datasource.activiti")
	public ActivitiDbProperties activitiDbProperties() {
		return new ActivitiDbProperties();
	}

	
	
	/**
	 * base数据源
	 * @return
	 * @author pangxianhe
	 * @date 2018年8月1日
	 */
	@Bean(name = "baseDataSource") // 装配该方法返回值为userDataSource管理bean
	@Qualifier("baseDataSource") // spring装配bean唯一标识
	///@Primary // 通过@Primary表示主数据源。
	public DataSource baseDataSource() {
		DruidDataSource dataSource = new DruidDataSource();
		baseDbProperties.getDruidDataSource(dataSource);
		return dataSource;
	}

	/**
	 * wii数据源
	 * @return
	 * @author pangxianhe
	 * @date 2018年8月1日
	 */
	@Bean(name = "wiiDataSource")
	@Qualifier("wiiDataSource")
	public DataSource wiiDataSource() {
		DruidDataSource dataSource = new DruidDataSource();
		wiiDbProperties.getDruidDataSource(dataSource);
		return dataSource;
	}

	/**
	 * activiti数据源
	 * @return
	 * @date 2018年8月7日
	 */
	@Bean(name = "activitiDataSource")
	@Qualifier("activitiDataSource")
	public DataSource activitiDataSource() {
		DruidDataSource dataSource = new DruidDataSource();
		activitiDbProperties.getDruidDataSource(dataSource);
		return dataSource;
	}
	/**
	 * 配置动态数据源组合
	 * @param baseDataSource
	 * @param wiiDataSource
	 * @return 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	@Bean(name = "multipleDataSource")
	@Qualifier("multipleDataSource")
	@Primary
	public DataSource MultipleDataSourceToChoose(@Qualifier("baseDataSource") DataSource baseDataSource,
			@Qualifier("wiiDataSource") DataSource wiiDataSource) {
		DynamicDataSource dynamicDataSource = new DynamicDataSource();
		Map<Object, Object> targetDataSources = new HashMap<>();
		targetDataSources.put(DataSourceNames.BASE, baseDataSource);
		targetDataSources.put(DataSourceNames.WII, wiiDataSource);
		dynamicDataSource.setTargetDataSources(targetDataSources);
		dynamicDataSource.setDefaultTargetDataSource(wiiDataSource);
		dynamicDataSource.afterPropertiesSet();
		DynamicDataSource.saveDataSourceTypeName(DataSourceNames.BASE);
		DynamicDataSource.saveDataSourceTypeName(DataSourceNames.WII);
		return dynamicDataSource;
	}
	/**
	 * 向spring容器暴露组合数据源JdbcTemplate
	 * @param dataSource
	 * @return 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	@Bean(name = "multipleDataJdbcTemplate")
	public JdbcTemplate primaryJdbcTemplate(@Qualifier("multipleDataSource") DataSource dataSource) {
		return new JdbcTemplate(dataSource);
	}
}

动态(代理)数据源组装

第二步:数据切换方法编辑

package com.app.wii.datasources;

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;

/**
 * 切换数据源注解
 * @author pangxianhe
 * @date 2018年11月27日
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ChangDataSource {

	/**
	 * 自定义注解,通过注解可以切换数据源,默认数据源为base数据源
	 * @return 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	String name() default DataSourceNames.BASE;
}

自定义注解:进行切换数据源

package com.app.wii.datasources;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 
 * 多数据源切面处理类
 * @date 2018年11月22日
 * @author pangxianhe
 */
@Aspect
@Order(-10) //设置容器加载完成之前进行加载
@Component
public class DataSourceAspect {
	Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
	/**
	 * 前置通知
	 * @param point
	 * @param source
	 * @throws Exception 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	@Before(value = "@annotation(source)")
	public void changeDataSource(JoinPoint point, ChangDataSource source) throws Exception {
		String name = source.name();
		logger.info("change dataSource :" + name);
		if (!DynamicDataSource.checkDataSourceType(name)) {
			throw new Exception("数据源"+name+"不存在,使用默认数据源  ");
		}
		DynamicDataSource.setDataSourceType(name);

	}
	/**
	 * 后置通知
	 * @param point
	 * @param source
	 * @throws Exception 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	@AfterReturning(value = "@annotation(source)")
	public void restoreDataSource(JoinPoint point, ChangDataSource source) {
		// 方法执行完毕之后,销毁当前数据源信息,进行垃圾回收。
		DynamicDataSource.clearDataSourceType();
	}

}

数据源切面,保证在运行时进行数据的切换

package com.app.wii.datasources;

/**
 * 
 * 增加多数据源,在此配置
 * @date 2018年11月22日
 * @author pangxianhe
 */
public interface DataSourceNames {
	/**
	 * base数据源
	 */
	String BASE = "baseDataSource";

	/**
	 * wii数据源
	 */
	String WII = "wiiDataSource";

}
package com.app.wii.datasources;

import java.util.ArrayList;
import java.util.List;

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

/**切换数据源方法
 * @date 2018年11月22日
 * @author pangxianhe
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
	 /**
     * 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    /**
     * 校验输入的数据库名称是否正确
     */
    private static List<String> dataSourceList=new ArrayList<>();

    /**
     * 使用setDataSourceType设置当前的
     * @param dataSourceType
     */
    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    public static String getDataSourceType() {
        return contextHolder.get()==null?DataSourceNames.BASE:contextHolder.get();
    }


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

    public static void saveDataSourceTypeName(String name){
        dataSourceList.add(name);
    }

    public static boolean checkDataSourceType(String name){
        return dataSourceList.contains(name);
    }

	@Override
	protected Object determineCurrentLookupKey() {
		return getDataSourceType();
	}


}

数据源切换具体方法

第三步,提供常用的方法,建议,如果可以的请尽量用jpa提供的方法

package com.app.base.framework.jpa.service;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;

/**
 * @FileName BaseJdbcTemplateService.java
 * @Description: 1、由于jpa高度封装,分包指定数据源之后没办法进行数据源的切换
 *               2、如果存在实体的情况建议调用jpa原有的方法,不存在实体的情况下可以调用本类方法获取数据源
 *               3、本类提供常用方法,如果需要个性定制方法,请通过queryJdbcTemplate获取数据源进行处理
 *               4、如果需要原生的JdbcTemp方法,请如下引用
 *                				@Qualifier("multipleDataJdbcTemplate")
								@Autowired
								private JdbcTemplate jdbcTemplate;
 * @Date 2018年11月27日 
 * @author pangxianhe
 * 
 */
@Service
public abstract interface BaseJdbcTemplateService<T, ID extends Serializable> {
	/**
	 * 根据sql查询list数据 ,默认base数据源,调用前请确认数据源
	 * @param sql
	 * @return
	 * @throws Exception 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	public List<Map<String, Object>> queryListBySql(String sql) throws Exception;
	/**
	 * 根据sql查询Map数据 ,默认base数据源,调用前请确认数据源
	 * @param sql
	 * @return
	 * @throws Exception 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	public Map<String, Object> queryMapBySql(String sql) throws Exception;
	/**
	 * 获取数据源JdbcTemplate,通过JdbcTemplate进行数据操作
	 * @return
	 * @throws Exception 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	public JdbcTemplate queryJdbcTemplate() throws Exception;
	/**
	 * 执行sql
	 * @param sql
	 * @throws Exception 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	public void execute(String sql) throws Exception;
	/**
	 * 根据sql获取对象
	 * @param sql
	 * @param rowMapper 如果存在实体具体操作如下,否则建议采用queryForList 取第一条
	 * RowMapper<DemoUser> rowMapper = new BeanPropertyRowMapper<DemoUser>(DemoUser.class);
	 * @return
	 * @throws Exception 
	 * @author pangxianhe
	 * @date 2018年11月27日
	 */
	public <T> T queryObjectBySql(String sql,RowMapper<T> rowMapper) throws Exception;
}
package com.app.base.framework.jpa.service;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;

import com.app.base.kernel.model.exception.ServiceException;
import com.app.tool.core.lang.AssertUtil;

/**
 * @FileName BaseJdbcTemplateServiceImpl.java
 * @Description:
 *
 * @Date 2018年11月27日
 * @author pangxianhe
 * 
 */
@Service
public class BaseJdbcTemplateServiceImpl<T, ID extends Serializable> implements BaseJdbcTemplateService<T, ID> {
	@Qualifier("multipleDataJdbcTemplate")
	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Override
	public List<Map<String, Object>> queryListBySql(String sql) throws Exception {
		AssertUtil.notNull(sql, "sql must not be null");
		try {
			return jdbcTemplate.queryForList(sql);
		} catch (Exception e) {
			throw new ServiceException("Problem invoking method, Cause:" + e.getMessage(), (Throwable) e);
		}

	}

	@Override
	public Map<String, Object> queryMapBySql(String sql) throws Exception {
		AssertUtil.notNull(sql, "sql must not be null");
		try {
			return jdbcTemplate.queryForMap(sql);
		} catch (Exception e) {
			throw new ServiceException("Problem invoking method, Cause:" + e.getMessage(), (Throwable) e);
		}

	}

	@Override
	public JdbcTemplate queryJdbcTemplate() throws Exception {
		return jdbcTemplate;
	}

	@Override
	public void execute(String sql) throws Exception {
		AssertUtil.notNull(sql, "sql must not be null");
		try {
			jdbcTemplate.execute(sql);
		} catch (Exception e) {
			throw new ServiceException("Problem invoking method, Cause:" + e.getMessage(), (Throwable) e);
		}

	}

	@Override
	public <T> T queryObjectBySql(String sql, RowMapper<T> rowMapper) throws Exception {
		AssertUtil.notNull(sql, "sql must not be null");
		try {
			return jdbcTemplate.queryForObject(sql, rowMapper);
		} catch (Exception e) {
			throw new ServiceException("Problem invoking method, Cause:" + e.getMessage(), (Throwable) e);
		}

	}
}

以上实现了常用的具体方法,但是建议,请尽量用jpa提供的方法

第四步:调用方式

	@Autowired
	private BaseJdbcTemplateService<?, ?> baseJdbcTemplateService;
	
	public List<Map<String, Object>> addUser() throws Exception{
		String sql = "SELECT * FROM demo_user LIMIT 10";
		List<Map<String, Object>> List = baseJdbcTemplateService.queryListBySql(sql);
		sql = "SELECT * FROM demo_user where user_id='07M3PX'";
		Map<String, Object> map = baseJdbcTemplateService.queryMapBySql(sql);
		System.out.println(JSONArray.toJSONString(List));
		System.out.println(JSON.toJSON(map));
		/*String sql = "UPDATE demo_user set user_name='朱小丽2' where user_id='07M3PX'";
		baseJdbcTemplateService.execute(sql);*/
	
		return null;
	}

在service进行引入,调用

	@GetMapping(value = "/hi3")
	@ApiOperation(value = "多数据源测试", notes = "多数据源测试")
	//@ChangDataSource(name = DataSourceNames.WII)
	public Result hi3Service() throws Exception {
		//多种方式获取,也可以注解注入
		SqlCommonService sqlCommonService = (SqlCommonService) ApplicationContextUtil.getBean("sqlCommonService");
		//切换数据源
		DynamicDataSource.setDataSourceType(DataSourceNames.WII);
		System.out.println(DynamicDataSource.getDataSourceType());
		List<Map<String, Object>> list = sqlCommonService.addUser();
		//demoUserService.queryDemoUser();
		return null;
		///return Result.success().put("list", list);
	}

在control中进切换数据源,本人亲测试没毛病,现并未考虑联合事物控制,因为基本调用的是查询方法,后续如果需要再考虑事物的控制。以上两天想出来的方案,如果有更好的方法,请不吝赐教,谢谢

  • 作者:贤和兄
  • 原文链接:https://blog.csdn.net/u010598111/article/details/84568830
    更新时间:2022-06-20 11:39:41