版權聲明:本文為博主原創文章,遵循 CC 4.0 by-sa 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/qq_37279783/article/details/82013702
這是實際應用場景中的多數據源切換案例
邏輯思路如下:
1.系統初始化,加載所有數據庫中配置的數據源,加載進去spring容器
2.通過兩種方法切換數據源:
2.1MultipleDataSource.setDataSourceKey(dataSourceKey);//切換
MultipleDataSource.clearDataSourceKey();//清除當前數據源並且還原到默認數據庫
2.2使用@SwitchDataSource(name="dateSourceKey")注解
<beans>
<-- 配置默認數據源 -->
<context:property-placeholder location="classpath*:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="minIdle" value="${rapid.jdbcPool.minPoolSize}"/>
<property name="maxActive" value="${rapid.jdbcPool.maxPoolSize}"/>
<property name="initialSize"
value="${rapid.jdbcPool.initialPoolSize}"/><!-- 初始化時建立物理連接的個數。初始化發生在顯示調用init方法,或者第一次getConnection時,default=0 -->
<property name="maxWait"
value="${rapid.jdbcPool.maxWait}"/><!-- 獲取連接時最大等待時間,單位毫秒。配置了maxWait之后,缺省啟用公平鎖,並發效率會有所下降,如果需要可以通過配置useUnfairLock屬性為true使用非公平鎖 -->
<property name="timeBetweenEvictionRunsMillis"
value="${rapid.jdbcPool.timeBetweenEvictionRunsMillise}"/><!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,毫秒 -->
<property name="minEvictableIdleTimeMillis"
value="${rapid.jdbcPool.minEvictableIdleTimeMillis}"/><!-- 配置一個連接在池中最小生存的時間,毫秒 -->
<!-- 建議配置為true,不影響性能,並且保證安全性。申請連接的時候檢測,如果空閑時間大於 timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效 -->
<property name="testWhileIdle" value="${rapid.jdbcPool.testWhileIdle}"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<!-- 用來檢測連接是否有效的sql,要求是一個查詢語句。如果validationQuery為null,testOnBorrow、testOnReturn、testWhileIdle都無效 -->
<property name="validationQuery" value="${rapid.jdbcPool.validationQuery}"/>
<!-- #是否緩存preparedStatement,也就是PSCache。PSCache對支持游標的數據庫性能提升巨大,比如說oracle。在mysql下建議關閉,default=false -->
<property name="poolPreparedStatements" value="${rapid.jdbcPool.poolPreparedStatements}"/>
<!-- #要啟用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改為true。
#在Druid中,不會存在Oracle下PSCache占用內存過多的問題,可以把這個數值配置大一些,比如說100 -->
<property name="maxOpenPreparedStatements" value="${rapid.jdbcPool.maxOpenPreparedStatements}"/>
<!-- #屬性類型是字符串,通過別名的方式配置擴展插件,常用的插件有:
監控統計用的filter:stat
日志用的filter:log4j
防御sql注入的filter:wall -->
<!--<property name="filters" value="stat"/>-->
<property name="timeBetweenLogStatsMillis"
value="${rapid.jdbcPool.timeBetweenLogStatsMillis}"/><!-- 定時把監控數據輸出到日志中,毫秒 -->
<property name="proxyFilters">
<list>
<ref bean="stat-filter"/>
<ref bean="log-filter"/>
</list>
</property>
</bean>
<!-- 多數據源切換 -->
<bean id="multipleDataSource" class="cn.core.persistent.hibernate.datasource.MultipleDataSource">
<property name="defaultTargetDataSource" ref="dataSource"/>
<property name="targetDataSources">
<map>
<entry key="dataSource" value-ref="dataSource"/>
</map>
</property>
</bean>
<!-- 多數據源注入 -->
<bean id="loadDataSource" class="cn.core.persistent.hibernate.datasource.LoadDataSource">
<property name="baseExtendDao" ref="baseExtendDao"/>
</bean>
<!-- 聲明事務 -->
<bean id="txManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="dataSource" ref="multipleDataSource"/>
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<aop:config>
<aop:pointcut id="myMethod" expression="execution(* cn.*.service..*.*(..))"/>
<aop:advisor advice-ref="multipleDataSource" order="1"
pointcut="execution(* cn.*.service..*.*(..)) and @annotation(cn.core.persistent.hibernate.datasource.SwitchDataSource)"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="myMethod" order="2"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="append*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="remove*" propagation="REQUIRED"/>
<tx:method name="repair*" propagation="REQUIRED"/>
<tx:method name="delAndRepair*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="modify*" propagation="REQUIRED"/>
<tx:method name="edit*" propagation="REQUIRED"/>
<tx:method name="init*" propagation="REQUIRED"/>
<tx:method name="test*" propagation="REQUIRED"
rollback-for="Exception"/><!-- 如果不寫 rollback-for它弄死都不給你回滾,這個東西弄了半天 -->
<tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="load*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="search*" propagation="SUPPORTS" read-only="true"/>
<!--指定當前方法以非事務方式執行操作,如果當前存在事務,就把當前事務掛起,等我以非事務的狀態運行完,再繼續原來的事務。查詢定義即可 read-only="true" 表示只讀-->
<tx:method name="*" propagation="NOT_SUPPORTED" read-only="true"/>
<!--<tx:method name="logical*" propagation="REQUIRED" /><!– 一般指邏輯刪除 –>-->
<!--<tx:method name="batch*" propagation="REQUIRED" /><!– 批量操作 –>-->
</tx:attributes>
</tx:advice>
</beans>
import cn.commons.string.StringUtils;
import cn.core.model.datasource.DataSource;
import cn.core.model.logger.DruidSQLLoggerMonitorAppender;
import cn.core.persistent.hibernate.AbstractHibernateDao;
import cn.core.persistent.hibernate.extend.BaseExtendDao;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Query;
import org.hibernate.Session;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* 多數據源加載
* @bug [nothing]
* @see [nothing]
* @備注:
*/
public class LoadDataSource extends AbstractHibernateDao {
protected final Log logger = LogFactory.getLog(getClass());
private BaseExtendDao baseExtendDao;
/**
* 加載數據庫內配置的數據源
* */
public List<DataSource> selectDataSourceForDateBase() {
Session session = null;
try {
session = super.getSessionFactory().openSession();
//查詢數據庫中所有數據源數據
Query query = session.getNamedQuery("DataSource.findAll");
return query.list();
} catch (Exception e) {
} finally {
if (session != null) {
session.flush();
session.close();
}
}
return new ArrayList();
}
/**
* 創建數據源並注入進 spring 容器
* */
public void buiderDataSourceToSpring(ApplicationContext applicationContext) {
//加載數據源
//根據數據源,創建 DruidDataSource.class
//重新創建多數據源
List<DataSource> listDataSource = this.selectDataSourceForDateBase();
if (listDataSource.size() > 0) {
//默認數據源bean的id
DruidDataSource druidDataSourceDefault = applicationContext.getBean(SwitchDataSource.master, DruidDataSource.class);
Map<Object, Object> targetDataSources = new HashedMap();
targetDataSources.put(SwitchDataSource.master, druidDataSourceDefault);//支撐庫
//Bean的實例工廠
DefaultListableBeanFactory dbf = (DefaultListableBeanFactory) ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
for (Iterator<DataSource> iterator = listDataSource.iterator(); iterator.hasNext(); ) {
DataSource ds = iterator.next();
//Bean構建 BeanService.class 要創建的Bean的Class對象
BeanDefinitionBuilder dataSourceBuider = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
//向里面的屬性注入值,提供get set方法
toolBuiderDataSourceToSpring(dataSourceBuider, ds);
dataSourceBuider.setInitMethodName("init");
dataSourceBuider.setDestroyMethodName("close");
//將實例注冊spring容器中 key 等同於 id配置
dbf.registerBeanDefinition(ds.getDataSourceKey(), dataSourceBuider.getBeanDefinition());
DruidDataSource druidDataSource = applicationContext.getBean(ds.getDataSourceKey(), DruidDataSource.class);
targetDataSources.put(ds.getDataSourceKey(), druidDataSource);
logger.info("加載數據源:名稱->[" + ds.getDataSourceKey() + "],url->[" + druidDataSource.getUrl() + "],driver->[" + druidDataSource.getDriverClassName() + "],username->[" + druidDataSource.getUsername() + "]");
}
//重構多數據源列表
MultipleDataSource multipleDataSource = applicationContext.getBean("multipleDataSource", MultipleDataSource.class);
multipleDataSource.setTargetDataSources(targetDataSources);
multipleDataSource.afterPropertiesSet();
}
}
/**
* 數據庫配置的數據源注入容器
*
* @title toolBuiderDataSourceToSpring
* @param dataSourceBuider spring 容器注入實例
* ds 數據庫配置的數據源
* @return
* @throws
*/
private void toolBuiderDataSourceToSpring(BeanDefinitionBuilder dataSourceBuider, DataSource ds) {
Assert.notNull(ds, "多數據源注入異常 -> 配置的數據源不能為 null!!");
Assert.notNull(dataSourceBuider, "多數據源注入異常 -> Spring 容器為 null!!");
Assert.hasText(ds.getDriverClassName(), "多數據源加載異常 -> [" + ds.getDataSourceKey() + "] -> [driverClassName is null]");
Assert.hasText(ds.getUrl(), "多數據源加載異常 -> [" + ds.getDataSourceKey() + "] -> [url is null]");
Assert.hasText(ds.getUsername(), "多數據源加載異常 -> [" + ds.getDataSourceKey() + "] -> [username is null]");
Assert.hasText(ds.getPassword(), "多數據源加載異常 -> [" + ds.getDataSourceKey() + "] -> [password is null]");
dataSourceBuider.addPropertyValue("driverClassName", ds.getDriverClassName());
dataSourceBuider.addPropertyValue("url", ds.getUrl());
dataSourceBuider.addPropertyValue("username", ds.getUsername());
dataSourceBuider.addPropertyValue("password", ds.getPassword());
if (StringUtils.hasText(ds.getConnectPoolProperties())) {
Map map = StringUtils.parseStringToMap(ds.getConnectPoolProperties(), ",", "=");
if (map.containsKey("minPoolSize")) {//最小連接
dataSourceBuider.addPropertyValue("minIdle", map.get("minPoolSize"));
}
if (map.containsKey("maxPoolSize")) {//最大連接
dataSourceBuider.addPropertyValue("maxActive", map.get("maxPoolSize"));
}
if (map.containsKey("initialPoolSize")) {//初始連接數
dataSourceBuider.addPropertyValue("initialSize", map.get("initialPoolSize"));
}
if (map.containsKey("maxWait")) {//獲取連接時最大等待時間
dataSourceBuider.addPropertyValue("maxWait", map.get("maxWait"));
}
if (map.containsKey("timeBetweenEvictionRunsMillis")) {//配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,毫秒
dataSourceBuider.addPropertyValue("timeBetweenEvictionRunsMillis", map.get("timeBetweenEvictionRunsMillis"));
}
if (map.containsKey("minEvictableIdleTimeMillis")) {//配置一個連接在池中最小生存的時間,毫秒
dataSourceBuider.addPropertyValue("minEvictableIdleTimeMillis", map.get("minEvictableIdleTimeMillis"));
}
//是否緩存preparedStatement,也就是PSCache。PSCache對支持游標的數據庫性能提升巨大,比如說oracle。在mysql下建議關閉,default=false
if (map.containsKey("poolPreparedStatements")) {
dataSourceBuider.addPropertyValue("poolPreparedStatements", map.get("poolPreparedStatements"));
}
//#要啟用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改為true。
//#在Druid中,不會存在Oracle下PSCache占用內存過多的問題,可以把這個數值配置大一些,比如說100
if (map.containsKey("maxOpenPreparedStatements")) {
dataSourceBuider.addPropertyValue("maxOpenPreparedStatements", map.get("maxOpenPreparedStatements"));
}
//用來檢測連接是否有效的sql,要求是一個查詢語句。如果validationQuery為null,testOnBorrow、testOnReturn、testWhileIdle都無效
if (map.containsKey("validationQuery")) {
String vq = (String) map.get("validationQuery");
if (StringUtils.hasText(vq)) {
dataSourceBuider.addPropertyValue("validationQuery", map.get("validationQuery"));
//建議配置為true,不影響性能,並且保證安全性。申請連接的時候檢測,如果空閑時間大於 timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效
if (map.containsKey("testWhileIdle")) {
dataSourceBuider.addPropertyValue("testWhileIdle", map.get("testWhileIdle"));
}
if (map.containsKey("testOnBorrow")) {
dataSourceBuider.addPropertyValue("testOnBorrow", map.get("testOnBorrow"));
}
if (map.containsKey("testOnReturn")) {
dataSourceBuider.addPropertyValue("testOnReturn", map.get("testOnReturn"));
}
}
}
//是否日志打印
if (map.containsKey("statementExecutableSqlLogEnable")) {
boolean isStatementExecutableSqlLogEnable = (Boolean) map.get("statementExecutableSqlLogEnable");
if (isStatementExecutableSqlLogEnable) {
DruidSQLLoggerMonitorAppender log4jFilter = new DruidSQLLoggerMonitorAppender();
log4jFilter.setStatementExecutableSqlLogEnable(true);
if (map.containsKey("serializationCurrentThreadExecutableSqlLogEnable")) {
log4jFilter.setSerializationCurrentThreadExecutableSqlLogEnable((Boolean) map.get("serializationCurrentThreadExecutableSqlLogEnable"));
}
List list = new ArrayList();
list.add(log4jFilter);
dataSourceBuider.addPropertyValue("proxyFilters", list);
}
}
}
}
public BaseExtendDao getBaseExtendDao() {
return baseExtendDao;
}
public void setBaseExtendDao(BaseExtendDao baseExtendDao) {
this.baseExtendDao = baseExtendDao;
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchDataSource {
String master = "dataSource";
String dataSourceKey() default SwitchDataSource.master;
}
import cn.commons.string.StringUtils;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.*;
/**
* 多數據源切換
* @bug [nothing]
* @see [nothing]
* @備注:
*/
public class MultipleDataSource extends AbstractRoutingDataSource implements MethodInterceptor {
//保存當前線程使用的數據源
private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal();
//設置數據源
public static void setDataSourceKey(String dataSourceName) {
if (StringUtils.hasText(dataSourceName)) {
MultipleDataSource.dataSourceKey.set(dataSourceName);
}
}
//清除數據源,清除后使用默認的支撐庫數據源
public static void clearDataSourceKey() {
MultipleDataSource.dataSourceKey.remove();
}
/**
*
* sessionFactory 加載數據源擴展方法
* 詳情請看 AbstractRoutingDataSource.determineTargetDataSource() 方法;
* 這是一個擴展,返回的是數據源的名稱,也就是 spring 配置的 baenName,通過返回的 beanName未找到數據源會直接使用支撐庫做為數據源
* */
@Override
protected Object determineCurrentLookupKey() {
//這里返回 null或"" 沒有關系,在 bean-core.xml 我們注入了默認數據源
return MultipleDataSource.dataSourceKey.get();
}
/**
* 結合 spring aop 與 annotation 動態切換數據源.
* 框架通過 Aop 實現了事務管理,那么數據源的切換與事務控制作用於相同的時候方法上,並且優先於事務控制切換數據源.詳細配置請參考 beans-core.xml
*
* 如何使用:
* 在需要切換數據源的service層的方法上,配置注解標簽,並指定數據源名稱,代碼如下:
* <code>
* @SwitchDataSource(name="dateSourceKey")
* public List getProducts(){
* ...
* }
* </code>
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result = null;
SwitchDataSource switchDataSource = null;
try {
switchDataSource = invocation.getMethod().getAnnotation(SwitchDataSource.class);//接口方法定義的注解
if (switchDataSource == null) {
// Class[] argsClass = new Class[invocation.getArguments().length];
// for (int i = 0, j = invocation.getArguments().length; i < j; i++) {
// argsClass[i] = invocation.getArguments()[i].getClass();
// }
//上面的獲取方式有可能來至於接口,這里嘗試在實現類獲取注解
// switchDataSource = invocation.getThis().getClass().getMethod(invocation.getMethod().getName(), argsClass).getAnnotation(SwitchDataSource.class);
// 20170526 修改
// 在接口中定義方法時,一般的形參都會實現父類或者接口.而實現類傳入的實參又會是具體的實現類.所以導致直接使用 getMethod(String name, Class<?>... parameterTypes) 是無法獲取方法的.
//嘗試在實現類獲取注解
String methodName = invocation.getMethod().getName();
Object[] args = invocation.getArguments();//實際參數;如 arraylist
Method[] methods = invocation.getThis().getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(methodName)) {//方法相同
Class[] typeClasss = methods[i].getParameterTypes();//定義的參數類型;如 list
if (typeClasss.length == args.length) {//參數數量相等
if (typeClasss.length == 0) {//無參情況
switchDataSource = methods[i].getAnnotation(SwitchDataSource.class);
break;
} else {
boolean isTrue = false;
//判斷所有的參數的類型是否相同
for (int j = 0; j < args.length; j++) {
if (typeClasss[j].isAssignableFrom(args[j].getClass())) {
isTrue = true;
} else {
isTrue = false;
break;
}
}
if (isTrue) {
switchDataSource = methods[i].getAnnotation(SwitchDataSource.class);
break;
}
}
}
}
}
}
if (switchDataSource != null) {
MultipleDataSource.setDataSourceKey(switchDataSource.dataSourceKey());
}
result = invocation.proceed();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (switchDataSource != null) {
MultipleDataSource.clearDataSourceKey();//還原數據源;
}
}
return result;
}
public static void main(String[] args) {
System.out.println(ArrayList.class.isAssignableFrom(List.class));
}
}
//系統初始化加載數據庫中的數據源
import javax.servlet.ServletException;
import cn.core.persistent.hibernate.datasource.LoadDataSource;
import cn.core.persistent.hibernate.datasource.MultipleDataSource;
import org.apache.commons.collections.map.HashedMap;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
*
* 提供框架啟動初始化擴展功能,繼承 DispatcherServlet 並實現 initFrameworkServlet() 方法,該方法原生提供 spring 擴展
*
* 目前提供擴展如下:
* 1.加載多數據源
* @類名稱:RapidFrameWorkServlet
*/
public class RapidFrameWorkServlet extends DispatcherServlet {
private static final long serialVersionUID = 1L;
/**
* 覆寫spring的一個方法用來初始化進而得到初始化參數
*/
@Override
public void initFrameworkServlet() throws ServletException {
try {
//加載多數據源
super.getWebApplicationContext().getBean("loadDataSource", LoadDataSource.class).buiderDataSourceToSpring(super.getWebApplicationContext());
} catch (Exception e) {
e.printStackTrace();
}
}
}
<-- 數據庫中數據源對象 -->
CREATE TABLE DATASOURCE
(
DATASOURCE_GUID VARCHAR2(32 BYTE),
DATASOURCE_NAME VARCHAR2(128 BYTE),
DATASOURCE_KEY VARCHAR2(128 BYTE) NOT NULL,
DRIVER_CLASS_NAME VARCHAR2(256 BYTE) NOT NULL,
URL VARCHAR2(512 BYTE) NOT NULL,
USERNAME VARCHAR2(128 BYTE) NOT NULL,
PASSWORD VARCHAR2(128 BYTE) NOT NULL,
CONNECT_POOL_PROPERTIES VARCHAR2(1024 BYTE),
OPERATOR_ID VARCHAR2(64 BYTE),
CREATE_TIME DATE,
MODIFY_TIME DATE,
RES_ACT_MAP_ID VARCHAR2(64 BYTE),
VALID_SIGN NUMBER
)
//數據源控制層類,在新增數據源后重新加載容器的數據源
public class DataSourceResource implements ApplicationContextAware {
@Resource
private IDataSourceService dataSourceService;
@Resource(name="loadDataSource")
private LoadDataSource loadDataSource;
private ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext paramApplicationContext) throws BeansException {
applicationContext=paramApplicationContext;
}
/**
* 添加數據
* @param dataSourceType
* @return
*/
@RequestMapping("/put")
@ResponseBody
@RepeatSubmitToken
public boolean addDataSourceTypeAction(DataSource dataSource){
boolean flag= dataSourceService.selectDataSourceName(dataSourceType.getDataSourceName());
if(flag){
return false;
}
boolean isSuccess = dataSourceService.addDataSourceType(dataSource);
//數據源添加成功后重新加載數據源
loadDataSource.buiderDataSourceToSpring(applicationContext);
return isSuccess;
}
}
————————————————
版權聲明:本文為CSDN博主「小蝸牛的路」的原創文章,遵循CC 4.0 by-sa版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_37279783/article/details/82013702