通過(guò)Spring Boot配置動(dòng)態(tài)數(shù)據(jù)源訪問多個(gè)數(shù)據(jù)庫(kù)的實(shí)現(xiàn)代碼
之前寫過(guò)一篇博客《Spring+Mybatis+Mysql搭建分布式數(shù)據(jù)庫(kù)訪問框架》描述如何通過(guò)Spring+Mybatis配置動(dòng)態(tài)數(shù)據(jù)源訪問多個(gè)數(shù)據(jù)庫(kù)。但是之前的方案有一些限制(原博客中也描述了):只適用于數(shù)據(jù)庫(kù)數(shù)量不多且固定的情況。針對(duì)數(shù)據(jù)庫(kù)動(dòng)態(tài)增加的情況無(wú)能為力。
下面講的方案能支持?jǐn)?shù)據(jù)庫(kù)動(dòng)態(tài)增刪,數(shù)量不限。
數(shù)據(jù)庫(kù)環(huán)境準(zhǔn)備
下面一Mysql為例,先在本地建3個(gè)數(shù)據(jù)庫(kù)用于測(cè)試。需要說(shuō)明的是本方案不限數(shù)據(jù)庫(kù)數(shù)量,支持不同的數(shù)據(jù)庫(kù)部署在不同的服務(wù)器上。如圖所示db_project_001、db_project_002、db_project_003。

搭建Java后臺(tái)微服務(wù)項(xiàng)目
創(chuàng)建一個(gè)Spring Boot的maven項(xiàng)目:

