动态数据源

动态数据源

SpringBoot 配置多数据源并动态切换

Spring Boot 中的多数据源配置方案

SpringBoot 多数据源配置/连接两个数据库

介绍

在实际开发中,往往会出现一个服务连接多个数据库的需求,这时候就需要在项目中进行灵活切换数据源来完成多个数据库操作。多数据源可以理解为多数据库,甚至可以是多个不同类型的数据库,比如一个是MySql,一个是Oracle。随着项目的扩大,有时需要数据库的拆分或者引入另一个数据库,这时就需要配置多个数据源。

实现思路

DataSource是和线程绑定的,动态数据源的配置主要是通过继承AbstractRoutingDataSource类实现的,实现在AbstractRoutingDataSource类中的 protected Object determineCurrentLookupKey()方法来获取数据源,所以我们需要先创建一个多线程 线程数据隔离的类来存放DataSource,然后在determineCurrentLookupKey()方法中通过这个类获取当前线程的DataSource,在AbstractRoutingDataSource类中,DataSource是通过Key-value的方式保存的,我们可以通过ThreadLocal来保存Key,从而实现数据源的动态切换。

引入mysql和jdbcTemplate依赖

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>

数据源配置

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.db2.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db2.url=jdbc:mysql://localhost:3306/db2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.db2.username=root
spring.datasource.db2.password=root
mybatis.type-aliases-package=com.example.demo02.pojo.domain
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.configuration.map-underscore-to-camel-case=true

创建数据源枚举类

@Getter
public enum DataSourceEnum {
DEFAULT_DATASOURCE("defaultDataSource"),
DB2_DATASOURCE("db2DataSource");
String dataSourName;
DataSourceEnum(String dataSourName) {
this.dataSourName = dataSourName;
}
}

数据源切换处理

public class DynamicDataSourceContextHolder {
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源
*
* @param dataSourceType
*/
public static void setDataSourceType(String dataSourceType) {
System.out.printf("切换到{%s}数据源", dataSourceType);
CONTEXT_HOLDER.set(dataSourceType);
}
/**
* 获取数据源变量
*
* @return
*/
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}

继承AbstractRoutingDataSource

/**
* 类描述:动态切换数据源主要依靠AbstractRoutingDataSource。
* 创建一个AbstractRoutingDataSource的子类,重写determineCurrentLookupKey方法,用于决定使用哪一个数据源。
* 这里主要用到AbstractRoutingDataSource的两个属性defaultTargetDataSource和targetDataSources。
* defaultTargetDataSource默认目标数据源,targetDataSources(map类型)存放用来切换的数据源。
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
// afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
super.afterPropertiesSet();
}
/**
* 根据Key获取数据源的信息
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}

注入数据源

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
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;
/**
* 多数据源配置-注入数据源
*/
@Configuration
public class DataSourceConfig {
/**
* 默认数据源
*
* @return
*/
@Bean("defaultDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource defaultDataSource() {
return DataSourceBuilder.create().build();
}
/**
* db2数据库数据源
*
* @return
*/
@Bean("db2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource db2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dataSource")
@Primary
public DynamicDataSource dataSource(@Qualifier("defaultDataSource") DataSource defaultDataSource,
@Qualifier("db2DataSource") DataSource db2DataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceEnum.DEFAULT_DATASOURCE.getDataSourName(), defaultDataSource);
targetDataSources.put(DataSourceEnum.DB2_DATASOURCE.getDataSourName(), db2DataSource);
return new DynamicDataSource(defaultDataSource, targetDataSources);
}
// TODO 测试数据源是否可用,其实就是使用当前数据源建立下连接,如果可以建立连接说明可用
// TODO 配置数据源的其他连接池配置
}

自定义多数据源切换注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DataSourceEnum value() default DataSourceEnum.DEFAULT_DATASOURCE;
}

AOP拦截类的实现

