查看: 2348|回复: 0

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

发表于 2018-4-16 08:00:05

之前写过一篇博客《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. 添加数据源配置

  1. package com.elon.dds.config;
  2. import javax.sql.DataSource;
  3. import org.apache.ibatis.session.SqlSessionFactory;
  4. import org.mybatis.spring.SqlSessionFactoryBean;
  5. import org.mybatis.spring.annotation.MapperScan;
  6. import org.springframework.beans.factory.annotation.Qualifier;
  7. import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
  8. import org.springframework.boot.context.properties.ConfigurationProperties;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import com.elon.dds.datasource.DynamicDataSource;
  12. /**
  13. * 数据源配置管理。
  14. *
  15. * @author elon
  16. * @version 2018年2月26日
  17. */
  18. @Configuration
  19. @MapperScan(basePackages="com.elon.dds.mapper", value="sqlSessionFactory")
  20. public class DataSourceConfig {
  21. /**
  22. * 根据配置参数创建数据源。使用派生的子类。
  23. *
  24. * @return 数据源
  25. */
  26. @Bean(name="dataSource")
  27. @ConfigurationProperties(prefix="spring.datasource")
  28. public DataSource getDataSource() {
  29. DataSourceBuilder builder = DataSourceBuilder.create();
  30. builder.type(DynamicDataSource.class);
  31. return builder.build();
  32. }
  33. /**
  34. * 创建会话工厂。
  35. *
  36. * @param dataSource 数据源
  37. * @return 会话工厂
  38. */
  39. @Bean(name="sqlSessionFactory")
  40. public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) {
  41. SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
  42. bean.setDataSource(dataSource);
  43. try {
  44. return bean.getObject();
  45. } catch (Exception e) {
  46. e.printStackTrace();
  47. return null;
  48. }
  49. }
  50. }
复制代码

2.定义动态数据源

1) 首先增加一个数据库标识类,用于区分不同的数据库访问。

由于我们为不同的project创建了单独的数据库,所以使用项目编码作为数据库的索引。而微服务支持多线程并发的,采用线程变量。

  1. package com.elon.dds.datasource;
  2. /**
  3. * 数据库标识管理类。用于区分数据源连接的不同数据库。
  4. *
  5. * @author elon
  6. * @version 2018-02-25
  7. */
  8. public class DBIdentifier {
  9. /**
  10. * 用不同的工程编码来区分数据库
  11. */
  12. private static ThreadLocal<String> projectCode = new ThreadLocal<String>();
  13. public static String getProjectCode() {
  14. return projectCode.get();
  15. }
  16. public static void setProjectCode(String code) {
  17. projectCode.set(code);
  18. }
  19. }
复制代码

