如何通过Spring Boot配置动态数据源访问多个数据库

来源:https://www.2cto.com/kf/201803/730560.html

下面以Mysql为例,先在本地建3个数据库用于测试。需要说明的是本方案不限数据库数量,支持不同的数据库部署在不同的服务器上。如图所示db_project_001、db_project_002、db_project_003。
如何通过Spring Boot配置动态数据源访问多个数据库_第1张图片

二、搭建Java后台微服务项目

创建一个Spring Boot的maven项目:
如何通过Spring Boot配置动态数据源访问多个数据库_第2张图片

config:数据源配置。
datasource:自己实现的动态数据源相关类。
dbmgr:管理项目编码与数据库IP、名称的映射关系(实际项目中这部分数据保存在redis缓存中,可动态增删)。
mapper:mybatis的数据库访问接口。
model:映射模型。
rest:微服务对外发布的restful接口,这里用来测试。
application.yml:配置数据库JDBC参数。

三、详细的代码实现

1、数据源配置管理类(DataSourceConfig.java)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

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)首先增加一个数据库标识类,用于区分不同的数据库(DBIdentifier.java)

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

package com.elon.dds.datasource;

 

/**

 * 数据库标识管理类。用于区分数据源连接的不同数据库。

 *

 * @author elon

 * @version 2018-02-25

 */

public class DBIdentifier {

 

    /**

     * 用不同的工程编码来区分数据库

     */

    private static ThreadLocal projectCode = new ThreadLocal();

 

    public static String getProjectCode() {

        return projectCode.get();

    }

 

    public static void setProjectCode(String code) {

        projectCode.set(code);

    }

}

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

package com.elon.dds.datasource;

 

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)

            {

                //有一些static final的属性不能修改。忽略。

                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控制数据连接释放(DDSTimer.java)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

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来管理不同的数据源,提供数据源的添加、查询功能(DDSHolder.java)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

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""> ddsMap = new HashMap"">();

 

    /**

     * 通过定时任务周期性清除不使用的数据源

     */

    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"">> iter = ddsMap.entrySet().iterator();

        for (; iter.hasNext(); ) {

 

            Entry""> 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用于定时清除空闲的数据源(ClearIdleTimerTask.java)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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();

    }

}

(6)管理项目编码与数据库IP和名称的映射关系(ProjectDBMgr.java)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

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""> dbNameMap = new HashMap"">();

 

    /**

     * 保存项目编码与数据库IP的映射关系。

     */

    private Map""> dbIPMap = new HashMap"">();

 

    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();

    }

}

(7)编写数据库访问的mapper(UserMapper.java)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

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 getUsers();

}

(8)定义查询对象模型(User.java)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

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;

    }

}

(9)定义查询数据的restful接口(WSUser.java)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

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 queryUser(@RequestParam(value="projectCode", required=true) String projectCode)

    {

        DBIdentifier.setProjectCode(projectCode);

        return userMapper.getUsers();

    }

}

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

(10)编写Spring Boot App的启动代码(App.java)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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);

    }

}

四、在application.yml中配置数据源

其中的数据库IP和数据库名称使用%s。在执行数据操作时动态切换。

?

1

2

3

4

5

6

7

8

9

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配置动态数据源访问多个数据库_第3张图片

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

如何通过Spring Boot配置动态数据源访问多个数据库_第4张图片

你可能感兴趣的:(如何通过Spring Boot配置动态数据源访问多个数据库)