背景:在以往的項目中,存在這樣的一個需求:沒有實體(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中進切換數據源,本人親測試沒毛病,現並未考慮聯合事物控制,因爲基本調用的是查詢方法,後續如果需要再考慮事物的控制。以上兩天想出來的方案,如果有更好的方法,請不吝賜教,謝謝