通过Spring Boot配置动态数据源访问多个数据库的实现代码
之前写过一篇博客《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日 */ @mapper public 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! * */ @springbootapplication public 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.driver logging: config: classpath:log4j2.xml
测试方案
1. 查询project_001的数据,正常返回
2. 查询project_002的数据,正常返回
总结
以上所述是小编给大家介绍的通过spring boot配置动态数据源访问多个数据库的实现代码,希望对大家有所帮助
上一篇: Java内部类知识汇总
下一篇: java9中gc log参数迁移