详解Spring Boot + Mybatis 实现动态数据源
作者:Boblim 发布时间:2023-06-08 13:53:18
动态数据源
在很多具体应用场景的时候,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务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 实现动态数据源详解整合网站的支持!
来源:https://www.cnblogs.com/fnlingnzb-learner/p/10710145.html#top


猜你喜欢
- 介绍:%是求余运算符,也叫模除运算符,用于求余数。%要求两个操作数均为整数(或可以隐式转换成整数的类型)。标准规定:如果%左边的操作数为负数
- 问题,打一个页面cpu暴涨,打开一次就涨100%,一会系统就卡的不行了。排查方法,因为是线上的linux,没有用jvm监控工具rim链接上去
- 代码一:class Program { static int
- 一、什么是 RestTemplate?RestTemplate是执行HTTP请求的同步阻塞式的客户端,它在HTTP客户端库(例如JDK Ht
- 用户列表页面开发项目介绍用户列表页面开发,可以实现简单的查询,删除,修改,和添加用户信息功能。前端使用vue框架,后端使用springboo
- 提示:建议一定要看后面的@RequestBody的核心逻辑源码以及六个重要结论!本文前半部分的内容都是一些基本知识常识,可选择性跳过。声明:
- 以前传统的比较方式是遍历图片中的每一个像素,然后进行比对。这样的比对在少量图片的比对上虽然效率低一点,但是也没有什么不好。但是在大量图片比对
- 大家可能在做app的时候,或多或少需要使用联系人,而根据google提供的api,你需要编写大量的代码,例如首先需要查询数据库,涉及到数据库
- 一、基于框架1.IDEIntelliJ IDEA2.软件环境Spring bootmysqlmybatisorg.apache.poi二、环
- 工具方法:本文的目的是把json串转成map键值对存储,而且只存储叶节点的数据maven 引用jar包版本:<dependency&g
- 面试题:1.如何保证多线程下 i++ 结果正确?2.一个线程如果出现了运行时异常会怎么样?3.一个线程运行时发生异常会怎样?为了避免临界区的
- 一、项目简述(+需求文档+PPT)功能:卡管理,卡消费,卡充值,图书借阅,消费,记录,注销等等功能。二、项目运行环境配置:Jdk1.8 +
- 太多的if-else不太直观,难以维护。 以下面代码为例,展示几种替代if else的方法。String input = &quo
- 二维数组实现数字拼图,供大家参考,具体内容如下二维数组可以自己随意定义大小,通过方法判断来实现对所有的数字进行随机打乱,并可以通过移动来正确
- webservice的POST和GET请求调用POST请求1.发送请求import java.io.DataOutputStream;imp
- Java是垃圾回收语言的一种,其优点是开发者无需特意管理内存分配,降低了应用由于局部故障(segmentation fault)导致崩溃,同
- 一、XML DOM概述XML 文档大小写敏感、属性用引号括起来,每一个标记都要闭合。DOM是XML文档的内存中树状的表示形式。继承关系图:X
- 该方法针对idea版本(2020.2.x)C:\Users\yanghao\AppData\Roaming\JetBrains\Intell
- Recyclerview现在基本已经替代Listview了,RecyclerView也越来越好用了 当我们有实现条目的拖拽排序和
- 好久没有更新了,之前公司在做 关注/粉丝 这块儿缓存的时候,我选择的就是 Bitmap ,那时是我第一次见识到这种数据数组形式,用到的有 S