通过AOP在执行sql语句前拦截,并切换到自定义注解指定的数据源上。有一点需要注意,自定义数据源注解与 @Transaction 注解同一个方法时会先执行 @Transaction ,即获取数据源在切换数据源之前,所以会导致自定义注解失效,因此需要使用 @Order (@Order的value越小,就越先执行),保证该AOP在 @Transactional 之前执行。

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.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 类描述:通过拦截自定义的@DataSource注解,在其执行之前处理设置当前执行SQL的数据源的信息
* CONTEXT_HOLDER.set(dataSourceType)这里的数据源信息从我们设置的注解上面获取信息,如果没有设置就是用默认的数据源的信息。
*
*/
@Aspect
@Component
@Order(-1)
public class DataSourceAspect {
/**
* 作用Mapper接口
*/
@Pointcut("execution(* com.example.demo02.mapper..*.*(..))")
public void dsPointCut() {
}
@Before("dsPointCut()")
public void changeDataSource(JoinPoint joinPoint) {
boolean datasource = false;
DataSource targetDataSource = null;
for (Class clazz : joinPoint.getTarget().getClass().getInterfaces()) {
datasource = clazz.isAnnotationPresent(DataSource.class);
if (datasource) {
targetDataSource = (DataSource) clazz.getAnnotation(DataSource.class);
break;
}
}
if (datasource) {
String dataSource = targetDataSource.value().getDataSourName();
// 数据源不存在则抛出异常
if (!DynamicDataSourceContextHolder.containsDataSource(dataSource)) {
log.error("datasource[{}] does not exist, will use default data source > {}"
, targetDataSource.value(), joinPoint.getSignature());
} else {
log.debug("Use DataSource: {} > {}", dataSource, joinPoint.getSignature());
DynamicDataSourceContextHolder.setDataSourceType(dataSource);
}
} else {
// 使用默认数据源
DynamicDataSourceContextHolder.setDataSourceType(DataSourceEnum.DEFAULT_DATASOURCE.getDataSourName());
}
}
@After("@annotation(targetDataSource)")
public void restoreDataSource(JoinPoint joinPoint, DataSource targetDataSource) {
log.debug("Revert DataSource: {} > {}", targetDataSource.value(), joinPoint.getSignature());
DynamicDataSourceContextHolder.clearDataSourceType();
}
}

启动类

需要在启动类的 @SpringBootApplication 注解中移除DataSource自动配置类,否则会默认自动配置,而不会使用我们自定义的DataSource,并且启动会有循环依赖的错误。

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@MapperScan(basePackages = "com.example.demo02.mapper")
public class Demo02Application {
public static void main(String[] args) {
SpringApplication.run(Demo02Application.class, args);
}
}

mybatis配置

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
/**
* Mybatis配置
*/
@Component
@DependsOn("dataSource")
@MapperScan(basePackages = {"com.example.demo02.mapper"}, sqlSessionFactoryRef = "sqlSessionFactory")
public class MybatisConfig {
@Value("${mybatis.mapper-locations}")
private String mapperLocation;
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources(mapperLocation));
return sqlSessionFactoryBean.getObject();
}
@Bean("sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean("transactionManager")
public DataSourceTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

🌰测试数据源切换

  • mapper
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
* @author leizi
* @create 2022-07-17 19:27
*/
@Repository
public interface StudentMapper {
/**
* 根据id获取名字
*
* @param id
* @return
*/
String getNameById(@Param("id") int id);
}
  • service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author leizi
* @create 2022-07-17 19:26
*/
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentMapper studentMapper;
@Autowired
private StudentDB2Mapper studentDB2Mapper;
@Override
public String getNameById(int id) {
return studentMapper.getNameById(id);
}
@Override
public String getUserNameByIdDB2(int id) {
return studentDB2Mapper.getUserNameByIdDB2(id);
}
}
  • controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author leizi
* @create 2022-07-17 19:31
*/
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping("/get/{id}")
public RestResult getNameById(@PathVariable("id") int id) {
String name = studentService.getNameById(id);
return RestResult.successResult(name);
}
@GetMapping("/get-db2/{id}")
public RestResult getUserNameByIdDB2(@PathVariable("id") int id) {
String name = studentService.getUserNameByIdDB2(id);
return RestResult.successResult(name);
}
}

遇到问题

Springboot报错:Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required

配置数据源多配置@Primary导致找不到主数据源而不能创建mybatis相关bean

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'studentMapper' defined in file [F:\code\demo02\target\classes\com\example\demo02\mapper\StudentMapper.class]: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required

posted @   Lz_蚂蚱  阅读(197)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起