整体思路:
- 引入基本依赖SpringBoot+Aop+MySql+MyBatis+lombok
- 在配置文件中配置多个数据源
- 创建数据源配置类用于读取配置
- 编写用于标识切换数据源的注解
- 创建数据源切换工具类DataSourceContextHolder
- 编写切面类用于在注解生效处切换数据源
- 编写配置类,加载数据源
- 创建动态数据源类,并继承AbstractRoutingDataSource,指定使用哪个数据源(关键)
项目demo gitee地址:多数据源动态切换demo
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
2.在配置文件中配置多个数据源
这里配置了上海,深圳,北京3个数据源,需要自己创建这3个库multi-sh,multi-sz,multi-bj
#默认数据源
datasource.default=sh
#上海库
spring.datasource.sh.url=jdbc:mysql://localhost:3306/multi-sh?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.sh.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.sh.username=root
spring.datasource.sh.password=123
#深圳库
spring.datasource.sz.url=jdbc:mysql://localhost:3306/multi-sz?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.sz.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.sz.username=root
spring.datasource.sz.password=123
#北京库
spring.datasource.bj.url=jdbc:mysql://localhost:3306/multi-bj?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.bj.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.bj.username=root
spring.datasource.bj.password=123
3.创建数据源配置类用于读取配置
spring获取统一前缀配置需要可以看我之前的文章:SpringBoot项目获取统一前缀配置以及获取非确定名称配置
package com.gooluke.datasource;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author gooluke
*/
@Setter
@Getter
@Component
@ConfigurationProperties(prefix = "spring")
public class MultiDataSourceProperties {
/**
* 这里的datasource是因为配置是spring.datasource.xx.xx,要配置成datasource,这样才会把配置自动映射进来
* 分别映射到url、driverClassName、username、password
*/
private Map<String, DataSourceConfig> datasource;
@Setter
@Getter
public static class DataSourceConfig {
private String url;
private String driverClassName;
private String username;
private String password;
}
}
4.编写用于标识切换数据源的注解
package com.gooluke.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author gooluke
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FixedDataSource {
String value();
/**
* 是否需要还原回之前的数据源(拓展)
*/
boolean needRecover() default false;
}
5.创建数据源切换工具类DataSourceContextHolder
package com.gooluke.datasource;
import com.gooluke.config.DataSourceConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author gooluke
* 将数据源信息存放至ThreadLocal
*/
public class DatasourceContextHolder {
private static final Logger log = LoggerFactory.getLogger(DatasourceContextHolder.class);
private static final ThreadLocal<String> DATASOURCE_THREAD_LOCAL = new ThreadLocal<>();
public static void setDatasource(String datasource) {
if (datasource != null && DataSourceConfig.dataSources.get(datasource) == null) {
String errorMsg = String.format("数据源[%s]未配置", datasource);
log.error(errorMsg);
throw new RuntimeException(errorMsg);
}
DATASOURCE_THREAD_LOCAL.set(datasource);
}
public static String getDatasource() {
return DATASOURCE_THREAD_LOCAL.get();
}
public static void clearDatasource() {
DATASOURCE_THREAD_LOCAL.remove();
}
}
6.编写切面类用于在注解生效处切换数据源
package com.gooluke.aspect;
import com.gooluke.common.annotation.FixedDataSource;
import com.gooluke.datasource.DatasourceContextHolder;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* @author gooluke
* 切换数据源切面类
* 这个已不再使用,使用com.gooluke.aop.DataSourceAnnotationAdvisor替代
*/
@Aspect
@Component
public class DataSourceAspect {
private static final Logger log = LoggerFactory.getLogger(DataSourceAspect.class);
/**
* 注解加在方法上
*/
@Pointcut("@annotation(com.gooluke.common.annotation.FixedDataSource)")
private void methodPointCut() {}
/**
* 注解加在方法上
*/
@Pointcut("@within(com.gooluke.common.annotation.FixedDataSource)")
public void classPointcut() {}
@Around(value = "methodPointCut() || classPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//记录当前数据源,和准备切换的数据源
String oldDatasource = DatasourceContextHolder.getDatasource();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
java.lang.reflect.Method method = methodSignature.getMethod();
FixedDataSource annotation = method.getAnnotation(FixedDataSource.class);
//方法上获取注解为空,再从类上获取
if (annotation == null) {
annotation = method.getDeclaringClass().getAnnotation(FixedDataSource.class);
}
String newDatasource = annotation.value();
//切换数据源,并执行操作
DatasourceContextHolder.setDatasource(newDatasource);
try {
return joinPoint.proceed();
} finally {
//是否切换回初始数据源
if (annotation.needRecover()) {
DatasourceContextHolder.setDatasource(oldDatasource);
}
}
}
}
7.编写配置类,加载数据源
这个配置类,主要就是将我们配置的多数据源解析然后统一管理,dynamicDataSource.setTargetDataSources(targetDataSources); 以及设置默认数据源。
package com.gooluke.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.gooluke.aop.DataSourceAnnotationAdvisor;
import com.gooluke.aop.DataSourceAnnotationInterceptor;
import com.gooluke.datasource.DynamicDataSource;
import com.gooluke.datasource.MultiDataSourceProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author gooluke
*/
@Configuration
@Slf4j
public class DataSourceConfig {
public static final Map<String, String> dataSources = new HashMap<>();
@Autowired
MultiDataSourceProperties dataSourceProperties;
@Value("${datasource.default:}")
private String defaultDataSourceName;
@Bean
@Primary
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//存放所有数据源
Map<Object, Object> targetDataSources = new HashMap<>();
Map<String, MultiDataSourceProperties.DataSourceConfig> datasourceMap = dataSourceProperties.getDatasource();
if (datasourceMap.entrySet().size() > 1 && (defaultDataSourceName == null || defaultDataSourceName.isEmpty())) {
throw new RuntimeException("存在多个数据源,未配置默认数据源:datasource.default");
}
datasourceMap.forEach((datasourceName, config) -> {
DataSource dataSource = createDataSource(config);
targetDataSources.put(datasourceName, dataSource);
dataSources.put(datasourceName, datasourceName);
log.info("已初始化数据库:{}", datasourceName);
if (datasourceMap.size() == 1 || (defaultDataSourceName != null && !defaultDataSourceName.isEmpty() && defaultDataSourceName.equals(datasourceName))) {
//这里设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(dataSource);
log.info("已设置默认数据源: {}", datasourceName);
}
});
//这里把数据源统一管理
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
private DataSource createDataSource(MultiDataSourceProperties.DataSourceConfig dataSourceConfig) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(dataSourceConfig.getUrl());
dataSource.setDriverClassName(dataSourceConfig.getDriverClassName());
dataSource.setUsername(dataSourceConfig.getUsername());
dataSource.setPassword(dataSourceConfig.getPassword());
dataSource.setValidationQuery("SELECT 1");
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(false);
dataSource.setTestOnReturn(false);
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dynamicDataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
org.springframework.core.io.Resource[] resources = resolver.getResources("classpath:/mapper/*.xml");
//org.springframework.core.io.Resource config = resolver.getResource("classpath:mybatis-config.xml");
sessionFactory.setMapperLocations(resources);
//sessionFactory.setConfigLocation(config);
return sessionFactory.getObject();
}
@Bean
@Primary
public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
8.创建动态数据源类,并继承AbstractRoutingDataSource,指定使用哪个数据源(关键)
这里可以理解为就是一个口子,让我们自己指定数据源,如果你返回的是null,则会指定我们配置类中设置的默认数据源:dynamicDataSource.setDefaultTargetDataSource(dataSource);
package com.gooluke.datasource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author gooluke
* 动态数据源
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 这里返回ThreadLocal中的数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return DatasourceContextHolder.getDatasource();
}
}
9.请求完成后,记得清空ThreadLocal,否则会造成内存泄漏
编写一个拦截器,在请求完成后,remove
package com.gooluke.interceptor;
import com.gooluke.datasource.DatasourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author gooluke
*/
@Component
@Slf4j
public class DataSourceInterceptor implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
DatasourceContextHolder.clearDatasource();
log.info("请求处理完成,清除数据源");
}
}
10.代码演示
将注解加在实现类方法上,或者加在mapper/dao接口上(一般加在这里,因为dao接口一般都是操作同一个库,这里指定了,其它别的方法直接调用即可)
10.1 service层:
package com.gooluke.service.impl;
import com.gooluke.dao.UserInfoDao;
import com.gooluke.dao.UserInfoDao2;
import com.gooluke.entity.TUserInfo;
import com.gooluke.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author gooluke
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserInfoDao userInfoDao;
@Autowired
private UserInfoDao2 userInfoDao2;
/**
* 在这里没有设置数据源,dao层设置了数据源,可以自动切换
*/
@Override
public List<TUserInfo> selectList() {
//先查深圳库,再查上海库
List<TUserInfo> tUserInfos = userInfoDao.selectUserList(new TUserInfo());
tUserInfos.forEach(System.out::println);
List<TUserInfo> tUserInfos2 = userInfoDao2.selectUserList(new TUserInfo());
tUserInfos2.forEach(System.out::println);
tUserInfos.addAll(tUserInfos2);
return tUserInfos;
}
}
10.2 dao层:
dao1指定深圳库:
package com.gooluke.dao;
import com.gooluke.common.annotation.FixedDataSource;
import com.gooluke.common.constants.DataSourceName;
import com.gooluke.entity.TUserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @author gooluke
*/
@Mapper
@FixedDataSource(DataSourceName.SHENZHEN)
public interface UserInfoDao {
List<TUserInfo> selectUserList(TUserInfo userInfo);
}
dao2指定上海库:
package com.gooluke.dao;
import com.gooluke.common.annotation.FixedDataSource;
import com.gooluke.common.constants.DataSourceName;
import com.gooluke.entity.TUserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @author gooluke
*/
@Mapper
@FixedDataSource(DataSourceName.SHANGHAI)
public interface UserInfoDao2 {
List<TUserInfo> selectUserList(TUserInfo userInfo);
}
10.3 观察结果
切库成功,分别查询了不同库的数据,并在最后清空了ThreadLocal中的数据
11.动态数据源(开源)dynamic-datasource-spring-boot-starter
上面这种Aop的实现方式在注解加在service接口的方法上其实是不生效的,当然也不建议加在service接口上,通常是加在实现类类上或者方法上。而Mapper/Dao接口的实现类是通过mybatis动态代理生成的,注解加在Mapper/Dao接口上是能生效的,我没有找到为啥他的实现类可以的文章。而我们也可以通过别的方式,把注解加在接口上的场景通过Aop拦截,只是不建议。下面是开源组件-动态数据源
'com.baomidou:dynamic-datasource-spring-boot-starter:3.3.2'
的Aop方案,有兴趣的可以去看一下他的源码,我的工程里也是用的这种方案,需要在配置类中声明@bean
11.1 创建一个DataSourceAnnotationAdvisor去继承AbstractPointcutAdvisor类,并实现BeanFactoryAware接口
11.2 重写getPointcut()、getAdvice()、setBeanFactory()方法
11.3 配置声明@Bean
@Bean
public DataSourceAnnotationAdvisor dataSourceAnnotationAdvisor() {
DataSourceAnnotationInterceptor dataSourceAnnotationInterceptor = new DataSourceAnnotationInterceptor();
return new DataSourceAnnotationAdvisor(dataSourceAnnotationInterceptor);
}