在一个项目中使用多个数据源的情况很多,所以动态切换数据源是项目中标配的功能,当然网上有相关的依赖可以使用,比如动态数据源,其依赖为,
com.baomidou dynamic-datasource-spring-boot-starter 3.5.1
今天,不使用现成的API,手动实现一个动态数据源。
一、环境及依赖在springboot、mybatis-plus的基础上实现动态数据源切换,
【资料图】
springboot:2.3.3.RELEASE
mybatis-plus-boot-starter:3.5.0
mysql驱动:8.0.32
除了这些依赖外没有其他的,目标是动态切换数据源。
二、实现思路先来看下,单数据源的情况。
在使用springboot和mybatis-plus时,我们没有配置数据源(DataSource),只配置了数据库相关的信息,便可以连接数据库进行数据库的操作,这是为什么呐。其实是基于spring-boot的自动配置,也就是autoConfiguration,在自动配置下有DataSourceAutoConfiguration类,该类会生成一个数据源并注入到spring的容器中,这样就可以使用该数据源提供的连接,访问数据库了。
感兴趣的小伙伴可以了解下这个类的具体实现逻辑。
要实现多数据源,并且可以自动切换。那么肯定就不能再使用DataSourceAutoConfigurtation了,因为它只能产生一个数据源,多个数据源要怎么办,spring提供了AbstractRoutingDataSource类,该类是一个抽象类,仅有一个抽象方法需要实现
Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context.Allows for arbitrary keys. The returned key needs to match the stored lookup key type, as resolved by the resolveSpecifiedLookupKey method.@Nullableprotected abstract Object determineCurrentLookupKey();
可以根据该类实现一个动态数据源。好了,现在了解了实现思路,开始实现一个动态数据源,要做以下的准备工作。
1、配置文件;
2、自定义动态数据源;
2.1、配置文件由于是多数据源,那么在配置文件中肯定是多个配置,不能再是一个数据库的配置了,这里使用两个mysql的配置进行演示,
#master 默认数据源spring: datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false username: root password: 123456#slave 从数据源 slave: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/test2?serverTimezone=GMT%2B8&autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false username: root password: 123456
这里使用了一个master一个slave两个数据源配置,其地址是一致的,但数据库示例不一样。 有了数据源的信息下一步要实现自己的数据源,
2.2、自定义动态数据源前边说,spring提供了AbstractRoutingDataSource类可以实现动态数据源,看下实现。
DynamicDatasource.java
package com.wcj.my.config.dynamic.source;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;/** * 动态数据源 * @date 2023/6/8 19:18 */public class DynamicDatasource extends AbstractRoutingDataSource { /** * Determine the current lookup key. This will typically be * implemented to check a thread-bound transaction context. * Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ @Override protected Object determineCurrentLookupKey() { return DynamicDatasourceHolder.getDataSource(); }}
这里的determineCurrentLookupKey方法,需要返回一个数据源,也就是说返回一个数据源的映射,这里返回一个DynamicDatasourceHolder.getDataSource()方法的返回值,DynamicDatasourceHolder是一个保存多个数据源的地方,
DynamicDatasourceHolder.java
package com.wcj.my.config.dynamic.source;import java.util.Queue;import java.util.concurrent.ArrayBlockingQueue;/** * @date 2023/6/8 19:42 */public class DynamicDatasourceHolder { //保存数据源的映射 private static Queue queue = new ArrayBlockingQueue(1); public static String getDataSource() { return queue.peek(); } public static void setDataSource(String dataSourceKey) { queue.add(dataSourceKey); } public static void removeDataSource(String dataSourceKey) { queue.remove(dataSourceKey); }}
该类很简单,使用一个队列保存数据源的映射,提供获取/设置数据源的方法。
这里使用ThreadLocal类更合适,这样可以实现线程的隔离,一个请求会有一个线程来处理,保证每隔线程使用的数据源是一样的。
到现在为止依旧没有出现如何创建多数据源,下面就来了,不着急。
DynamicDatasourceConfig.java
package com.wcj.my.config.dynamic.source;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;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.Primary;import javax.sql.DataSource;import java.util.HashMap;import java.util.Map;/** * @date 2023/6/8 19:51 */@Configuration@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})public class DynamicDatasourceConfig { @Bean("master") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDatasource(){ return DataSourceBuilder.create().build(); } @Bean("slave") @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaveDatasource(){ return DataSourceBuilder.create().build(); } @Bean @Primary public DataSource dataSource(){ Map
首先,在该类上有个一个@Configuration注解,标明这是一个配置类;
其次,有一个@EnableAutonConfiguration注解,该注解中有个数组类型的exclude属性,排除不需要自动配置的类,这里排除的是当然就是DataSourceAutoConfiguration类了;因为下面会自动生成数据源,不需要自动配置了;
然后,在类中是标有@Bean的方法,这些方法便是生成数据源类,且映射为”master“、”slave“,可以有多个。使用的是DataSourceBuilder类帮助生成;
最后,生成一个DynamicDatasource,且标有@Primary注解,这里需要设置”master“、”slave“两个映射代表的数据源;
这样便向spring容器中注入了三个数据源,分别是”master“、”slave“代表的数据源,他们是需要实际使用的数据源。还有一个是DynamicDatasource,提供数据源的设置。这三个都是DataSource的子类。
三、使用多数据源上面已经完成了多数据源的配置,下面看怎么使用吧,还记得DynamicDatasourceHolder类中有set/get方法吗,就是使用这个类提供的方法,
UserSerivce.java
package com.wcj.my.service;import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;import com.wcj.my.dto.UserDto;import com.wcj.my.entity.User;import com.wcj.my.mapper.UserMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/** * @date 2023/6/8 15:19 */@Servicepublic class UserService { @Autowired private UserMapper userMapper; /**默认使用master数据源 */ public boolean saveUser(UserDto userDto) { User user = new User(); user.setUName(userDto.getName()); user.setUCode(userDto.getCode()); user.setUAge(userDto.getAge()); user.setUAddress(userDto.getAddress()); int num = userMapper.insert(user); if (num > 0) { return true; } return false; } /** *使用slave数据源 */ public boolean saveUserSlave(UserDto userDto) { DynamicDatasourceHolder.setDataSource("slave"); User user = new User(); user.setUName(userDto.getName()); user.setUCode(userDto.getCode()); user.setUAge(userDto.getAge()); user.setUAddress(userDto.getAddress()); int num = userMapper.insert(user); DynamicDatasourceHolder.removeDataSource("slave"); if (num > 0) { return true; } return false; }}
上面的service层方法在调用dao层方法的时候,使用DynamicDatasourceHolder.setDataSource()方法设置了需要使用的数据源, 通过这样的方式便可以实现动态数据源了。
不知道,小伙伴们有没有感觉到,这样每次在调用方法的时候都需要设置数据源是不是很麻烦,有没有一种更方面的方式,比如说注解。
四、动态数据源注解@DDS现在来实现一个动态数据源的注解来代替上面的每次都调用DynamicDatasourceHolder.setDataSource()方法来设置数据源。
先看下,@DDS注解的定义
DDS.java
package com.wcj.my.config.dynamic.source.aspect;import org.springframework.stereotype.Component;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/**动态数据源的注解 * 用在类和方法上,方法上的优先级大于类上的 * 默认值是master * @date 2023/6/9 16:19 */@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Componentpublic @interface DDS { String value() default "master";}
注解@DDS使用在类和方法上,切方法上的优先级大于类上的。有一个value的属性,指明使用的数据源,默认是”master“。
实现一个切面,来切@DDS注解
DynamicDatasourceAspect.java
package com.wcj.my.config.dynamic.source.aspect;import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.stereotype.Component;import java.util.Objects;/** * 动态数据源切面 * @date 2023/6/9 16:23 */@Aspect@Componentpublic class DynamicDatasourceAspect { /** * 切点,切的是带有@DDS的注解 */ @Pointcut("@annotation(com.wcj.my.config.dynamic.source.aspect.DDS)") public void dynamicDatasourcePointcut(){ } /** * 环绕通知 * @param joinPoint * @return * @throws Throwable */ @Around("dynamicDatasourcePointcut()") public Object around(ProceedingJoinPoint joinPoint)throws Throwable{ String datasourceKey="master"; //类上的注解 Class> targetClass=joinPoint.getTarget().getClass(); DDS annotation=targetClass.getAnnotation(DDS.class); //方法上的注解 MethodSignature methodSignature=(MethodSignature)joinPoint.getSignature(); DDS annotationMethod=methodSignature.getMethod().getAnnotation(DDS.class); if(Objects.nonNull(annotationMethod)){ datasourceKey=annotationMethod.value(); }else{ datasourceKey=annotation.value(); } //设置数据源 DynamicDatasourceHolder.setDataSource(datasourceKey); try{ return joinPoint.proceed(); }finally { DynamicDatasourceHolder.removeDataSource(datasourceKey); } }}
这样一个动态数据源的注解便可以了,看下怎么使用,
UserServiceByAnnotation.java
package com.wcj.my.service;import com.wcj.my.config.dynamic.source.DynamicDatasourceHolder;import com.wcj.my.config.dynamic.source.aspect.DDS;import com.wcj.my.dto.UserDto;import com.wcj.my.entity.User;import com.wcj.my.mapper.UserMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/** * @date 2023/6/8 15:19 */@Servicepublic class UserServiceByAnnotation { @Autowired private UserMapper userMapper; @DDS("master") public boolean saveUser(UserDto userDto){ User user=new User(); user.setUName(userDto.getName()); user.setUCode(userDto.getCode()); user.setUAge(userDto.getAge()); user.setUAddress(userDto.getAddress()); int num=userMapper.insert(user); if(num>0){ return true; } return false; } @DDS("slave") public boolean saveUserSlave(UserDto userDto){ User user=new User(); user.setUName(userDto.getName()); user.setUCode(userDto.getCode()); user.setUAge(userDto.getAge()); user.setUAddress(userDto.getAddress()); int num=userMapper.insert(user); if(num>0){ return true; } return false; }}
使用起来很简单,在需要切换数据源的方法或类上使用@DDS注解即可,使用value来改变数据源就好了。
五、动态数据源的原理很多小伙伴可能和我有一样的疑惑,使用DynamicDatasourceHolder.setDataSource或@DDS就可以设置数据源了,是怎么实现的,下面分析下,我们指定dao层的Mapper其实是一个代理对象,其会使用mybatis中的sqlSessionTempalte进行数据库的操作,在sqlSessionTemplate中会使用DefaultSqlSession对象,最终会使用DataSource,而使用了动态数据源的对象中会注入一个DynamicDataSource,在进行数据库操作时最终会获得一个数据库连接,这里便会使用DynamicDataSource获得一个连接,由于它继承了AbstractRoutingDataSource类,看下其getConnection方法,
@Overridepublic Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}
看下determineTargetDataSource()方法,
protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");、 //自己实现的,在调用方法时进行了设置,实现动态数据源的目的 Object lookupKey = determineCurrentLookupKey();DataSource dataSource = this.resolvedDataSources.get(lookupKey);if (dataSource == null && (this.lenientFallback || lookupKey == null)) {dataSource = this.resolvedDefaultDataSource;}if (dataSource == null) {throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");}return dataSource;}
看上面的注释,determineCurrentLookupkey()方法便是在DynamicDatasource类中进行了实现,从而实现了动态设置数据源的目的。
六、总结本文动手实现了一个动态数据源,并切提供了注解的方式,主要有以下几点
1、继承AbstractRoutingDataSource类的determineCurrentLookupkey()方法,动态设置数据源;
2、取消DataSourceAutoConfiguration的自动配置,手动向spring容器中注入多个数据源;
3、基于@DDS注解动态设置数据源;
最后,本文用到的源码均可关注下方公众号获得。另外,关注公众号回复”45“可获得一份极客时间的”mysql实战45讲“,很干的干货!
关键词:
凡注有"实况网-重新发现生活"或电头为"实况网-重新发现生活"的稿件,均为实况网-重新发现生活独家版权所有,未经许可不得转载或镜像;授权转载必须注明来源为"实况网-重新发现生活",并保留"实况网-重新发现生活"的电头。
热点
- 当前滚动:周到晨报 | 周三起连续4天日冲击高温线;移动咖啡车成精彩“出场方式”,有人买车“出摊”;这部关于老年人情感世界的沪语电影,来听背后故事→
- 天天新资讯:谁伴我闯荡歌词解析_谁伴我闯荡歌词
- 上影节|黄轩:这次演一个“黑化的犯罪艺术家”
- 瑞士百达集团百达朗:坚守长期主义 始终与客户站在一起 环球最资讯
- 中国邮政邮件编号查询 中国邮政邮寄编号查询
- 海贼vs火影视频(海贼vs火影)
- 四川巴中南江土特产有什么
- 一山更比一山高!又一“传媒娱乐”黑马,20家机构抢筹百亿,或成妖王? 天天微速讯
- 本赛季欧冠创造机会次数榜:格拉利什35次高居榜首,德布劳内第三
- 雪落的声音陆虎_雪落的声音歌词 每日快播