方案能支持数据库动态增删,数量不限。
数据库环境准备
下面以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的数据,正常返回