config:數(shù)據(jù)源配置管理類。
datasource:自己實(shí)現(xiàn)的數(shù)據(jù)源管理邏輯。
dbmgr:管理了項(xiàng)目編碼與數(shù)據(jù)庫(kù)IP、名稱的映射關(guān)系(實(shí)際項(xiàng)目中這部分?jǐn)?shù)據(jù)保存在redis緩存中,可動(dòng)態(tài)增刪)。
mapper:數(shù)據(jù)庫(kù)訪問接口。
model:映射模型。
rest:微服務(wù)對(duì)外發(fā)布的restful接口,這里用來(lái)測(cè)試。
application.yml:配置了數(shù)據(jù)庫(kù)的JDBC參數(shù)。
詳細(xì)的代碼實(shí)現(xiàn)
1. 添加數(shù)據(jù)源配置
package com.elon.dds.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.elon.dds.datasource.DynamicDataSource;
/**
* 數(shù)據(jù)源配置管理。
*
* @author elon
* @version 2018年2月26日
*/
@Configuration
@MapperScan(basePackages="com.elon.dds.mapper", value="sqlSessionFactory")
public class DataSourceConfig {
/**
* 根據(jù)配置參數(shù)創(chuàng)建數(shù)據(jù)源。使用派生的子類。
*
* @return 數(shù)據(jù)源
*/
@Bean(name="dataSource")
@ConfigurationProperties(prefix="spring.datasource")
public DataSource getDataSource() {
DataSourceBuilder builder = DataSourceBuilder.create();
builder.type(DynamicDataSource.class);
return builder.build();
}
/**
* 創(chuàng)建會(huì)話工廠。
*
* @param dataSource 數(shù)據(jù)源
* @return 會(huì)話工廠
*/
@Bean(name="sqlSessionFactory")
public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
try {
return bean.getObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
2.定義動(dòng)態(tài)數(shù)據(jù)源
1) 首先增加一個(gè)數(shù)據(jù)庫(kù)標(biāo)識(shí)類,用于區(qū)分不同的數(shù)據(jù)庫(kù)訪問。
由于我們?yōu)椴煌膒roject創(chuàng)建了單獨(dú)的數(shù)據(jù)庫(kù),所以使用項(xiàng)目編碼作為數(shù)據(jù)庫(kù)的索引。而微服務(wù)支持多線程并發(fā)的,采用線程變量。
package com.elon.dds.datasource;
/**
* 數(shù)據(jù)庫(kù)標(biāo)識(shí)管理類。用于區(qū)分?jǐn)?shù)據(jù)源連接的不同數(shù)據(jù)庫(kù)。
*
* @author elon
* @version 2018-02-25
*/
public class DBIdentifier {
/**
* 用不同的工程編碼來(lái)區(qū)分?jǐn)?shù)據(jù)庫(kù)
*/
private static ThreadLocal<String> projectCode = new ThreadLocal<String>();
public static String getProjectCode() {
return projectCode.get();
}
public static void setProjectCode(String code) {
projectCode.set(code);
}
}
2) 從DataSource派生了一個(gè)DynamicDataSource,在其中實(shí)現(xiàn)數(shù)據(jù)庫(kù)連接的動(dòng)態(tài)切換
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.apache.tomcat.jdbc.pool.PoolProperties;
import com.elon.dds.dbmgr.ProjectDBMgr;
/**
* 定義動(dòng)態(tài)數(shù)據(jù)源派生類。從基礎(chǔ)的DataSource派生,動(dòng)態(tài)性自己實(shí)現(xiàn)。
*
* @author elon
* @version 2018-02-25
*/
public class DynamicDataSource extends DataSource {
private static Logger log = LogManager.getLogger(DynamicDataSource.class);
/**
* 改寫本方法是為了在請(qǐng)求不同工程的數(shù)據(jù)時(shí)去連接不同的數(shù)據(jù)庫(kù)。
*/
@Override
public Connection getConnection(){
String projectCode = DBIdentifier.getProjectCode();
//1、獲取數(shù)據(jù)源
DataSource dds = DDSHolder.instance().getDDS(projectCode);
//2、如果數(shù)據(jù)源不存在則創(chuàng)建
if (dds == null) {
try {
DataSource newDDS = initDDS(projectCode);
DDSHolder.instance().addDDS(projectCode, newDDS);
} catch (IllegalArgumentException | IllegalAccessException e) {
log.error("Init data source fail. projectCode:" + projectCode);
return null;
}
}
dds = DDSHolder.instance().getDDS(projectCode);
try {
return dds.getConnection();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
/**
* 以當(dāng)前數(shù)據(jù)對(duì)象作為模板復(fù)制一份。
*
* @return dds
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
private DataSource initDDS(String projectCode) throws IllegalArgumentException, IllegalAccessException {
DataSource dds = new DataSource();
// 2、復(fù)制PoolConfiguration的屬性
PoolProperties property = new PoolProperties();
Field[] pfields = PoolProperties.class.getDeclaredFields();
for (Field f : pfields) {
f.setAccessible(true);
Object value = f.get(this.getPoolProperties());
try
{
f.set(property, value);
}
catch (Exception e)
{
log.info("Set value fail. attr name:" + f.getName());
continue;
}
}
dds.setPoolProperties(property);
// 3、設(shè)置數(shù)據(jù)庫(kù)名稱和IP(一般來(lái)說(shuō),端口和用戶名、密碼都是統(tǒng)一固定的)
String urlFormat = this.getUrl();
String url = String.format(urlFormat, ProjectDBMgr.instance().getDBIP(projectCode),
ProjectDBMgr.instance().getDBName(projectCode));
dds.setUrl(url);
return dds;
}
}
3) 通過(guò)DDSTimer控制數(shù)據(jù)連接釋放(超過(guò)指定時(shí)間未使用的數(shù)據(jù)源釋放)
package com.elon.dds.datasource;
import org.apache.tomcat.jdbc.pool.DataSource;
/**
* 動(dòng)態(tài)數(shù)據(jù)源定時(shí)器管理。長(zhǎng)時(shí)間無(wú)訪問的數(shù)據(jù)庫(kù)連接關(guān)閉。
*
* @author elon
* @version 2018年2月25日
*/
public class DDSTimer {
/**
* 空閑時(shí)間周期。超過(guò)這個(gè)時(shí)長(zhǎng)沒有訪問的數(shù)據(jù)庫(kù)連接將被釋放。默認(rèn)為10分鐘。
*/
private static long idlePeriodTime = 10 * 60 * 1000;
/**
* 動(dòng)態(tài)數(shù)據(jù)源
*/
private DataSource dds;
/**
* 上一次訪問的時(shí)間
*/
private long lastUseTime;
public DDSTimer(DataSource dds) {
this.dds = dds;
this.lastUseTime = System.currentTimeMillis();
}
/**
* 更新最近訪問時(shí)間
*/
public void refreshTime() {
lastUseTime = System.currentTimeMillis();
}
/**
* 檢測(cè)數(shù)據(jù)連接是否超時(shí)關(guān)閉。
*
* @return true-已超時(shí)關(guān)閉; false-未超時(shí)
*/
public boolean checkAndClose() {
if (System.currentTimeMillis() - lastUseTime > idlePeriodTime)
{
dds.close();
return true;
}
return false;
}
public DataSource getDds() {
return dds;
}
}
4) 增加DDSHolder來(lái)管理不同的數(shù)據(jù)源,提供數(shù)據(jù)源的添加、查詢功能
package com.elon.dds.datasource;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Timer;
import org.apache.tomcat.jdbc.pool.DataSource;
/**
* 動(dòng)態(tài)數(shù)據(jù)源管理器。
*
* @author elon
* @version 2018年2月25日
*/
public class DDSHolder {
/**
* 管理動(dòng)態(tài)數(shù)據(jù)源列表。<工程編碼,數(shù)據(jù)源>
*/
private Map<String, DDSTimer> ddsMap = new HashMap<String, DDSTimer>();
/**
* 通過(guò)定時(shí)任務(wù)周期性清除不使用的數(shù)據(jù)源
*/
private static Timer clearIdleTask = new Timer();
static {
clearIdleTask.schedule(new ClearIdleTimerTask(), 5000, 60 * 1000);
};
private DDSHolder() {
}
/*
* 獲取單例對(duì)象
*/
public static DDSHolder instance() {
return DDSHolderBuilder.instance;
}
/**
* 添加動(dòng)態(tài)數(shù)據(jù)源。
*
* @param projectCode 項(xiàng)目編碼
* @param dds dds
*/
public synchronized void addDDS(String projectCode, DataSource dds) {
DDSTimer ddst = new DDSTimer(dds);
ddsMap.put(projectCode, ddst);
}
/**
* 查詢動(dòng)態(tài)數(shù)據(jù)源
*
* @param projectCode 項(xiàng)目編碼
* @return dds
*/
public synchronized DataSource getDDS(String projectCode) {
if (ddsMap.containsKey(projectCode)) {
DDSTimer ddst = ddsMap.get(projectCode);
ddst.refreshTime();
return ddst.getDds();
}
return null;
}
/**
* 清除超時(shí)無(wú)人使用的數(shù)據(jù)源。
*/
public synchronized void clearIdleDDS() {
Iterator<Entry<String, DDSTimer>> iter = ddsMap.entrySet().iterator();
for (; iter.hasNext(); ) {
Entry<String, DDSTimer> entry = iter.next();
if (entry.getValue().checkAndClose())
{
iter.remove();
}
}
}
/**
* 單例構(gòu)件類
* @author elon
* @version 2018年2月26日
*/
private static class DDSHolderBuilder {
private static DDSHolder instance = new DDSHolder();
}
}
5) 定時(shí)器任務(wù)ClearIdleTimerTask用于定時(shí)清除空閑的數(shù)據(jù)源
package com.elon.dds.datasource;
import java.util.TimerTask;
/**
* 清除空閑連接任務(wù)。
*
* @author elon
* @version 2018年2月26日
*/
public class ClearIdleTimerTask extends TimerTask {
@Override
public void run() {
DDSHolder.instance().clearIdleDDS();
}
}
3. 管理項(xiàng)目編碼與數(shù)據(jù)庫(kù)IP和名稱的映射關(guān)系
package com.elon.dds.dbmgr;
import java.util.HashMap;
import java.util.Map;
/**
* 項(xiàng)目數(shù)據(jù)庫(kù)管理。提供根據(jù)項(xiàng)目編碼查詢數(shù)據(jù)庫(kù)名稱和IP的接口。
* @author elon
* @version 2018年2月25日
*/
public class ProjectDBMgr {
/**
* 保存項(xiàng)目編碼與數(shù)據(jù)名稱的映射關(guān)系。這里是硬編碼,實(shí)際開發(fā)中這個(gè)關(guān)系數(shù)據(jù)可以保存到redis緩存中;
* 新增一個(gè)項(xiàng)目或者刪除一個(gè)項(xiàng)目只需要更新緩存。到時(shí)這個(gè)類的接口只需要修改為從緩存拿數(shù)據(jù)。
*/
private Map<String, String> dbNameMap = new HashMap<String, String>();
/**
* 保存項(xiàng)目編碼與數(shù)據(jù)庫(kù)IP的映射關(guān)系。
*/
private Map<String, String> dbIPMap = new HashMap<String, String>();
private ProjectDBMgr() {
dbNameMap.put("project_001", "db_project_001");
dbNameMap.put("project_002", "db_project_002");
dbNameMap.put("project_003", "db_project_003");
dbIPMap.put("project_001", "127.0.0.1");
dbIPMap.put("project_002", "127.0.0.1");
dbIPMap.put("project_003", "127.0.0.1");
}
public static ProjectDBMgr instance() {
return ProjectDBMgrBuilder.instance;
}
// 實(shí)際開發(fā)中改為從緩存獲取
public String getDBName(String projectCode) {
if (dbNameMap.containsKey(projectCode)) {
return dbNameMap.get(projectCode);
}
return "";
}
//實(shí)際開發(fā)中改為從緩存中獲取
public String getDBIP(String projectCode) {
if (dbIPMap.containsKey(projectCode)) {
return dbIPMap.get(projectCode);
}
return "";
}
private static class ProjectDBMgrBuilder {
private static ProjectDBMgr instance = new ProjectDBMgr();
}
}
4. 定義數(shù)據(jù)庫(kù)訪問的mapper
package com.elon.dds.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import com.elon.dds.model.User;
/**
* Mybatis映射接口定義。
*
* @author elon
* @version 2018年2月26日
*/
@Mapper
public interface UserMapper
{
/**
* 查詢所有用戶數(shù)據(jù)
* @return 用戶數(shù)據(jù)列表
*/
@Results(value= {
@Result(property="userId", column="id"),
@Result(property="name", column="name"),
@Result(property="age", column="age")
})
@Select("select id, name, age from tbl_user")
List<User> getUsers();
}
5. 定義查詢對(duì)象模型
package com.elon.dds.model;
public class User
{
private int userId = -1;
private String name = "";
private int age = -1;
@Override
public String toString()
{
return "name:" + name + "|age:" + age;
}
public int getUserId()
{
return userId;
}
public void setUserId(int userId)
{
this.userId = userId;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public int getAge()
{
return age;
}
public void setAge(int age)
{
this.age = age;
}
}
6. 定義查詢用戶數(shù)據(jù)的restful接口
package com.elon.dds.rest;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.elon.dds.datasource.DBIdentifier;
import com.elon.dds.mapper.UserMapper;
import com.elon.dds.model.User;
/**
* 用戶數(shù)據(jù)訪問接口。
*
* @author elon
* @version 2018年2月26日
*/
@RestController
@RequestMapping(value="/user")
public class WSUser {
@Autowired
private UserMapper userMapper;
/**
* 查詢項(xiàng)目中所有用戶信息
*
* @param projectCode 項(xiàng)目編碼
* @return 用戶列表
*/
@RequestMapping(value="/v1/users", method=RequestMethod.GET)
public List<User> queryUser(@RequestParam(value="projectCode", required=true) String projectCode)
{
DBIdentifier.setProjectCode(projectCode);
return userMapper.getUsers();
}
}
要求每次查詢都要帶上projectCode參數(shù)。
7. 編寫Spring Boot App的啟動(dòng)代碼
package com.elon.dds;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Hello world!
*
*/
@SpringBootApplication
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
SpringApplication.run(App.class, args);
}
}
8. 在application.yml中配置數(shù)據(jù)源
其中的數(shù)據(jù)庫(kù)IP和數(shù)據(jù)庫(kù)名稱使用%s。在查詢用戶數(shù)據(jù)中動(dòng)態(tài)切換。
spring: datasource: url: jdbc:mysql://%s:3306/%s?useUnicode=true&characterEncoding=utf-8 username: root password: driver-class-name: com.mysql.jdbc.Driver logging: config: classpath:log4j2.xml
測(cè)試方案
1. 查詢project_001的數(shù)據(jù),正常返回

