动态数据源
在很多具体应用场景的时候,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务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:
package com.main.example.dynamic.datasource; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) public class DynamicDatasourceApplication { public static void main(String[] args) { SpringApplication.run(DynamicDatasourceApplication.class, args); } }
数据源配置类
创建一个数据源配置类,主要做以下几件事情:
1. 配置 dao,model(bean),xml mapper文件的扫描路径。
2. 注入数据源配置属性,创建数据源。
3. 创建一个动态数据源,装入数据源。
4. 将动态数据源设置到SQL会话工厂和事务管理器。
如此,当进行数据库操作时,就会通过我们创建的动态数据源去获取要操作的数据源了。
DbSourceConfig.java:
package com.main.example.config.dao; import com.main.example.common.DataEnum; import com.main.example.common.DynamicDataSource; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; //数据库配置统一在config/mysql/db.properties中 @Configuration @PropertySource(value = "classpath:config/mysql/db.properties") public class DbSourceConfig { private String typeAliasesPackage = "com.main.example.bean.**.*"; @Bean(name = "exampleDataSource") @ConfigurationProperties(prefix = "spring.datasource.example") public DataSource exampleDataSource() { return DataSourceBuilder.create().build(); } /* * 动态数据源 * dbMap中存放数据源名称与数据源实例,数据源名称存于DataEnum.DbSource中 * setDefaultTargetDataSource方法设置默认数据源 */ @Bean(name = "dynamicDataSource") public DataSource dynamicDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); //配置多数据源 Map<Object, Object> dbMap = new HashMap(); dbMap.put(DataEnum.DbSource.example.getName(), exampleDataSource()); dynamicDataSource.setTargetDataSources(dbMap); // 设置默认数据源 dynamicDataSource.setDefaultTargetDataSource(exampleDataSource()); return dynamicDataSource; } /* * 数据库连接会话工厂 * 将动态数据源赋给工厂 * mapper存于resources/mapper目录下 * 默认bean存于com.main.example.bean包或子包下,也可直接在mapper中指定 */ @Bean(name = "sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dynamicDataSource()); sqlSessionFactory.setTypeAliasesPackage(typeAliasesPackage); //扫描bean PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); sqlSessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml")); // 扫描映射文件 return sqlSessionFactory; } @Bean public PlatformTransactionManager transactionManager() { // 配置事务管理, 使用事务时在方法头部添加@Transactional注解即可 return new DataSourceTransactionManager(dynamicDataSource()); } }
动态数据源类
我们上一步把这个动态数据源设置到了SQL会话工厂和事务管理器,这样在操作数据库时就会通过动态数据源类来获取要操作的数据源了。
动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource 中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key。
所以如果我们需要动态切换数据源,就可以通过以下两种方式定制:
1. 覆写 determineCurrentLookupKey 方法
通过覆写 determineCurrentLookupKey 方法,从一个自定义的 DbSourceContext.getDbSource() 获取数据源key值,这样在我们想动态切换数据源的时候,只要通过 DbSourceContext.setDbSource(key) 的方式就可以动态改变数据源了。这种方式要求在获取数据源之前,要先初始化各个数据源到 DbSourceContext 中,我们案例就是采用这种方式实现的,所以要将数据源都事先初始化到DynamicDataSource 中。
2. 可以通过覆写 determineTargetDataSource,因为数据源就是在这个方法创建并返回的,所以这种方式就比较自由了,支持到任何你希望的地方读取数据源信息,只要最终返回一个 DataSource 的实现类即可。比如你可以到数据库、本地文件、网络接口等方式读取到数据源信息然后返回相应的数据源对象就可以了。
DynamicDataSource.java:
package com.main.example.common; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DbSourceContext.getDbSource(); } }
数据源上下文
动态数据源的切换主要是通过调用这个类的方法来完成的。在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。比如系统登录时,根据用户信息调用这个类的数据源切换方法切换到用户对应的数据库。完整代码如下:
DbSourceContext.java:
package com.main.example.common; import org.apache.log4j.Logger; public class DbSourceContext { private static Logger logger = Logger.getLogger(DbSourceContext.class); private static final ThreadLocal<String> dbContext = new ThreadLocal<String>(); public static void setDbSource(String source) { logger.debug("set source ====>" + source); dbContext.set(source); } public static String getDbSource() { logger.debug("get source ====>" + dbContext.get()); return dbContext.get(); } public static void clearDbSource() { dbContext.remove(); } }
注解式数据源
到这里,在任何想要动态切换数据源的时候,只要调用DbSourceContext.setDbSource(key) 就可以完成了。
接下来我们实现通过注解的方式来进行数据源的切换,原理就是添加注解(如@DbSource(value="example")),然后实现注解切面进行数据源切换。
创建一个动态数据源注解,拥有一个value值,用于标识要切换的数据源的key。
DbSource.java:
package com.main.example.config.dao; import java.lang.annotation.*; /** * 动态数据源注解 * @author * @date April 12, 2019 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DbSource { /** * 数据源key值 * @return */ String value(); }
创建一个AOP切面,拦截带 @DataSource 注解的方法,在方法执行前切换至目标数据源,执行完成后恢复到默认数据源。
DynamicDataSourceAspect.java:
package com.main.example.config.dao; import com.main.example.common.DbSourceContext; import org.apache.log4j.Logger; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /** * 动态数据源切换处理器 * @author linzhibao * @date April 12, 2019 */ @Aspect @Order(-1) // 该切面应当先于 @Transactional 执行 @Component public class DynamicDataSourceAspect { private static Logger logger = Logger.getLogger(DynamicDataSourceAspect.class); /** * 切换数据源 * @param point * @param dbSource */ //@Before("@annotation(dbSource)") 注解在对应方法,拦截有@DbSource的方法 //注解在类对象,拦截有@DbSource类下所有的方法 @Before("@within(dbSource)") public void switchDataSource(JoinPoint point, DbSource dbSource) { // 切换数据源 DbSourceContext.setDbSource(dbSource.value()); } /** * 重置数据源 * @param point * @param dbSource */ //注解在类对象,拦截有@DbSource类下所有的方法 @After("@within(dbSource)") public void restoreDataSource(JoinPoint point, DbSource dbSource) { // 将数据源置为默认数据源 DbSourceContext.clearDbSource(); } }
到这里,动态数据源相关的处理代码就完成了。
编写用户业务代码
接下来编写用户查询业务代码,用来进行测试,Dao层只需添加一个查询接口即可。
ExampleDao.java:
package com.main.example.dao; import com.main.example.common.DataEnum; import com.main.example.config.dao.DbSource; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.List; @Component("exampleDao") //切换数据源注解,以DataEnum.DbSource中的值为准 @DbSource("example") public class ExampleDao extends DaoBase { private static final String MAPPER_NAME_SPACE = "com.main.example.dao.ExampleMapper"; public List<String> selectAllMessages() { return selectList(MAPPER_NAME_SPACE, "selectAllMessages"); } }
Controler代码:
TestExampleDao.java:
package com.main.example.dao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; @RestController public class TestExampleDao { @Autowired ExampleDao exampleDao; @RequestMapping(value = "/test/example") public List<String> selectAllMessages() { try { List<String> ldata = exampleDao.selectAllMessages(); if(ldata == null){System.out.println("*********it is null.***********");return null;} for(String d : ldata) { System.out.println(d); } return ldata; }catch(Exception e) { e.printStackTrace(); } return new ArrayList<>(); } }
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">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 })
以上所述是小编给大家介绍的Spring Boot + Mybatis 实现动态数据源详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对小牛知识库网站的支持!
本文向大家介绍Mybatis动态SQL实例详解,包括了Mybatis动态SQL实例详解的使用技巧和注意事项,需要的朋友参考一下 动态SQL 什么是动态SQL? MyBatis的官方文档中是这样介绍的? 动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列
本文向大家介绍详解Mybatis动态sql,包括了详解Mybatis动态sql的使用技巧和注意事项,需要的朋友参考一下 1.什么是mybatis动态sql 看到动态,我们就应该想到,这是一个可以变化的sql语句 MyBatis的动态SQL是基于OGNL表达式的,它可以帮助我们方便的在SQL语句中实现某些逻辑 2.mybatis动态sql使用前准备 a.数据库表 b.创建类 3.使用mybatis动
本文向大家介绍SpringBoot 使用Mybatis分页插件实现详解,包括了SpringBoot 使用Mybatis分页插件实现详解的使用技巧和注意事项,需要的朋友参考一下 这篇文章主要介绍了SpringBoot 使用Mybatis分页插件实现详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1、导入分页插件包和jpa包 2、增加分页配置 配
本文向大家介绍Mybatis注解实现多数据源读写分离详解,包括了Mybatis注解实现多数据源读写分离详解的使用技巧和注意事项,需要的朋友参考一下 首先需要建立两个库进行测试,我这里使用的是master_test和slave_test两个库,两张库都有一张同样的表(偷懒,喜喜),表结构 表名 t_user | 字段名 | 类型 | 备注 | | :------: | :------: | :---
本文向大家介绍springboot+mybatis-plus实现内置的CRUD使用详解,包括了springboot+mybatis-plus实现内置的CRUD使用详解的使用技巧和注意事项,需要的朋友参考一下 springboot+mybatis-plus实现内置的CRUD使用详情,具体修改删除操作内容后文也有详细说明 mybatis-plus的特性 无侵入:只做增强不做改变,引入它不会对现有工程产
本文向大家介绍mybatis的动态sql详解(精),包括了mybatis的动态sql详解(精)的使用技巧和注意事项,需要的朋友参考一下 MyBatis 的一个强大的特性之一通常是它的动态 SQL 能力。如果你有使用 JDBC 或其他 相似框架的经验,你就明白条件地串联 SQL 字符串在一起是多么的痛苦,确保不能忘了空 格或在列表的最后省略逗号。动态 SQL 可以彻底处理这种痛苦。 通常使用动态SQ