2) 从DataSource派生了一个DynamicDataSource,在其中实现数据库连接的动态切换

  1. import java.lang.reflect.Field;
  2. import java.sql.Connection;
  3. import java.sql.SQLException;
  4. import org.apache.logging.log4j.LogManager;
  5. import org.apache.logging.log4j.Logger;
  6. import org.apache.tomcat.jdbc.pool.DataSource;
  7. import org.apache.tomcat.jdbc.pool.PoolProperties;
  8. import com.elon.dds.dbmgr.ProjectDBMgr;
  9. /**
  10. * 定义动态数据源派生类。从基础的DataSource派生,动态性自己实现。
  11. *
  12. * @author elon
  13. * @version 2018-02-25
  14. */
  15. public class DynamicDataSource extends DataSource {
  16. private static Logger log = LogManager.getLogger(DynamicDataSource.class);
  17. /**
  18. * 改写本方法是为了在请求不同工程的数据时去连接不同的数据库。
  19. */
  20. @Override
  21. public Connection getConnection(){
  22. String projectCode = DBIdentifier.getProjectCode();
  23. //1、获取数据源
  24. DataSource dds = DDSHolder.instance().getDDS(projectCode);
  25. //2、如果数据源不存在则创建
  26. if (dds == null) {
  27. try {
  28. DataSource newDDS = initDDS(projectCode);
  29. DDSHolder.instance().addDDS(projectCode, newDDS);
  30. } catch (IllegalArgumentException | IllegalAccessException e) {
  31. log.error("Init data source fail. projectCode:" + projectCode);
  32. return null;
  33. }
  34. }
  35. dds = DDSHolder.instance().getDDS(projectCode);
  36. try {
  37. return dds.getConnection();
  38. } catch (SQLException e) {
  39. e.printStackTrace();
  40. return null;
  41. }
  42. }
  43. /**
  44. * 以当前数据对象作为模板复制一份。
  45. *
  46. * @return dds
  47. * @throws IllegalAccessException
  48. * @throws IllegalArgumentException
  49. */
  50. private DataSource initDDS(String projectCode) throws IllegalArgumentException, IllegalAccessException {
  51. DataSource dds = new DataSource();
  52. // 2、复制PoolConfiguration的属性
  53. PoolProperties property = new PoolProperties();
  54. Field[] pfields = PoolProperties.class.getDeclaredFields();
  55. for (Field f : pfields) {
  56. f.setAccessible(true);
  57. Object value = f.get(this.getPoolProperties());
  58. try
  59. {
  60. f.set(property, value);
  61. }
  62. catch (Exception e)
  63. {
  64. log.info("Set value fail. attr name:" + f.getName());
  65. continue;
  66. }
  67. }
  68. dds.setPoolProperties(property);
  69. // 3、设置数据库名称和IP(一般来说,端口和用户名、密码都是统一固定的)
  70. String urlFormat = this.getUrl();
  71. String url = String.format(urlFormat, ProjectDBMgr.instance().getDBIP(projectCode),
  72. ProjectDBMgr.instance().getDBName(projectCode));
  73. dds.setUrl(url);
  74. return dds;
  75. }
  76. }
复制代码

3) 通过DDSTimer控制数据连接释放(超过指定时间未使用的数据源释放)

  1. package com.elon.dds.datasource;
  2. import org.apache.tomcat.jdbc.pool.DataSource;
  3. /**
  4. * 动态数据源定时器管理。长时间无访问的数据库连接关闭。
  5. *
  6. * @author elon
  7. * @version 2018年2月25日
  8. */
  9. public class DDSTimer {
  10. /**
  11. * 空闲时间周期。超过这个时长没有访问的数据库连接将被释放。默认为10分钟。
  12. */
  13. private static long idlePeriodTime = 10 * 60 * 1000;
  14. /**
  15. * 动态数据源
  16. */
  17. private DataSource dds;
  18. /**
  19. * 上一次访问的时间
  20. */
  21. private long lastUseTime;
  22. public DDSTimer(DataSource dds) {
  23. this.dds = dds;
  24. this.lastUseTime = System.currentTimeMillis();
  25. }
  26. /**
  27. * 更新最近访问时间
  28. */
  29. public void refreshTime() {
  30. lastUseTime = System.currentTimeMillis();
  31. }
  32. /**
  33. * 检测数据连接是否超时关闭。
  34. *
  35. * @return true-已超时关闭; false-未超时
  36. */
  37. public boolean checkAndClose() {
  38. if (System.currentTimeMillis() - lastUseTime > idlePeriodTime)
  39. {
  40. dds.close();
  41. return true;
  42. }
  43. return false;
  44. }
  45. public DataSource getDds() {
  46. return dds;
  47. }
  48. }
复制代码

