Spring Boot + Mybatis 实现动态数据源

动态数据源

在很多具体应用场景的时候,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务A要访问A数据库,业务B要访问B数据库等,都可以使用动态数据源方案进行解决。接下来,我们就来讲解如何实现动态数据源,以及在过程中剖析动态数据源背后的实现原理。

实现案例

本教程案例基于 Spring Boot + Mybatis + MySQL 实现。

数据库设计

首先需要安装好MySQL数据库,新建数据库 example,创建example表,用来测试数据源,SQL脚本如下:

 CREATE TABLE `example` (
  `pk` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `message` varchar(100) NOT NULL,
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `modify_time` datetime DEFAULT NULL COMMENT '生效时间',
  PRIMARY KEY (`pk`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='测试用例表'

添加依赖

添加Spring Boot,Spring Aop,Mybatis,MySQL相关依赖。

pom.xml

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.1</version>
    </dependency>
    <!-- spring aop -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.8</version>
    </dependency>

自定义配置文件

新建自定义配置文件resource/config/mysql/db.properties,添加数据源:

#数据库设置
spring.datasource.example.jdbc-url=jdbc:mysql://localhost:3306/example?characterEncoding=UTF-8
spring.datasource.example.username=root
spring.datasource.example.password=123456
spring.datasource.example.driver-class-name=com.mysql.jdbc.Driver

启动类

启动类添加 exclude = {DataSourceAutoConfiguration.class}, 以禁用数据源默认自动配置。

数据源默认自动配置会读取 spring.datasource.* 的属性创建数据源,所以要禁用以进行定制。

DynamicDatasourceApplication.java:

 1 package com.main.example.dynamic.datasource;
 2 
 3 import org.springframework.boot.SpringApplication;
 4 import org.springframework.boot.autoconfigure.SpringBootApplication;
 5 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 6 
 7 @SpringBootApplication(exclude = {
 8         DataSourceAutoConfiguration.class
 9 })
10 public class DynamicDatasourceApplication {
11  
12   public static void main(String[] args) {
13     SpringApplication.run(DynamicDatasourceApplication.class, args);
14   }
15 
16 }

数据源配置类

创建一个数据源配置类,主要做以下几件事情:

1. 配置 dao,model(bean),xml mapper文件的扫描路径。

2. 注入数据源配置属性,创建数据源。

3. 创建一个动态数据源,装入数据源。

4. 将动态数据源设置到SQL会话工厂和事务管理器。

如此,当进行数据库操作时,就会通过我们创建的动态数据源去获取要操作的数据源了。

DbSourceConfig.java:

 1 package com.main.example.config.dao;
 2 
 3 import com.main.example.common.DataEnum;
 4 import com.main.example.common.DynamicDataSource;
 5 import org.mybatis.spring.SqlSessionFactoryBean;
 6 import org.springframework.boot.context.properties.ConfigurationProperties;
 7 import org.springframework.boot.jdbc.DataSourceBuilder;
 8 import org.springframework.context.annotation.Bean;
 9 import org.springframework.context.annotation.Configuration;
10 import org.springframework.context.annotation.PropertySource;
11 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
12 import org.springframework.jdbc.datasource.DataSourceTransactionManager;
13 import org.springframework.transaction.PlatformTransactionManager;
14 
15 import javax.sql.DataSource;
16 import java.util.HashMap;
17 import java.util.Map;
18 
19 //数据库配置统一在config/mysql/db.properties中
20 @Configuration
21 @PropertySource(value = "classpath:config/mysql/db.properties")
22 public class DbSourceConfig {
23     private String typeAliasesPackage = "com.main.example.bean.**.*";
24 
25     @Bean(name = "exampleDataSource")
26     @ConfigurationProperties(prefix = "spring.datasource.example")
27     public DataSource exampleDataSource() {
28         return DataSourceBuilder.create().build();
29     }
30 
31     /*
32      * 动态数据源
33      * dbMap中存放数据源名称与数据源实例,数据源名称存于DataEnum.DbSource中
34      * setDefaultTargetDataSource方法设置默认数据源
35      */
36     @Bean(name = "dynamicDataSource")
37     public DataSource dynamicDataSource() {
38         DynamicDataSource dynamicDataSource = new DynamicDataSource();
39         //配置多数据源
40         Map<Object, Object> dbMap = new HashMap();
41         dbMap.put(DataEnum.DbSource.example.getName(), exampleDataSource());
42         dynamicDataSource.setTargetDataSources(dbMap);
43 
44         // 设置默认数据源
45         dynamicDataSource.setDefaultTargetDataSource(exampleDataSource());
46 
47         return dynamicDataSource;
48     }
49 
50     /*
51      * 数据库连接会话工厂
52      * 将动态数据源赋给工厂
53      * mapper存于resources/mapper目录下
54      * 默认bean存于com.main.example.bean包或子包下,也可直接在mapper中指定
55      */
56     @Bean(name = "sqlSessionFactory")
57     public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
58         SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
59         sqlSessionFactory.setDataSource(dynamicDataSource());
60         sqlSessionFactory.setTypeAliasesPackage(typeAliasesPackage); //扫描bean
61         PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
62         sqlSessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));    // 扫描映射文件
63 
64         return sqlSessionFactory;
65     }
66 
67     @Bean
68     public PlatformTransactionManager transactionManager() {
69         // 配置事务管理, 使用事务时在方法头部添加@Transactional注解即可
70         return new DataSourceTransactionManager(dynamicDataSource());
71     }
72 }

