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

数据库环境准备

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

 

 


Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