4) 增加DDSHolder来管理不同的数据源,提供数据源的添加、查询功能

  1. package com.elon.dds.datasource;
  2. import java.util.HashMap;
  3. import java.util.Iterator;
  4. import java.util.Map;
  5. import java.util.Map.Entry;
  6. import java.util.Timer;
  7. import org.apache.tomcat.jdbc.pool.DataSource;
  8. /**
  9. * 动态数据源管理器。
  10. *
  11. * @author elon
  12. * @version 2018年2月25日
  13. */
  14. public class DDSHolder {
  15. /**
  16. * 管理动态数据源列表。<工程编码,数据源>
  17. */
  18. private Map<String, DDSTimer> ddsMap = new HashMap<String, DDSTimer>();
  19. /**
  20. * 通过定时任务周期性清除不使用的数据源
  21. */
  22. private static Timer clearIdleTask = new Timer();
  23. static {
  24. clearIdleTask.schedule(new ClearIdleTimerTask(), 5000, 60 * 1000);
  25. };
  26. private DDSHolder() {
  27. }
  28. /*
  29. * 获取单例对象
  30. */
  31. public static DDSHolder instance() {
  32. return DDSHolderBuilder.instance;
  33. }
  34. /**
  35. * 添加动态数据源。
  36. *
  37. * @param projectCode 项目编码
  38. * @param dds dds
  39. */
  40. public synchronized void addDDS(String projectCode, DataSource dds) {
  41. DDSTimer ddst = new DDSTimer(dds);
  42. ddsMap.put(projectCode, ddst);
  43. }
  44. /**
  45. * 查询动态数据源
  46. *
  47. * @param projectCode 项目编码
  48. * @return dds
  49. */
  50. public synchronized DataSource getDDS(String projectCode) {
  51. if (ddsMap.containsKey(projectCode)) {
  52. DDSTimer ddst = ddsMap.get(projectCode);
  53. ddst.refreshTime();
  54. return ddst.getDds();
  55. }
  56. return null;
  57. }
  58. /**
  59. * 清除超时无人使用的数据源。
  60. */
  61. public synchronized void clearIdleDDS() {
  62. Iterator<Entry<String, DDSTimer>> iter = ddsMap.entrySet().iterator();
  63. for (; iter.hasNext(); ) {
  64. Entry<String, DDSTimer> entry = iter.next();
  65. if (entry.getValue().checkAndClose())
  66. {
  67. iter.remove();
  68. }
  69. }
  70. }
  71. /**
  72. * 单例构件类
  73. * @author elon
  74. * @version 2018年2月26日
  75. */
  76. private static class DDSHolderBuilder {
  77. private static DDSHolder instance = new DDSHolder();
  78. }
  79. }
复制代码

5) 定时器任务ClearIdleTimerTask用于定时清除空闲的数据源

  1. package com.elon.dds.datasource;
  2. import java.util.TimerTask;
  3. /**
  4. * 清除空闲连接任务。
  5. *
  6. * @author elon
  7. * @version 2018年2月26日
  8. */
  9. public class ClearIdleTimerTask extends TimerTask {
  10. @Override
  11. public void run() {
  12. DDSHolder.instance().clearIdleDDS();
  13. }
  14. }
复制代码

3. 管理项目编码与数据库IP和名称的映射关系

  1. package com.elon.dds.dbmgr;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. /**
  5. * 项目数据库管理。提供根据项目编码查询数据库名称和IP的接口。
  6. * @author elon
  7. * @version 2018年2月25日
  8. */
  9. public class ProjectDBMgr {
  10. /**
  11. * 保存项目编码与数据名称的映射关系。这里是硬编码,实际开发中这个关系数据可以保存到redis缓存中;
  12. * 新增一个项目或者删除一个项目只需要更新缓存。到时这个类的接口只需要修改为从缓存拿数据。
  13. */
  14. private Map<String, String> dbNameMap = new HashMap<String, String>();
  15. /**
  16. * 保存项目编码与数据库IP的映射关系。
  17. */
  18. private Map<String, String> dbIPMap = new HashMap<String, String>();
  19. private ProjectDBMgr() {
  20. dbNameMap.put("project_001", "db_project_001");
  21. dbNameMap.put("project_002", "db_project_002");
  22. dbNameMap.put("project_003", "db_project_003");
  23. dbIPMap.put("project_001", "127.0.0.1");
  24. dbIPMap.put("project_002", "127.0.0.1");
  25. dbIPMap.put("project_003", "127.0.0.1");
  26. }
  27. public static ProjectDBMgr instance() {
  28. return ProjectDBMgrBuilder.instance;
  29. }
  30. // 实际开发中改为从缓存获取
  31. public String getDBName(String projectCode) {
  32. if (dbNameMap.containsKey(projectCode)) {
  33. return dbNameMap.get(projectCode);
  34. }
  35. return "";
  36. }
  37. //实际开发中改为从缓存中获取
  38. public String getDBIP(String projectCode) {
  39. if (dbIPMap.containsKey(projectCode)) {
  40. return dbIPMap.get(projectCode);
  41. }
  42. return "";
  43. }
  44. private static class ProjectDBMgrBuilder {
  45. private static ProjectDBMgr instance = new ProjectDBMgr();
  46. }
  47. }