动态数据源类

我们上一步把这个动态数据源设置到了SQL会话工厂和事务管理器,这样在操作数据库时就会通过动态数据源类来获取要操作的数据源了。

动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource 中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key。

所以如果我们需要动态切换数据源,就可以通过以下两种方式定制:

1. 覆写 determineCurrentLookupKey 方法

通过覆写 determineCurrentLookupKey 方法,从一个自定义的 DbSourceContext.getDbSource() 获取数据源key值,这样在我们想动态切换数据源的时候,只要通过  DbSourceContext.setDbSource(key)  的方式就可以动态改变数据源了。这种方式要求在获取数据源之前,要先初始化各个数据源到 DbSourceContext 中,我们案例就是采用这种方式实现的,所以要将数据源都事先初始化到DynamicDataSource 中。

2. 可以通过覆写 determineTargetDataSource,因为数据源就是在这个方法创建并返回的,所以这种方式就比较自由了,支持到任何你希望的地方读取数据源信息,只要最终返回一个 DataSource 的实现类即可。比如你可以到数据库、本地文件、网络接口等方式读取到数据源信息然后返回相应的数据源对象就可以了。

DynamicDataSource.java:

 1 package com.main.example.common;
 2 
 3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 4 
 5 public class DynamicDataSource extends AbstractRoutingDataSource {
 6 
 7     @Override
 8     protected Object determineCurrentLookupKey() {
 9         return DbSourceContext.getDbSource();
10     }
11 
12 }

数据源上下文

动态数据源的切换主要是通过调用这个类的方法来完成的。在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。比如系统登录时,根据用户信息调用这个类的数据源切换方法切换到用户对应的数据库。完整代码如下:

DbSourceContext.java:

 1 package com.main.example.common;
 2 
 3 import org.apache.log4j.Logger;
 4 
 5 public class DbSourceContext {
 6     private static Logger logger = Logger.getLogger(DbSourceContext.class);
 7 
 8     private static final ThreadLocal<String> dbContext = new ThreadLocal<String>();
 9 
10     public static void setDbSource(String source) {
11         logger.debug("set source ====>" + source);
12         dbContext.set(source);
13     }
14 
15     public static String getDbSource() {
16         logger.debug("get source ====>" + dbContext.get());
17         return dbContext.get();
18     }
19 
20     public static void clearDbSource() {
21         dbContext.remove();
22     }
23 }

注解式数据源

到这里,在任何想要动态切换数据源的时候,只要调用DbSourceContext.setDbSource(key)  就可以完成了。

接下来我们实现通过注解的方式来进行数据源的切换,原理就是添加注解(如@DbSource(value="example")),然后实现注解切面进行数据源切换。

创建一个动态数据源注解,拥有一个value值,用于标识要切换的数据源的key。

DbSource.java:

 1 package com.main.example.config.dao;
 2 
 3 import java.lang.annotation.*;
 4 
 5 /**
 6  * 动态数据源注解
 7  * @author
 8  * @date April 12, 2019
 9  */
10 @Target({ElementType.METHOD, ElementType.TYPE})
11 @Retention(RetentionPolicy.RUNTIME)
12 @Documented
13 public @interface DbSource {
14     /**
15      * 数据源key值
16      * @return
17      */
18     String value();
19 }

创建一个AOP切面,拦截带 @DataSource 注解的方法,在方法执行前切换至目标数据源,执行完成后恢复到默认数据源。

DynamicDataSourceAspect.java:

 1 package com.main.example.config.dao;
 2 
 3 import com.main.example.common.DbSourceContext;
 4 import org.apache.log4j.Logger;
 5 import org.aspectj.lang.JoinPoint;
 6 import org.aspectj.lang.annotation.After;
 7 import org.aspectj.lang.annotation.Aspect;
 8 import org.aspectj.lang.annotation.Before;
 9 import org.springframework.core.annotation.Order;