2. 查詢project_002的數(shù)據(jù),正常返回

總結(jié)
以上所述是小編給大家介紹的通過(guò)Spring Boot配置動(dòng)態(tài)數(shù)據(jù)源訪問多個(gè)數(shù)據(jù)庫(kù)的實(shí)現(xiàn)代碼,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
IKAnalyzer使用不同版本中文分詞的切詞方式實(shí)現(xiàn)相同功能效果
今天小編就為大家分享一篇關(guān)于IKAnalyzer使用不同版本中文分詞的切詞方式實(shí)現(xiàn)相同功能效果,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12
Java實(shí)戰(zhàn)之在線租房系統(tǒng)的實(shí)現(xiàn)
這篇文章主要介紹了利用Java實(shí)現(xiàn)的在線租房系統(tǒng),文中用到了SpringBoot、Redis、MySQL、Vue等技術(shù),文中示例代碼講解詳細(xì),需要的可以參考一下2022-02-02
SpringBoot啟動(dòng)時(shí)加載指定方法的方式小結(jié)
本文主要給大家介紹了Spring Boot項(xiàng)目啟動(dòng)時(shí)加載指定方法都有哪些方式的,文中給大家介紹了五種常用的方式,有詳細(xì)的代碼示例,具有一定的參考價(jià)值,需要的朋友可以參考下2023-08-08
解析ConcurrentHashMap: 預(yù)熱(內(nèi)部一些小方法分析)
ConcurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成。Segment的結(jié)構(gòu)和HashMap類似,是一種數(shù)組和鏈表結(jié)構(gòu),今天給大家普及java面試常見問題---ConcurrentHashMap知識(shí),一起看看吧2021-06-06
ConcurrentMap.putIfAbsent(key,value)用法實(shí)例
這篇文章主要介紹了ConcurrentMap.putIfAbsent(key,value)用法實(shí)例,分享了相關(guān)代碼示例,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02
Curator實(shí)現(xiàn)zookeeper的節(jié)點(diǎn)監(jiān)聽詳解
這篇文章主要介紹了Curator實(shí)現(xiàn)zookeeper的節(jié)點(diǎn)監(jiān)聽詳解,Curtor框架中一共有三個(gè)實(shí)現(xiàn)監(jiān)聽的方式,一種是NodeCache監(jiān)聽指定節(jié)點(diǎn),一種是pathChildrenCache監(jiān)聽子節(jié)點(diǎn),一種是TreeCache可以監(jiān)控所有節(jié)點(diǎn) 相當(dāng)于以上兩種的合集,需要的朋友可以參考下2023-12-12
使用springboot logback動(dòng)態(tài)獲取application的配置項(xiàng)
這篇文章主要介紹了使用springboot logback動(dòng)態(tài)獲取application的配置項(xiàng),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
jasypt對(duì)配置文件的數(shù)據(jù)加密與解密方式
這篇文章主要介紹了jasypt對(duì)配置文件的數(shù)據(jù)加密與解密方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01

