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