10 import org.springframework.stereotype.Component;
11 
12 /**
13  * 动态数据源切换处理器
14  * @author linzhibao
15  * @date April 12, 2019
16  */
17 @Aspect
18 @Order(-1)  // 该切面应当先于 @Transactional 执行
19 @Component
20 public class DynamicDataSourceAspect {
21     private static Logger logger = Logger.getLogger(DynamicDataSourceAspect.class);
22     /**
23      * 切换数据源
24      * @param point
25      * @param dbSource
26      */
27     //@Before("@annotation(dbSource)") 注解在对应方法,拦截有@DbSource的方法
28     //注解在类对象,拦截有@DbSource类下所有的方法
29     @Before("@within(dbSource)")
30     public void switchDataSource(JoinPoint point, DbSource dbSource) {
31             // 切换数据源
32             DbSourceContext.setDbSource(dbSource.value());
33     }
34 
35     /**
36      * 重置数据源
37      * @param point
38      * @param dbSource
39      */
40     //注解在类对象,拦截有@DbSource类下所有的方法
41     @After("@within(dbSource)")
42     public void restoreDataSource(JoinPoint point, DbSource dbSource) {
43         // 将数据源置为默认数据源
44         DbSourceContext.clearDbSource();
45     }
46 }

到这里,动态数据源相关的处理代码就完成了。

编写用户业务代码

接下来编写用户查询业务代码,用来进行测试,Dao层只需添加一个查询接口即可。

ExampleDao.java:

 1 package com.main.example.dao;
 2 
 3 import com.main.example.common.DataEnum;
 4 import com.main.example.config.dao.DbSource;
 5 import org.springframework.context.annotation.Bean;
 6 import org.springframework.stereotype.Component;
 7 
 8 import javax.annotation.Resource;
 9 import java.util.List;
10 
11 @Component("exampleDao")
12 //切换数据源注解,以DataEnum.DbSource中的值为准
13 @DbSource("example")
14 public class ExampleDao extends DaoBase {
15     private static final String MAPPER_NAME_SPACE = "com.main.example.dao.ExampleMapper";
16 
17     public List<String> selectAllMessages() {
18         return selectList(MAPPER_NAME_SPACE, "selectAllMessages");
19     }
20 }

Controler代码:

TestExampleDao.java:

 1 package com.main.example.dao;
 2 
 3 import org.springframework.beans.factory.annotation.Autowired;
 4 import org.springframework.web.bind.annotation.RequestMapping;
 5 import org.springframework.web.bind.annotation.RestController;
 6 
 7 import java.util.ArrayList;
 8 import java.util.List;
 9 
10 @RestController
11 public class TestExampleDao {
12     @Autowired
13     ExampleDao exampleDao;
14 
15     @RequestMapping(value = "/test/example")
16     public List<String> selectAllMessages() {
17         try {
18             List<String> ldata = exampleDao.selectAllMessages();
19             if(ldata == null){System.out.println("*********it is null.***********");return null;}
20             for(String d : ldata) {
21                 System.out.println(d);
22             }
23             return ldata;
24         }catch(Exception e) {
25             e.printStackTrace();
26         }
27 
28         return new ArrayList<>();
29     }
30 }

ExampleMapper.xml代码:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.main.example.dao.ExampleMapper">
    <select id="selectAllMessages" resultType="java.lang.String">
        SELECT
        message
        FROM example
    </select>

</mapper>

测试效果

启动系统,访问 http://localhost:80/test/example,分别测试两个接口,成功返回数据。

可能遇到的问题

1.报错:java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName

原因:

spring boot从1.X升级到2.X版本之后,一些配置及用法有了变化,如果不小心就会碰到“jdbcUrl is required with driverClassName.”的错误

解决方法:

在1.0 配置数据源的过程中主要是写成:spring.datasource.url 和spring.datasource.driverClassName。

而在2.0升级之后需要变更成:spring.datasource.jdbc-url和spring.datasource.driver-class-name即可解决!

 2.自定义配置文件

自定义配置文件需要在指定配置类上加上@PropertySource标签,例如:

@PropertySource(value = "classpath:config/mysql/db.properties")

若是作用于配置类中的方法,则在方法上加上@ConfigurationProperties,例如:

@ConfigurationProperties(prefix = "spring.datasource.example")

配置项前缀为spring.datasource.example

若是作用于配置类上,则在类上加上@ConfigurationProperties(同上),并且在启动类上加上@EnableConfigurationProperties(XXX.class)

3.多数据源

需要在启动类上取消自动装载数据源,如:

@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class
})

附: 

如果想在数据层数据层直接使用mapper,只需要在对应的包下建立和*mapper.xml中namespace对应的类,然后在该类上加上@Mapper标注,或者在程序初始时使用@MapperScan扫描全mapper包

转载请注明出处:https://www.cnblogs.com/fnlingnzb-learner/p/10710145.html

原文地址:https://www.cnblogs.com/fnlingnzb-learner/p/10710145.html