Spring Boot配置动态数据源访问N个数据库,支持数据库动态增删,数量不限
方案能支持数据库动态增删,数量不限。
数据库环境准备
下面以Mysql为例,先在本地建3个数据库用于测试。需要说明的是本方案不限数据库数量,支持不同的数据库部署在不同的服务器上。如图所示db_project_001、db_project_002、db_project_003。
搭建Java后台微服务项目
创建一个Spring Boot的maven项目:
config:数据源配置。
datasource:自己实现的动态数据源相关类。
dbmgr:管理项目编码与数据库IP、名称的映射关系(实际项目中这部分数据保存在redis缓存中,可动态增删)。
mapper:mybatis的数据库访问接口。
model:映射模型。
rest:微服务对外发布的restful接口,这里用来测试。
application.yml:配置数据库JDBC参数。
详细的代码实现
1. 数据源配置管理类(DataSourceConfig.java)
1 package com.elon.dds.config; 2 3 import javax.sql.DataSource; 4 5 import org.apache.ibatis.session.SqlSessionFactory; 6 import org.mybatis.spring.SqlSessionFactoryBean; 7 import org.mybatis.spring.annotation.MapperScan; 8 import org.springframework.beans.factory.annotation.Qualifier; 9 import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; 10 import org.springframework.boot.context.properties.ConfigurationProperties; 11 import org.springframework.context.annotation.Bean; 12 import org.springframework.context.annotation.Configuration; 13 14 import com.elon.dds.datasource.DynamicDataSource; 15 16 /** 17 * 数据源配置管理。 18 * 19 * @author elon 20 * @version 2018年2月26日 21 */ 22 @Configuration 23 @MapperScan(basePackages="com.elon.dds.mapper", value="sqlSessionFactory") 24 public class DataSourceConfig { 25 26 /** 27 * 根据配置参数创建数据源。使用派生的子类。 28 * 29 * @return 数据源 30 */ 31 @Bean(name="dataSource") 32 @ConfigurationProperties(prefix="spring.datasource") 33 public DataSource getDataSource() { 34 DataSourceBuilder builder = DataSourceBuilder.create(); 35 builder.type(DynamicDataSource.class); 36 return builder.build(); 37 } 38 39 /** 40 * 创建会话工厂。 41 * 42 * @param dataSource 数据源 43 * @return 会话工厂 44 */ 45 @Bean(name="sqlSessionFactory") 46 public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) { 47 SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); 48 bean.setDataSource(dataSource); 49 50 try { 51 return bean.getObject(); 52 } catch (Exception e) { 53 e.printStackTrace(); 54 return null; 55 } 56 } 57 }
2. 定义动态数据源
1) 首先增加一个数据库标识类,用于区分不同的数据库(DBIdentifier.java)
由于我们为不同的project创建了单独的数据库,所以使用项目编码作为数据库的索引。而微服务支持多线程并发的,采用线程变量。
1 package com.elon.dds.datasource; 2 3 /** 4 * 数据库标识管理类。用于区分数据源连接的不同数据库。 5 * 6 * @author elon 7 * @version 2018-02-25 8 */ 9 public class DBIdentifier { 10 11 /** 12 * 用不同的工程编码来区分数据库 13 */ 14 private static ThreadLocal<String> projectCode = new ThreadLocal<String>(); 15 16 public static String getProjectCode() { 17 return projectCode.get(); 18 } 19 20 public static void setProjectCode(String code) { 21 projectCode.set(code); 22 } 23 }
2) 从DataSource派生了一个DynamicDataSource,在其中实现数据库连接的动态切换(DynamicDataSource.java)
1 package com.elon.dds.datasource; 2 3 import java.lang.reflect.Field; 4 import java.sql.Connection; 5 import java.sql.SQLException; 6 7 import org.apache.logging.log4j.LogManager; 8 import org.apache.logging.log4j.Logger; 9 import org.apache.tomcat.jdbc.pool.DataSource; 10 import org.apache.tomcat.jdbc.pool.PoolProperties; 11 12 import com.elon.dds.dbmgr.ProjectDBMgr; 13 14 /** 15 * 定义动态数据源派生类。从基础的DataSource派生,动态性自己实现。 16 * 17 * @author elon 18 * @version 2018-02-25 19 */ 20 public class DynamicDataSource extends DataSource { 21 22 private static Logger log = LogManager.getLogger(DynamicDataSource.class); 23 24 /** 25 * 改写本方法是为了在请求不同工程的数据时去连接不同的数据库。 26 */ 27 @Override 28 public Connection getConnection(){ 29 30 String projectCode = DBIdentifier.getProjectCode(); 31 32 //1、获取数据源 33 DataSource dds = DDSHolder.instance().getDDS(projectCode); 34 35 //2、如果数据源不存在则创建 36 if (dds == null) { 37 try { 38 DataSource newDDS = initDDS(projectCode); 39 DDSHolder.instance().addDDS(projectCode, newDDS); 40 } catch (IllegalArgumentException | IllegalAccessException e) { 41 log.error("Init data source fail. projectCode:" + projectCode); 42 return null; 43 } 44 } 45 46 dds = DDSHolder.instance().getDDS(projectCode); 47 try { 48 return dds.getConnection(); 49 } catch (SQLException e) { 50 e.printStackTrace(); 51 return null; 52 } 53 } 54 55 /** 56 * 以当前数据对象作为模板复制一份。 57 * 58 * @return dds 59 * @throws IllegalAccessException 60 * @throws IllegalArgumentException 61 */ 62 private DataSource initDDS(String projectCode) throws IllegalArgumentException, IllegalAccessException { 63 64 DataSource dds = new DataSource(); 65 66 // 2、复制PoolConfiguration的属性 67 PoolProperties property = new PoolProperties(); 68 Field[] pfields = PoolProperties.class.getDeclaredFields(); 69 for (Field f : pfields) { 70 f.setAccessible(true); 71 Object value = f.get(this.getPoolProperties()); 72 73 try 74 { 75 f.set(property, value); 76 } 77 catch (Exception e) 78 { 79 //有一些static final的属性不能修改。忽略。 80 log.info("Set value fail. attr name:" + f.getName()); 81 continue; 82 } 83 } 84 dds.setPoolProperties(property); 85 86 // 3、设置数据库名称和IP(一般来说,端口和用户名、密码都是统一固定的) 87 String urlFormat = this.getUrl(); 88 String url = String.format(urlFormat, ProjectDBMgr.instance().getDBIP(projectCode), 89 ProjectDBMgr.instance().getDBName(projectCode)); 90 dds.setUrl(url); 91 92 return dds; 93 } 94 }
3) 通过DDSTimer控制数据连接释放(DDSTimer.java)
1 package com.elon.dds.datasource; 2 3 import org.apache.tomcat.jdbc.pool.DataSource; 4 5 /** 6 * 动态数据源定时器管理。长时间无访问的数据库连接关闭。 7 * 8 * @author elon 9 * @version 2018年2月25日 10 */ 11 public class DDSTimer { 12 13 /** 14 * 空闲时间周期。超过这个时长没有访问的数据库连接将被释放。默认为10分钟。 15 */ 16 private static long idlePeriodTime = 10 * 60 * 1000; 17 18 /** 19 * 动态数据源 20 */ 21 private DataSource dds; 22 23 /** 24 * 上一次访问的时间 25 */ 26 private long lastUseTime; 27 28 public DDSTimer(DataSource dds) { 29 this.dds = dds; 30 this.lastUseTime = System.currentTimeMillis(); 31 } 32 33 /** 34 * 更新最近访问时间 35 */ 36 public void refreshTime() { 37 lastUseTime = System.currentTimeMillis(); 38 } 39 40 /** 41 * 检测数据连接是否超时关闭。 42 * 43 * @return true-已超时关闭; false-未超时 44 */ 45 public boolean checkAndClose() { 46 47 if (System.currentTimeMillis() - lastUseTime > idlePeriodTime) 48 { 49 dds.close(); 50 return true; 51 } 52 53 return false; 54 } 55 56 public DataSource getDds() { 57 return dds; 58 } 59 }
4) 通过DDSHolder来管理不同的数据源,提供数据源的添加、查询功能(DDSHolder.java)
1 package com.elon.dds.datasource; 2 3 import java.util.HashMap; 4 import java.util.Iterator; 5 import java.util.Map; 6 import java.util.Map.Entry; 7 import java.util.Timer; 8 9 import org.apache.tomcat.jdbc.pool.DataSource; 10 11 /** 12 * 动态数据源管理器。 13 * 14 * @author elon 15 * @version 2018年2月25日 16 */ 17 public class DDSHolder { 18 19 /** 20 * 管理动态数据源列表。<工程编码,数据源> 21 */ 22 private Map<String, DDSTimer> ddsMap = new HashMap<String, DDSTimer>(); 23 24 /** 25 * 通过定时任务周期性清除不使用的数据源 26 */ 27 private static Timer clearIdleTask = new Timer(); 28 static { 29 clearIdleTask.schedule(new ClearIdleTimerTask(), 5000, 60 * 1000); 30 }; 31 32 private DDSHolder() { 33 34 } 35 36 /* 37 * 获取单例对象 38 */ 39 public static DDSHolder instance() { 40 return DDSHolderBuilder.instance; 41 } 42 43 /** 44 * 添加动态数据源。 45 * 46 * @param projectCode 项目编码 47 * @param dds dds 48 */ 49 public synchronized void addDDS(String projectCode, DataSource dds) { 50 51 DDSTimer ddst = new DDSTimer(dds); 52 ddsMap.put(projectCode, ddst); 53 } 54 55 /** 56 * 查询动态数据源 57 * 58 * @param projectCode 项目编码 59 * @return dds 60 */ 61 public synchronized DataSource getDDS(String projectCode) { 62 63 if (ddsMap.containsKey(projectCode)) { 64 DDSTimer ddst = ddsMap.get(projectCode); 65 ddst.refreshTime(); 66 return ddst.getDds(); 67 } 68 69 return null; 70 } 71 72 /** 73 * 清除超时无人使用的数据源。 74 */ 75 public synchronized void clearIdleDDS() { 76 77 Iterator<Entry<String, DDSTimer>> iter = ddsMap.entrySet().iterator(); 78 for (; iter.hasNext(); ) { 79 80 Entry<String, DDSTimer> entry = iter.next(); 81 if (entry.getValue().checkAndClose()) 82 { 83 iter.remove(); 84 } 85 } 86 } 87 88 /** 89 * 单例构件类 90 * @author elon 91 * @version 2018年2月26日 92 */ 93 private static class DDSHolderBuilder { 94 private static DDSHolder instance = new DDSHolder(); 95 } 96 }
5) 定时器任务ClearIdleTimerTask用于定时清除空闲的数据源(ClearIdleTimerTask.java)
1 package com.elon.dds.datasource; 2 3 import java.util.TimerTask; 4 5 /** 6 * 清除空闲连接任务。 7 * 8 * @author elon 9 * @version 2018年2月26日 10 */ 11 public class ClearIdleTimerTask extends TimerTask { 12 13 @Override 14 public void run() { 15 DDSHolder.instance().clearIdleDDS(); 16 } 17 }
3. 管理项目编码与数据库IP和名称的映射关系(ProjectDBMgr.java)
1 package com.elon.dds.dbmgr; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 /** 7 * 项目数据库管理。提供根据项目编码查询数据库名称和IP的接口。 8 * @author elon 9 * @version 2018年2月25日 10 */ 11 public class ProjectDBMgr { 12 13 /** 14 * 保存项目编码与数据名称的映射关系。这里是硬编码,实际开发中这个关系数据可以保存到redis缓存中; 15 * 新增一个项目或者删除一个项目只需要更新缓存。到时这个类的接口只需要修改为从缓存拿数据。 16 */ 17 private Map<String, String> dbNameMap = new HashMap<String, String>(); 18 19 /** 20 * 保存项目编码与数据库IP的映射关系。 21 */ 22 private Map<String, String> dbIPMap = new HashMap<String, String>(); 23 24 private ProjectDBMgr() { 25 dbNameMap.put("project_001", "db_project_001"); 26 dbNameMap.put("project_002", "db_project_002"); 27 dbNameMap.put("project_003", "db_project_003"); 28 29 dbIPMap.put("project_001", "127.0.0.1"); 30 dbIPMap.put("project_002", "127.0.0.1"); 31 dbIPMap.put("project_003", "127.0.0.1"); 32 } 33 34 public static ProjectDBMgr instance() { 35 return ProjectDBMgrBuilder.instance; 36 } 37 38 // 实际开发中改为从缓存获取 39 public String getDBName(String projectCode) { 40 if (dbNameMap.containsKey(projectCode)) { 41 return dbNameMap.get(projectCode); 42 } 43 44 return ""; 45 } 46 47 //实际开发中改为从缓存中获取 48 public String getDBIP(String projectCode) { 49 if (dbIPMap.containsKey(projectCode)) { 50 return dbIPMap.get(projectCode); 51 } 52 53 return ""; 54 } 55 56 private static class ProjectDBMgrBuilder { 57 private static ProjectDBMgr instance = new ProjectDBMgr(); 58 } 59 }
4. 编写数据库访问的mapper(UserMapper.java)
1 package com.elon.dds.mapper; 2 3 import java.util.List; 4 5 import org.apache.ibatis.annotations.Mapper; 6 import org.apache.ibatis.annotations.Result; 7 import org.apache.ibatis.annotations.Results; 8 import org.apache.ibatis.annotations.Select; 9 10 import com.elon.dds.model.User; 11 12 /** 13 * Mybatis映射接口定义。 14 * 15 * @author elon 16 * @version 2018年2月26日 17 */ 18 @Mapper 19 public interface UserMapper 20 { 21 /** 22 * 查询所有用户数据 23 * @return 用户数据列表 24 */ 25 @Results(value= { 26 @Result(property="userId", column="id"), 27 @Result(property="name", column="name"), 28 @Result(property="age", column="age") 29 }) 30 @Select("select id, name, age from tbl_user") 31 List<User> getUsers(); 32 }
5. 定义查询对象模型(User.java)
1 package com.elon.dds.model; 2 3 public class User 4 { 5 private int userId = -1; 6 7 private String name = ""; 8 9 private int age = -1; 10 11 @Override 12 public String toString() 13 { 14 return "name:" + name + "|age:" + age; 15 } 16 17 public int getUserId() 18 { 19 return userId; 20 } 21 22 public void setUserId(int userId) 23 { 24 this.userId = userId; 25 } 26 27 public String getName() 28 { 29 return name; 30 } 31 32 public void setName(String name) 33 { 34 this.name = name; 35 } 36 37 public int getAge() 38 { 39 return age; 40 } 41 42 public void setAge(int age) 43 { 44 this.age = age; 45 } 46 }
6. 定义查询数据的restful接口(WSUser.java)
1 package com.elon.dds.rest; 2 3 import java.util.List; 4 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.bind.annotation.RequestMethod; 8 import org.springframework.web.bind.annotation.RequestParam; 9 import org.springframework.web.bind.annotation.RestController; 10 11 import com.elon.dds.datasource.DBIdentifier; 12 import com.elon.dds.mapper.UserMapper; 13 import com.elon.dds.model.User; 14 15 /** 16 * 用户数据访问接口。 17 * 18 * @author elon 19 * @version 2018年2月26日 20 */ 21 @RestController 22 @RequestMapping(value="/user") 23 public class WSUser { 24 25 @Autowired 26 private UserMapper userMapper; 27 28 /** 29 * 查询项目中所有用户信息 30 * 31 * @param projectCode 项目编码 32 * @return 用户列表 33 */ 34 @RequestMapping(value="/v1/users", method=RequestMethod.GET) 35 public List<User> queryUser(@RequestParam(value="projectCode", required=true) String projectCode) 36 { 37 DBIdentifier.setProjectCode(projectCode); 38 return userMapper.getUsers(); 39 } 40 }
要求每次查询都要带上projectCode参数。
7. 编写Spring Boot App的启动代码(App.java)
1 package com.elon.dds; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 6 /** 7 * Hello world! 8 * 9 */ 10 @SpringBootApplication 11 public class App 12 { 13 public static void main( String[] args ) 14 { 15 System.out.println( "Hello World!" ); 16 SpringApplication.run(App.class, args); 17 } 18 }
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 8 logging: 9 config: classpath:log4j2.xml
测试方案
1. 查询project_001的数据,正常返回
2. 查询project_002的数据,正常返回