动态数据源
在很多具体应用场景的时候,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务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
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.1
org.springframework.boot
spring-boot-starter-aop
mysql
mysql-connector-java
5.1.8
自定义配置文件
新建自定义配置文件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 packagecom.main.example.dynamic.datasource;2
3 importorg.springframework.boot.SpringApplication;4 importorg.springframework.boot.autoconfigure.SpringBootApplication;5 importorg.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;6
7 @SpringBootApplication(exclude ={8 DataSourceAutoConfiguration.class
9 })10 public classDynamicDatasourceApplication {11
12 public static voidmain(String[] args) {13 SpringApplication.run(DynamicDatasourceApplication.class, args);14 }15
16 }
数据源配置类
创建一个数据源配置类,主要做以下几件事情:
1. 配置 dao,model(bean),xml mapper文件的扫描路径。
2. 注入数据源配置属性,创建数据源。
3. 创建一个动态数据源,装入数据源。
4. 将动态数据源设置到SQL会话工厂和事务管理器。
如此,当进行数据库操作时,就会通过我们创建的动态数据源去获取要操作的数据源了。
DbSourceConfig.java:
1 packagecom.main.example.config.dao;2
3 importcom.main.example.common.DataEnum;4 importcom.main.example.common.DynamicDataSource;5 importorg.mybatis.spring.SqlSessionFactoryBean;6 importorg.springframework.boot.context.properties.ConfigurationProperties;7 importorg.springframework.boot.jdbc.DataSourceBuilder;8 importorg.springframework.context.annotation.Bean;9 importorg.springframework.context.annotation.Configuration;10 importorg.springframework.context.annotation.PropertySource;11 importorg.springframework.core.io.support.PathMatchingResourcePatternResolver;12 importorg.springframework.jdbc.datasource.DataSourceTransactionManager;13 importorg.springframework.transaction.PlatformTransactionManager;14
15 importjavax.sql.DataSource;16 importjava.util.HashMap;17 importjava.util.Map;18
19 //数据库配置统一在config/mysql/db.properties中
20 @Configuration21 @PropertySource(value = "classpath:config/mysql/db.properties")22 public classDbSourceConfig {23 private String typeAliasesPackage = "com.main.example.bean.**.*";24
25 @Bean(name = "exampleDataSource")26 @ConfigurationProperties(prefix = "spring.datasource.example")27 publicDataSource exampleDataSource() {28 returnDataSourceBuilder.create().build();29 }30
31 /*
32 * 动态数据源33 * dbMap中存放数据源名称与数据源实例,数据源名称存于DataEnum.DbSource中34 * setDefaultTargetDataSource方法设置默认数据源35 */
36 @Bean(name = "dynamicDataSource")37 publicDataSource dynamicDataSource() {38 DynamicDataSource dynamicDataSource = newDynamicDataSource();39 //配置多数据源
40 Map dbMap = newHashMap();41 dbMap.put(DataEnum.DbSource.example.getName(), exampleDataSource());42 dynamicDataSource.setTargetDataSources(dbMap);43
44 //设置默认数据源
45 dynamicDataSource.setDefaultTargetDataSource(exampleDataSource());46
47 returndynamicDataSource;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() throwsException {58 SqlSessionFactoryBean sqlSessionFactory = newSqlSessionFactoryBean();59 sqlSessionFactory.setDataSource(dynamicDataSource());60 sqlSessionFactory.setTypeAliasesPackage(typeAliasesPackage); //扫描bean
61 PathMatchingResourcePatternResolver resolver = newPathMatchingResourcePatternResolver();62 sqlSessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml")); //扫描映射文件
63
64 returnsqlSessionFactory;65 }66
67 @Bean68 publicPlatformTransactionManager transactionManager() {69 //配置事务管理, 使用事务时在方法头部添加@Transactional注解即可
70 return newDataSourceTransactionManager(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 packagecom.main.example.common;2
3 importorg.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;4
5 public class DynamicDataSource extendsAbstractRoutingDataSource {6
7 @Override8 protectedObject determineCurrentLookupKey() {9 returnDbSourceContext.getDbSource();10 }11
12 }
数据源上下文
动态数据源的切换主要是通过调用这个类的方法来完成的。在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。比如系统登录时,根据用户信息调用这个类的数据源切换方法切换到用户对应的数据库。完整代码如下:
DbSourceContext.java:
1 packagecom.main.example.common;2
3 importorg.apache.log4j.Logger;4
5 public classDbSourceContext {6 private static Logger logger = Logger.getLogger(DbSourceContext.class);7
8 private static final ThreadLocal dbContext = new ThreadLocal();9
10 public static voidsetDbSource(String source) {11 logger.debug("set source ====>" +source);12 dbContext.set(source);13 }14
15 public staticString getDbSource() {16 logger.debug("get source ====>" +dbContext.get());17 returndbContext.get();18 }19
20 public static voidclearDbSource() {21 dbContext.remove();22 }23 }
注解式数据源
到这里,在任何想要动态切换数据源的时候,只要调用DbSourceContext.setDbSource(key) 就可以完成了。
接下来我们实现通过注解的方式来进行数据源的切换,原理就是添加注解(如@DbSource(value="example")),然后实现注解切面进行数据源切换。
创建一个动态数据源注解,拥有一个value值,用于标识要切换的数据源的key。
DbSource.java:
1 packagecom.main.example.config.dao;2
3 import java.lang.annotation.*;4
5 /**
6 * 动态数据源注解7 *@author
8 * @date April 12, 20199 */
10 @Target({ElementType.METHOD, ElementType.TYPE})11 @Retention(RetentionPolicy.RUNTIME)12 @Documented13 public @interfaceDbSource {14 /**
15 * 数据源key值16 *@return
17 */
18 String value();19 }
创建一个AOP切面,拦截带 @DataSource 注解的方法,在方法执行前切换至目标数据源,执行完成后恢复到默认数据源。
DynamicDataSourceAspect.java:
1 packagecom.main.example.config.dao;2
3 importcom.main.example.common.DbSourceContext;4 importorg.apache.log4j.Logger;5 importorg.aspectj.lang.JoinPoint;6 importorg.aspectj.lang.annotation.After;7 importorg.aspectj.lang.annotation.Aspect;8 importorg.aspectj.lang.annotation.Before;9 importorg.springframework.core.annotation.Order;10 importorg.springframework.stereotype.Component;11
12 /**
13 * 动态数据源切换处理器14 *@authorlinzhibao15 * @date April 12, 201916 */
17 @Aspect18 @Order(-1) //该切面应当先于 @Transactional 执行
19 @Component20 public classDynamicDataSourceAspect {21 private static Logger logger = Logger.getLogger(DynamicDataSourceAspect.class);22 /**
23 * 切换数据源24 *@parampoint25 *@paramdbSource26 */
27 //@Before("@annotation(dbSource)") 注解在对应方法,拦截有@DbSource的方法28 //注解在类对象,拦截有@DbSource类下所有的方法
29 @Before("@within(dbSource)")30 public voidswitchDataSource(JoinPoint point, DbSource dbSource) {31 //切换数据源
32 DbSourceContext.setDbSource(dbSource.value());33 }34
35 /**
36 * 重置数据源37 *@parampoint38 *@paramdbSource39 */
40 //注解在类对象,拦截有@DbSource类下所有的方法
41 @After("@within(dbSource)")42 public voidrestoreDataSource(JoinPoint point, DbSource dbSource) {43 //将数据源置为默认数据源
44 DbSourceContext.clearDbSource();45 }46 }
到这里,动态数据源相关的处理代码就完成了。
编写用户业务代码
接下来编写用户查询业务代码,用来进行测试,Dao层只需添加一个查询接口即可。
ExampleDao.java:
1 packagecom.main.example.dao;2
3 importcom.main.example.common.DataEnum;4 importcom.main.example.config.dao.DbSource;5 importorg.springframework.context.annotation.Bean;6 importorg.springframework.stereotype.Component;7
8 importjavax.annotation.Resource;9 importjava.util.List;10
11 @Component("exampleDao")12 //切换数据源注解,以DataEnum.DbSource中的值为准
13 @DbSource("example")14 public class ExampleDao extendsDaoBase {15 private static final String MAPPER_NAME_SPACE = "com.main.example.dao.ExampleMapper";16
17 public ListselectAllMessages() {18 return selectList(MAPPER_NAME_SPACE, "selectAllMessages");19 }20 }
Controler代码:
TestExampleDao.java:
1 packagecom.main.example.dao;2
3 importorg.springframework.beans.factory.annotation.Autowired;4 importorg.springframework.web.bind.annotation.RequestMapping;5 importorg.springframework.web.bind.annotation.RestController;6
7 importjava.util.ArrayList;8 importjava.util.List;9
10 @RestController11 public classTestExampleDao {12 @Autowired13 ExampleDao exampleDao;14
15 @RequestMapping(value = "/test/example")16 public ListselectAllMessages() {17 try{18 List 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 returnldata;24 }catch(Exception e) {25 e.printStackTrace();26 }27
28 return new ArrayList<>();29 }30 }
ExampleMapper.xml代码:
/p>
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
SELECT
message
FROM example
测试效果
启动系统,访问 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包