欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

通过Spring Boot配置动态数据源访问多个数据库的实现代码

程序员文章站 2022-03-10 10:52:55
之前写过一篇博客《spring+mybatis+mysql搭建分布式数据库访问框架》描述如何通过spring+mybatis配置动态数据源访问多个数据库。但是之前的方案有一...

之前写过一篇博客《spring+mybatis+mysql搭建分布式数据库访问框架》描述如何通过spring+mybatis配置动态数据源访问多个数据库。但是之前的方案有一些限制(原博客中也描述了):只适用于数据库数量不多且固定的情况。针对数据库动态增加的情况无能为力。

下面讲的方案能支持数据库动态增删,数量不限。

数据库环境准备

下面一mysql为例,先在本地建3个数据库用于测试。需要说明的是本方案不限数据库数量,支持不同的数据库部署在不同的服务器上。如图所示db_project_001、db_project_002、db_project_003。

通过Spring Boot配置动态数据源访问多个数据库的实现代码

搭建java后台微服务项目

创建一个spring boot的maven项目:

通过Spring Boot配置动态数据源访问多个数据库的实现代码

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的数据,正常返回 

通过Spring Boot配置动态数据源访问多个数据库的实现代码

2.       查询project_002的数据,正常返回

通过Spring Boot配置动态数据源访问多个数据库的实现代码

总结

以上所述是小编给大家介绍的通过spring boot配置动态数据源访问多个数据库的实现代码,希望对大家有所帮助