复制代码

4. 定义数据库访问的mapper

  1. package com.elon.dds.mapper;
  2. import java.util.List;
  3. import org.apache.ibatis.annotations.Mapper;
  4. import org.apache.ibatis.annotations.Result;
  5. import org.apache.ibatis.annotations.Results;
  6. import org.apache.ibatis.annotations.Select;
  7. import com.elon.dds.model.User;
  8. /**
  9. * Mybatis映射接口定义。
  10. *
  11. * @author elon
  12. * @version 2018年2月26日
  13. */
  14. @Mapper
  15. public interface UserMapper
  16. {
  17. /**
  18. * 查询所有用户数据
  19. * @return 用户数据列表
  20. */
  21. @Results(value= {
  22. @Result(property="userId", column="id"),
  23. @Result(property="name", column="name"),
  24. @Result(property="age", column="age")
  25. })
  26. @Select("select id, name, age from tbl_user")
  27. List<User> getUsers();
  28. }
复制代码

5. 定义查询对象模型

  1. package com.elon.dds.model;
  2. public class User
  3. {
  4. private int userId = -1;
  5. private String name = "";
  6. private int age = -1;
  7. @Override
  8. public String toString()
  9. {
  10. return "name:" + name + "|age:" + age;
  11. }
  12. public int getUserId()
  13. {
  14. return userId;
  15. }
  16. public void setUserId(int userId)
  17. {
  18. this.userId = userId;
  19. }
  20. public String getName()
  21. {
  22. return name;
  23. }
  24. public void setName(String name)
  25. {
  26. this.name = name;
  27. }
  28. public int getAge()
  29. {
  30. return age;
  31. }
  32. public void setAge(int age)
  33. {
  34. this.age = age;
  35. }
  36. }
复制代码

6. 定义查询用户数据的restful接口

  1. package com.elon.dds.rest;
  2. import java.util.List;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RequestMethod;
  6. import org.springframework.web.bind.annotation.RequestParam;
  7. import org.springframework.web.bind.annotation.RestController;
  8. import com.elon.dds.datasource.DBIdentifier;
  9. import com.elon.dds.mapper.UserMapper;
  10. import com.elon.dds.model.User;
  11. /**
  12. * 用户数据访问接口。
  13. *
  14. * @author elon
  15. * @version 2018年2月26日
  16. */
  17. @RestController
  18. @RequestMapping(value="/user")
  19. public class WSUser {
  20. @Autowired
  21. private UserMapper userMapper;
  22. /**
  23. * 查询项目中所有用户信息
  24. *
  25. * @param projectCode 项目编码
  26. * @return 用户列表
  27. */
  28. @RequestMapping(value="/v1/users", method=RequestMethod.GET)
  29. public List<User> queryUser(@RequestParam(value="projectCode", required=true) String projectCode)
  30. {
  31. DBIdentifier.setProjectCode(projectCode);
  32. return userMapper.getUsers();
  33. }
  34. }
复制代码

要求每次查询都要带上projectCode参数。

7. 编写Spring Boot App的启动代码

  1. package com.elon.dds;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. /**
  5. * Hello world!
  6. *
  7. */
  8. @SpringBootApplication
  9. public class App
  10. {
  11. public static void main( String[] args )
  12. {
  13. System.out.println( "Hello World!" );
  14. SpringApplication.run(App.class, args);
  15. }
  16. }
复制代码

8. 在application.yml中配置数据源

其中的数据库IP和数据库名称使用%s。在查询用户数据中动态切换。

  1. spring:
  2. datasource:
  3. url: jdbc:mysql://%s:3306/%s?useUnicode=true&characterEncoding=utf-8
  4. username: root
  5. password:
  6. driver-class-name: com.mysql.jdbc.Driver
  7. logging:
  8. config: classpath:log4j2.xml
复制代码

测试方案

1. 查询project_001的数据,正常返回

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

总结

以上所述是小编给大家介绍的通过Spring Boot配置动态数据源访问多个数据库的实现代码,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对程序员之家网站的支持!



回复

使用道具 举报