之前寫過一篇博客《Spring+Mybatis+Mysql搭建分佈式數據庫訪問框架》描述如何通過Spring+Mybatis配置動態數據源訪問多個數據庫。但是之前的方案有一些限制(原博客中也描述了):只適用於數據庫數量不多且固定的情況。針對數據庫動態增加的情況無能為力。
下面講的方案能支持數據庫動態增刪,數量不限。
數據庫環境準備
下面一Mysql為例,先在本地建3個數據庫用於測試。需要說明的是本方案不限數據庫數量,支持不同的數據庫部署在不同的服務器上。如圖所示db_project_001、db_project_002、db_project_003。
搭建Java後台微服務項目
創建一個Spring Boot的maven項目:
config:數據源配置管理類。
datasource:自己實現的數據源管理邏輯。
dbmgr:管理了項目編碼與數據庫IP、名稱的映射關係(實際項目中這部分數據保存在redis緩存中,可動態增刪)。
mapper:數據庫訪問接口。
model:映射模型。
rest:微服務對外發布的restful接口,這裡用來測試。
application.yml:配置了數據庫的JDBC參數。
詳細的代碼實現
1. 添加數據源配置
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;/** * 數據源配置管理。 * * @author elon * @version 2018年2月26日*/@Configuration@MapperScan(basePackages="com.elon.dds.mapper", value="sqlSessionFactory")public class DataSourceConfig { /** * 根據配置參數創建數據源。使用派生的子類。 * * @return 數據源*/ @Bean(name="dataSource") @ConfigurationProperties(prefix="spring.datasource") public DataSource getDataSource() { DataSourceBuilder builder = DataSourceBuilder.create(); builder.type(DynamicDataSource.class); return builder.build(); } /** * 創建會話工廠。 * * @param dataSource 數據源* @return 會話工廠*/ @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.定義動態數據源
1) 首先增加一個數據庫標識類,用於區分不同的數據庫訪問。
由於我們為不同的project創建了單獨的數據庫,所以使用項目編碼作為數據庫的索引。而微服務支持多線程並發的,採用線程變量。
package com.elon.dds.datasource;/** * 數據庫標識管理類。用於區分數據源連接的不同數據庫。 * * @author elon * @version 2018-02-25 */public class DBIdentifier { /** * 用不同的工程編碼來區分數據庫*/ 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派生了一個DynamicDataSource,在其中實現數據庫連接的動態切換
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;/** * 定義動態數據源派生類。從基礎的DataSource派生,動態性自己實現。 * * @author elon * @version 2018-02-25 */public class DynamicDataSource extends DataSource { private static Logger log = LogManager.getLogger(DynamicDataSource.class); /** * 改寫本方法是為了在請求不同工程的數據時去連接不同的數據庫。 */ @Override public Connection getConnection(){ String projectCode = DBIdentifier.getProjectCode(); //1、獲取數據源DataSource dds = DDSHolder.instance().getDDS(projectCode); //2、如果數據源不存在則創建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; } } /** * 以當前數據對像作為模板複製一份。 * * @return dds * @throws IllegalAccessException * @throws IllegalArgumentException */ private DataSource initDDS(String projectCode) throws IllegalArgumentException, IllegalAccessException { DataSource dds = new DataSource(); // 2、複製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、設置數據庫名稱和IP(一般來說,端口和用戶名、密碼都是統一固定的) String urlFormat = this.getUrl(); String url = String.format(urlFormat, ProjectDBMgr.instance().getDBIP(projectCode), ProjectDBMgr.instance().getDBName(projectCode)); dds.setUrl(url); return dds; }}3) 通過DDSTimer控制數據連接釋放(超過指定時間未使用的數據源釋放)
package com.elon.dds.datasource;import org.apache.tomcat.jdbc.pool.DataSource;/** * 動態數據源定時器管理。長時間無訪問的數據庫連接關閉。 * * @author elon * @version 2018年2月25日*/public class DDSTimer { /** * 空閒時間週期。超過這個時長沒有訪問的數據庫連接將被釋放。默認為10分鐘。 */ private static long idlePeriodTime = 10 * 60 * 1000; /** * 動態數據源*/ private DataSource dds; /** * 上一次訪問的時間*/ private long lastUseTime; public DDSTimer(DataSource dds) { this.dds = dds; this.lastUseTime = System.currentTimeMillis(); } /** * 更新最近訪問時間*/ public void refreshTime() { lastUseTime = System.currentTimeMillis(); } /** * 檢測數據連接是否超時關閉。 * * @return true-已超時關閉; false-未超時*/ public boolean checkAndClose() { if (System.currentTimeMillis() - lastUseTime > idlePeriodTime) { dds.close(); return true; } return false; } public DataSource getDds() { return dds; }}4) 增加DDSHolder來管理不同的數據源,提供數據源的添加、查詢功能
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;/** * 動態數據源管理器。 * * @author elon * @version 2018年2月25日*/public class DDSHolder { /** * 管理動態數據源列表。 <工程編碼,數據源> */ private Map<String, DDSTimer> ddsMap = new HashMap<String, DDSTimer>(); /** * 通過定時任務週期性清除不使用的數據源*/ private static Timer clearIdleTask = new Timer(); static { clearIdleTask.schedule(new ClearIdleTimerTask(), 5000, 60 * 1000); }; private DDSHolder() { } /* * 獲取單例對象*/ public static DDSHolder instance() { return DDSHolderBuilder.instance; } /** * 添加動態數據源。 * * @param projectCode 項目編碼* @param dds dds */ public synchronized void addDDS(String projectCode, DataSource dds) { DDSTimer ddst = new DDSTimer(dds); ddsMap.put(projectCode, ddst); } /** * 查詢動態數據源* * @param projectCode 項目編碼* @return dds */ public synchronized DataSource getDDS(String projectCode) { if (ddsMap.containsKey(projectCode)) { DDSTimer ddst = ddsMap.get(projectCode); ddst.refreshTime(); return ddst.getDds(); } return null; } /** * 清除超時無人使用的數據源。 */ 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(); } } } /** * 單例構件類* @author elon * @version 2018年2月26日*/ private static class DDSHolderBuilder { private static DDSHolder instance = new DDSHolder(); }}5) 定時器任務ClearIdleTimerTask用於定時清除空閒的數據源
package com.elon.dds.datasource;import java.util.TimerTask;/** * 清除空閒連接任務。 * * @author elon * @version 2018年2月26日*/public class ClearIdleTimerTask extends TimerTask { @Override public void run() { DDSHolder.instance().clearIdleDDS(); }}3. 管理項目編碼與數據庫IP和名稱的映射關係
package com.elon.dds.dbmgr;import java.util.HashMap;import java.util.Map;/** * 項目數據庫管理。提供根據項目編碼查詢數據庫名稱和IP的接口。 * @author elon * @version 2018年2月25日*/public class ProjectDBMgr { /** * 保存項目編碼與數據名稱的映射關係。這裡是硬編碼,實際開發中這個關係數據可以保存到redis緩存中; * 新增一個項目或者刪除一個項目只需要更新緩存。到時這個類的接口只需要修改為從緩存拿數據。 */ private Map<String, String> dbNameMap = new HashMap<String, String>(); /** * 保存項目編碼與數據庫IP的映射關係。 */ 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; } // 實際開發中改為從緩存獲取public String getDBName(String projectCode) { if (dbNameMap.containsKey(projectCode)) { return dbNameMap.get(projectCode); } return ""; } //實際開發中改為從緩存中獲取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 . 定義數據庫訪問的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日*/@Mapperpublic interface UserMapper{ /** * 查詢所有用戶數據* @return 用戶數據列表*/ @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. 定義查詢對像模型
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. 定義查詢用戶數據的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;/** * 用戶數據訪問接口。 * * @author elon * @version 2018年2月26日*/@RestController@RequestMapping(value="/user")public class WSUser { @Autowired private UserMapper userMapper; /** * 查詢項目中所有用戶信息* * @param projectCode 項目編碼* @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參數。
7. 編寫Spring Boot App的啟動代碼
package com.elon.dds;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;/** * Hello world! * */@SpringBootApplicationpublic class App{ public static void main( String[] args ) { System.out.println( "Hello World!" ); SpringApplication.run(App.class, args); }} 8. 在application.yml中配置數據源
其中的數據庫IP和數據庫名稱使用%s。在查詢用戶數據中動態切換。
spring: datasource: url: jdbc:mysql://%s:3306/%s?useUnicode=true&characterEncoding=utf-8 username: root password: driver-class-name: com.mysql.jdbc.Driverlogging: config: classpath:log4j2.xml
測試方案
1. 查詢project_001的數據,正常返回
2. 查詢project_002的數據,正常返回
總結
以上所述是小編給大家介紹的通過Spring Boot配置動態數據源訪問多個數據庫的實現代碼,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對武林網網站的支持!