原來寫過一篇關於SSM多數據源配置的博客,爲什麼今天又要寫一篇呢?當然是因爲需求的變更(蹭訪問量),原來的博客中,多個數據源是配置在xml文件中的,每一個數據源都對應了一個會話管理器dataSource,這樣就把數據源的數量給訂死了,你有幾個會話管理器就有幾個數據源,不太方便,所以這次想達到一個動態添加刪除數據源的效果.
原來的博文請見 https://blog.csdn.net/qq_37612755/article/details/82908700 寫的反正也不咋地
1.創建存放數據源信息的實體類
/**
* 數據源實體類
*/
public class SourceEntity {
private String url;
private String username;
private String password;
private String driver;
private String type;
private String beanKey;
public SourceEntity(String type,String driver,String url, String username, String password,String beanKey ) {
this.url = url;
this.username = username;
this.password = password;
this.driver = driver;
this.type = type;
this.beanKey = beanKey;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getBeanKey() {
return beanKey;
}
public void setBeanKey(String beanKey) {
this.beanKey = beanKey;
}
}
2.創建 CustomerContextHolder ,用於切換數據源信息和清除數據源信息
public class CustomerContextHolder {
public static final String DATASOURCE_DEFAULT = "dataSource";
public static final String DATASOURCE_REGION = "dataSourceRegion";
private static ThreadLocal<String> contentHolder = new ThreadLocal<>();
public static String getCustomerType() {
return contentHolder.get();
}
public static void setCustomerType(String customerType) {
contentHolder.set(customerType);
}
public static void clearCustomerType() {
contentHolder.remove();
}
}
3.在mybatis.xml 文件中添加下面的配置 ,dataSource 爲你的主數據庫,主數據庫還是使用xml配置的方式
<!--動態數據源配置-->
<bean id="dynamicDataSource" class="com.mlkj.common.config.dataSourceConfig.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!--現有數據源-->
<entry value-ref="dataSource" key="dataSource"/>
</map>
</property>
<!--默認數據源-->
<property name="defaultTargetDataSource" ref="dataSource"/>
</bean>
4.在 sqlSessionFactory 的 dataSource中引入 3 中的數據源配置
<!-- MyBatis begin -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dynamicDataSource"/>
<property name="typeAliasesPackage" value="com.mlkj"/>
<property name="typeAliasesSuperType" value="com.mlkj.common.persistence.BaseEntity"/>
<property name="mapperLocations" value="classpath:/mappings/**/*.xml"/>
<property name="configLocation" value="classpath:/mybatis-config.xml"></property>
</bean>
5 .創建 DynamicDataSource 繼承 AbstractRoutingDataSource 多數據源支持類,重寫 以下方法,當數據在請求數據庫之前,會調用 determineCurrentLookupKey 方法獲取到要使用的數據源標誌,如果沒有拿到,則使用默認數據源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
private ApplicationContext applicationContext;
@Lazy
@Autowired
private DynamicDataSourceSummoner summoner;
@Override
protected String determineCurrentLookupKey() {
System.out.println("當前線程:"+Thread.currentThread().getName()+"正在使用的數據源"+CustomerContextHolder.getCustomerType());
return com.mlkj.common.utils.StringUtils.isNotBlank(CustomerContextHolder.getCustomerType())?CustomerContextHolder.getCustomerType():CustomerContextHolder.DATASOURCE_DEFAULT;
}
@Override
protected DataSource determineTargetDataSource() {
String beanKey = CustomerContextHolder.getCustomerType();
//獲取當前線程中所使用的數據源標誌,然後去 applicationContext 中查看是否有對應的bean
if (!StringUtils.hasText(beanKey) || applicationContext.containsBean(beanKey)) {
return super.determineTargetDataSource();
}else{
//如果沒有對應的bean,則初始化
summoner.registerDynamicDataSources();
}
return super.determineTargetDataSource();
}
}
4. 創建 DynamicDataSourceSummoner 實現ApplicationListener<ContextRefreshedEvent>類,會在Spring 加載完成後執行,這時候我們將會話管理器註冊到bean中,Global.getConfig類爲讀取配置文件類,你們的可能不一樣
@Slf4j
@Component
public class DynamicDataSourceSummoner implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private ConfigurableApplicationContext applicationContext;
@Autowired
private DynamicDataSource dynamicDataSource;
private static boolean loaded = false;
/**
* Spring加載完成後執行
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 防止重複執行
if (!loaded) {
loaded = true;
try {
registerDynamicDataSources();
} catch (Exception e) {
e.printStackTrace();
System.out.println("數據源初始化失敗, Exception:" + e.getMessage());
}
}
}
/**
* 從數據庫讀取租戶的DB配置,並動態注入Spring容器
*/
public void registerDynamicDataSources() {
// 把數據源bean註冊到容器中
List<SourceEntity> entities = Lists.newArrayList();
//由於這裏只有一個數據源,所以就直接從配置文件中讀取了,以後如果有多個可以改成從主數據庫中讀取
SourceEntity entity = new SourceEntity(Global.getConfig("jdbc.typeRegion"),
Global.getConfig("jdbc.driverRegion"), Global.getConfig("jdbc.urlRegion"),
Global.getConfig("jdbc.usernameRegion"), Global.getConfig("jdbc.passwordRegion"), CustomerContextHolder.DATASOURCE_REGION);
entities.add(entity);
addDataSourceBeans(entities);
}
/**
* 根據DataSource創建bean並註冊到容器中
*/
private void addDataSourceBeans(List<SourceEntity> sourceEntities) {
Map<Object, Object> targetDataSources = Maps.newLinkedHashMap();
BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
for (SourceEntity entity : sourceEntities) {
// 如果該數據源已經在spring裏面註冊過,則不重新註冊
if (applicationContext.containsBean(entity.getBeanKey())) {
DruidDataSource existsDataSource = applicationContext.getBean(entity.getBeanKey(), DruidDataSource.class);
if (isSameDataSource(existsDataSource, entity)) {
continue;
}
}
// 組裝bean
AbstractBeanDefinition beanDefinition = getBeanDefinition(entity);
// 註冊bean
beanFactory.registerBeanDefinition(entity.getBeanKey(), beanDefinition);
// 放入map中,注意一定是剛纔創建bean對象
DruidDataSource bean = null;
try {
bean = applicationContext.getBean(entity.getBeanKey(), DruidDataSource.class);
targetDataSources.put(entity.getBeanKey(), bean);
CacheUtils.put(RealDataUtils.IS_SOURCE_BREAK, false);
} catch (Exception e) {
e.printStackTrace();
//如果報錯,刪除註冊的bean
beanFactory.removeBeanDefinition(entity.getBeanKey());
try {
Boolean isSourceBreak = (Boolean) CacheUtils.get(RealDataUtils.IS_SOURCE_BREAK);
if (isSourceBreak == null || !isSourceBreak) {
RealDataUtils.sendMessage("區域庫", e.getMessage(), new Date());
CacheUtils.put(RealDataUtils.IS_SOURCE_BREAK, true);
}
} catch (Exception ex) {
ex.printStackTrace();
}
throw e;
}
}
// 將創建的map對象set到 targetDataSources;
dynamicDataSource.setTargetDataSources(targetDataSources);
// 必須執行此操作,纔會重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有這樣,動態切換纔會起效
dynamicDataSource.afterPropertiesSet();
}
/**
* 組裝數據源spring bean
*/
private AbstractBeanDefinition getBeanDefinition(SourceEntity entity) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
builder.getBeanDefinition().setAttribute("id", entity.getBeanKey());
// 其他配置繼承defaultDataSource
//builder.setParentName(CustomerContextHolder.DATASOURCE_REGION);
builder.setInitMethodName("init");
builder.setDestroyMethodName("close");
builder.addPropertyValue("name", entity.getBeanKey());
builder.addPropertyValue("url", entity.getUrl());
builder.addPropertyValue("username", entity.getUsername());
builder.addPropertyValue("password", entity.getPassword());
//配置初始化大小、最小、最大
builder.addPropertyValue("initialSize", Global.getConfig("jdbc.pool.init"));
builder.addPropertyValue("minIdle", Global.getConfig("jdbc.pool.minIdle"));
builder.addPropertyValue("maxActive", Global.getConfig("jdbc.pool.maxActive"));
//配置獲取連接等待超時的時間
builder.addPropertyValue("maxWait", "60000");
// 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
builder.addPropertyValue("timeBetweenEvictionRunsMillis", "60000");
//連接重試時間
builder.addPropertyValue("timeBetweenConnectErrorMillis", "5000");
//配置一個連接在池中最小生存的時間,單位是毫秒
builder.addPropertyValue("minEvictableIdleTimeMillis", "300000");
builder.addPropertyValue("validationQuery", Global.getConfig("jdbc.testSql"));
builder.addPropertyValue("testWhileIdle", "true");
builder.addPropertyValue("testOnBorrow", "false");
builder.addPropertyValue("testOnReturn", "false");
//數據庫連接錯誤時關閉連接
/*builder.addPropertyValue("breakAfterAcquireFailure", "true");
builder.addPropertyValue("connectionErrorRetryAttempts", "0");*/
//打開PSCache,並且指定每個連接上PSCache的大小(Oracle使用)
//builder.addPropertyValue("poolPreparedStatements", "true");
//builder.addPropertyValue("maxPoolPreparedStatementPerConnectionSize", "20");
//以下配置建議在調試時使用,線上會影響性能
//超過時間限制是否回收
//builder.addPropertyValue("removeAbandoned", "true");
//超時時間;單位爲秒。180秒=3分鐘
//builder.addPropertyValue("removeAbandonedTimeout", "180");
//關閉abanded連接時輸出錯誤日誌
builder.addPropertyValue("logAbandoned", "true");
//配置監控統計攔截的filters
builder.addPropertyValue("filters", "stat");
//builder.addPropertyValue("connectionProperties", DataSourceUtil.getConnectionProperties(entity.getDbPublicKey()));
return builder.getBeanDefinition();
}
/**
* 判斷Spring容器裏面的DataSource與數據庫的DataSource信息是否一致
* 備註:這裏沒有判斷public_key,因爲另外三個信息基本可以確定唯一了
*/
private boolean isSameDataSource(DruidDataSource existsDataSource, SourceEntity entity) {
boolean sameUrl = Objects.equals(existsDataSource.getUrl(), entity.getUrl());
if (!sameUrl) {
return false;
}
boolean sameUser = Objects.equals(existsDataSource.getUsername(), entity.getUsername());
if (!sameUser) {
return false;
}
try {
//String decryptPassword = ConfigTools.decrypt(entity.getDbPublicKey(), entity.getDbPassword());
return Objects.equals(existsDataSource.getPassword(), entity.getPassword());
} catch (Exception e) {
e.printStackTrace();
System.out.println("數據源密碼校驗失敗,Exception:{}" + e.getMessage());
return false;
}
}
5 完整的mybatis.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd"
default-lazy-init="true">
<!-- MyBatis begin -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dynamicDataSource"/>
<property name="typeAliasesPackage" value="com.mlkj"/>
<property name="typeAliasesSuperType" value="com.mlkj.common.persistence.BaseEntity"/>
<property name="mapperLocations" value="classpath:/mappings/**/*.xml"/>
<property name="configLocation" value="classpath:/mybatis-config.xml"></property>
</bean>
<!-- 掃描basePackage下所有以@MyBatisDao註解的接口 -->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="basePackage" value="com.mlkj"/>
<property name="annotationClass" value="com.mlkj.common.persistence.annotation.MyBatisDao"/>
</bean>
<!-- 定義事務 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dynamicDataSource" />
</bean>
<!-- 配置 Annotation 驅動,掃描@Transactional註解的類定義事務 -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
<!-- MyBatis end -->
<!-- 數據源配置, 使用 BoneCP 數據庫連接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 數據源驅動類可不寫,Druid默認會自動根據URL識別DriverClass -->
<property name="driverClassName" value="${jdbc.driver}" />
<!-- 基本屬性 url、user、password -->
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${jdbc.pool.init}" />
<property name="minIdle" value="${jdbc.pool.minIdle}" />
<property name="maxActive" value="${jdbc.pool.maxActive}" />
<!-- 配置獲取連接等待超時的時間 -->
<property name="maxWait" value="60000" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="validationQuery" value="${jdbc.testSql}" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<!-- 打開PSCache,並且指定每個連接上PSCache的大小(Oracle使用)
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> -->
<!--以下配置建議在調試時使用,線上會影響性能-->
<!-- 超過時間限制是否回收
<property name="removeAbandoned" value="true" /> -->
<!-- 超時時間;單位爲秒。180秒=3分鐘
<property name="removeAbandonedTimeout" value="180" /> -->
<!-- 關閉abanded連接時輸出錯誤日誌
<property name="logAbandoned" value="true" /> -->
<!-- 配置監控統計攔截的filters -->
<property name="filters" value="stat" />
</bean>
<!--動態數據源配置-->
<bean id="dynamicDataSource" class="com.mlkj.common.config.dataSourceConfig.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!--現有數據源-->
<entry value-ref="dataSource" key="dataSource"/>
</map>
</property>
<!--默認數據源-->
<property name="defaultTargetDataSource" ref="dataSource"/>
</bean>
</beans>
當我們需要切換數據源的時候,只需要在連接數據庫之前調用 CustomerContextHolder.setCustomerType(CustomerContextHolder.DATASOURCE_REGION); 就可以了, CustomerContextHolder.DATASOURCE_REGION爲你對應的數據源信息,數據庫查詢完成後在調用CustomerContextHolder.clearCustomerType(); 就可以切換會默認數據源
例子:
//切換數據源
CustomerContextHolder.setCustomerType(CustomerContextHolder.DATASOURCE_REGION);
syncDataDao.findList(new SyncTable())
//切回到默認數據源
CustomerContextHolder.clearCustomerType();