SpringBoot实现自定义动态多数据源
最近有个项目,有两种数据库,默认数据库是mysql,另外一种是sqserver,但是开发的过程中,有个很糙蛋的需求,有一个设备的数据存在sqlserver,是会根据年份的增加,会更改存储的数据源,但是表名是一样的表名。例如,今年2025年,这个设备数据就会把数据存到名称为Water2025的sqlserver数据库的device表里,到了2026年,这个设备数据又会存到名称为Water2026的sqlserver数据库的device表里,传统的多数据源就要每到一年手动添加一个数据源,这个很麻烦,所以我要实现个自定义多数据源来处理这个需求。
我的想法是,通过自定义注解,加aop代理去修改当前执行线程,所持有的数据源,但是这个修改是在,线程执行到mapper层之前完成的,因为mybatis本身就走了代理,然后我们再去代理的话,拿的对象属性是空。
一、原理
我来简单的讲解下多数据源的实现原理,方便大家理解:
1、springboot中,有个spring-jdbc包是用来创建jdbc连接的,其中有个AbstractRoutingDataSource抽象类,这个类就是实现多数据源的关键核心。
2、其中对我们有利用价值的一个是,Map<Object, Object> targetDataSources键值对,这个键值对是用来存储数据源的,key是个当前数据源的唯一标识,也是方便springboot通过这个key,确定当前线程要查询的数据库。而Map<Object, DataSource> resolvedDataSources这个键值对呢,我们往①和②看,可以看到,Map<Object, DataSource> resolvedDataSources键值对,实际上就是Map<Object, Object> targetDataSources。这里提到Map<Object, DataSource> resolvedDataSources键值对,只是为了方便我们下一步讲解。
3、第二个对我们有利用价值的是,determineCurrentLookupKey()抽象方法(ps:见名知意,确定当前查找键)。首先我们再看determineTargetDataSource()方法(ps:见名知意,确定目标数据源),序号①从determineCurrentLookupKey()抽象方法中获取key,获取到key之后到,序号②从Map<Object, DataSource> resolvedDataSources键值对中,获取对应的数据源,返回的dateSource数据源就是,当前线程要查询的数据源。
4、后面就是springboot去执行了,不需要我们再去关心了,相信通过这个3步讲解,大家对原理已经有了大致的了解,现在我们要来一步步开始实现。
二、实现
执行流程:
1、在项目启动时,会执行③自定义多数据源配置,将默认数据源,也就是德鲁伊的数据源配置,和从②中获取targetDataSources键值对,注入到spring中,让spring管理。
2、我们定义了个@DataSource自定义注解,和aop切点,当一个请求过来时,会检测方法或者类上是否带有我们自定义@DataSource注解,有得话,会执行⑨中的around()方法,这个方法会解析前端传入得入参,看入参中是否包含startYearFlag值,如果没有,会有个兜底的默认的数据源。
3、而后执行②中的setDataSource()方法,修改当前线程的数据源。
①maven依赖和yml配置文件
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>2.7.18</spring-boot.version>
<fastjson2.version>2.0.43</fastjson2.version>
<mybatis-plus.version>3.5.4</mybatis-plus.version>
<hutool.version>5.8.22</hutool.version>
<druid.version>1.2.23</druid.version>
<sqlserver.version>8.2.2.jre8</sqlserver.version>
</properties>
<dependencies>
<!-- springBoot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springBoot test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- Apache Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- SQL Server驱动-->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>${sqlserver.version}</version>
</dependency>
<!-- Mysql驱动包 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!--引入druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- hutool 的依赖配置-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
</dependencies>
spring:
application:
# 应用名称
name: @artifactId@
profiles:
# 环境配置
active: @profiles.active@
output:
ansi:
enabled: ALWAYS
logging:
#log日志文件配置
file_basePath: ./logs
file_prefix: ${spring.application.name}
mybatis-plus:
mapper-locations: classpath:com/jyy/**/mapper/mapping/*Mapper.xml
global-config:
banner: false
server:
port: 28000
--- # mysql配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/Water2025?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
username: root
password: root
--- # 动态数据源配置
dynamic:
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
url: jdbc:sqlserver://127.0.0.1:1433;
username: sa
password: Bwcx66
urlSuffix: ''
②自定义多数据源类
package com.jyy.dynamicdatasource.config;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.druid.pool.DruidDataSource;
import com.jyy.dynamicdatasource.DynamicContextHolder;
import com.jyy.dynamicdatasource.DynamicProperties;
import com.jyy.dynamicdatasource.utils.DynamicDbUtil;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义多数据源
*
* @author jyy
* @data 2025-01-14
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 目标数据源
*/
public static Map<Object, Object> targetDataSources = new ConcurrentHashMap<>(10);
/**
* 确定当前数据源
*
* @return 当前数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicContextHolder.peek();
}
/**
* 设置数据源
*
* @param dbKey 数据源key
* @param dynamicProperties 数据源配置对象
*/
public static void setDataSource(String dbKey, DynamicProperties dynamicProperties) {
// 检查数据源是否存在
if (!DynamicDataSource.targetDataSources.containsKey(dbKey)) {
// 获取新的数据源
DruidDataSource dataSource = DynamicDbUtil.getDbSourceByDbKey(dbKey, dynamicProperties);
// 添加到targetDataSources中
DynamicDataSource.targetDataSources.put(dbKey, dataSource);
}
// 将数据源放key入ThreadLocal中,
DynamicContextHolder.push(dbKey);
// 获取动态数据源Bean
DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringUtil.getBean("dynamicDataSource");
// 使得修改后的targetDataSources生效
dynamicDataSource.afterPropertiesSet();
}
}
③自定义多数据源配置
package com.jyy.dynamicdatasource.config;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import java.sql.SQLException;
/**
* 配置多数据源
*
* @author jyy
* @data 2025-01-14
*/
@Slf4j
@Configuration
public class DynamicDataSourceConfig {
/**
* 从配置文件中获取配置
*
* @return 数据源配置对象
*/
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
/**
* 默认数据源
*
* @param properties 数据源配置对象
* @return 数据源对象
*/
@Bean
public DruidDataSource defaultDataSource(DataSourceProperties properties) {
// 默认数据源,通过配置获取创建
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getDriverClassName());
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword());
try {
druidDataSource.init();
} catch (SQLException e) {
log.error(e.toString());
}
return druidDataSource;
}
/**
* 初始化数据源
*
* @param defaultDataSource 默认数据源
* @return 动态数据源
*/
@Bean
@Primary
@DependsOn({"defaultDataSource"})
public DynamicDataSource dynamicDataSource(DruidDataSource defaultDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 设置targetDataSources(通过数据库配置获取,首次创建没有数据源)
dynamicDataSource.setTargetDataSources(DynamicDataSource.targetDataSources);
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
log.info("------- 初始化数据源 -------");
return dynamicDataSource;
}
}
④多数据源上下文
package com.jyy.dynamicdatasource;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* 多数据源上下文
*
* @author jyy
* @data 2025-01-14
*/
public class DynamicContextHolder {
private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = ThreadLocal.withInitial(ArrayDeque::new);
/**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String peek() {
return CONTEXT_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
*
* @param dataSource 数据源名称
*/
public static void push(String dataSource) {
CONTEXT_HOLDER.get().push(dataSource);
}
/**
* 清空当前线程数据源
*/
public static void poll() {
Deque<String> deque = CONTEXT_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
CONTEXT_HOLDER.remove();
}
}
}
⑤自定义数据源工具类
package com.jyy.dynamicdatasource.utils;
import com.alibaba.druid.pool.DruidDataSource;
import com.jyy.dynamicdatasource.DynamicProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* 数据源工具类
*
* @author jyy
* @data 2025-01-14
*/
@Slf4j
public class DynamicDbUtil {
/**
* 获取数据源
*
* @param dbKey 数据源key
* @param dynamicProperties 数据源配置对象
* @return DruidDataSource
*/
public static DruidDataSource getDbSourceByDbKey(final String dbKey, DynamicProperties dynamicProperties) {
DruidDataSource dataSource = getJdbcDataSource(dbKey, dynamicProperties);
log.info("-------------------创建数据库[ {} ]DB连接-------------------", dbKey);
return dataSource;
}
/**
* 获取数据源【最底层方法,不要随便调用】
*
* @param dbKey 数据源key
* @param dynamicProperties 数据源配置对象
*/
private static DruidDataSource getJdbcDataSource(final String dbKey, DynamicProperties dynamicProperties) {
DruidDataSource dataSource = new DruidDataSource();
String driverClassName = dynamicProperties.getDriverClassMame();
String url = dynamicProperties.getJdbcUrl(dbKey);
String dbUser = dynamicProperties.getUsername();
String dbPassword = dynamicProperties.getPassword();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
//dataSource.setValidationQuery("SELECT 1 FROM DUAL");
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(false);
dataSource.setTestOnReturn(false);
dataSource.setBreakAfterAcquireFailure(true);
dataSource.setConnectionErrorRetryAttempts(0);
dataSource.setUsername(dbUser);
dataSource.setMaxWait(60000);
dataSource.setPassword(dbPassword);
log.info("******************************************");
log.info("* *");
log.info("*====[ {} ]=====Druid连接池已启用 ====*", driverClassName);
log.info("* *");
log.info("******************************************");
return dataSource;
}
/**
* 关闭数据库连接池
*
* @param dbKey
* @return
*/
public static void closeDbKey(final String dbKey, DynamicProperties dynamicProperties) {
DruidDataSource dataSource = getDbSourceByDbKey(dbKey, dynamicProperties);
try {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.getConnection().commit();
dataSource.getConnection().close();
dataSource.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private static JdbcTemplate getJdbcTemplate(String dbKey, DynamicProperties dynamicProperties) {
DruidDataSource dataSource = getDbSourceByDbKey(dbKey, dynamicProperties);
return new JdbcTemplate(dataSource);
}
/**
* 获取连接
*
* @param url
* @param username
* @param password
* @param driverName
* @return
*/
public static Connection getConn(String url, String username, String password, String driverName) {
Connection conn = null;
try {
Class.forName(driverName);
conn = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
throw new RuntimeException("无法连接,问题:" + e.getMessage(), e);
}
return conn;
}
/**
* 关闭数据库连接
*
* @param
*/
public static void closeConnection(Connection conn) {
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
throw new RuntimeException("close connection failure", e);
}
}
}
⑥数据源配置类
package com.jyy.dynamicdatasource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 数据源配置类
*
* @author jyy
* @date 2025-01-13
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "dynamic")
public class DynamicProperties {
private String url;
@Value("${dynamic.driver-class-name}")
private String driverClassMame;
private String username;
private String password;
private String urlSuffix;
/**
* 获取数据库连接地址
*
* @param databaseName 数据库名称
* @return 数据库连接地址
*/
public String getJdbcUrl(String databaseName) {
// 驱动区分
switch (driverClassMame) {
// sqlserver
case DynamicConstant.SQLSERVER_DRIVE: {
// jdbc:sqlserver://127.0.0.1:1433;databaseName=MyDatabase;
return url + "databaseName=" + databaseName + ";";
}
// mysql
case DynamicConstant.MYSQL_DRIVE: {
// jdbc:mysql://127.0.0.1:3306/jyy-test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
return url + databaseName + urlSuffix;
}
default:
log.error("数据源类型不存在");
return null;
}
}
}
⑦数据源常量
package com.jyy.dynamicdatasource;
/**
* 数据源常量
*
* @author jyy
* @date 2025-01-13
*/
public final class DynamicConstant {
private DynamicConstant() {
}
public static final String SQLSERVER_PREFIX = "Water";
/**
* sqlserver驱动
*/
public static final String SQLSERVER_DRIVE = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
/**
* mysql驱动
*/
public static final String MYSQL_DRIVE = "com.mysql.cj.jdbc.Driver";
}
⑧自定义注解
package com.jyy.dynamicdatasource.annotation;
import java.lang.annotation.*;
/**
* 多数据源注解
*
* @author jyy
* @data 2025-01-14
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
String value() default "";
}
⑨aop
package com.jyy.dynamicdatasource.aspect;
import com.jyy.dynamicdatasource.DynamicBaseQuery;
import com.jyy.dynamicdatasource.DynamicConstant;
import com.jyy.dynamicdatasource.DynamicContextHolder;
import com.jyy.dynamicdatasource.DynamicProperties;
import com.jyy.dynamicdatasource.annotation.DataSource;
import com.jyy.dynamicdatasource.config.DynamicDataSource;
import com.jyy.dynamicdatasource.utils.DateUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDate;
/**
* 多数据源,切面处理类
*
* @author jyy
* @data 2025-01-14
*/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
@RequiredArgsConstructor
public class DataSourceAspect {
private final DynamicProperties dynamicProperties;
private static final Pattern YEAR_PATTERN = Pattern.compile("^\\d{4}$");
/**
* 定义切点
* {@code @annotation(com.jyy.dynamicdatasource.annotation.DataSource)}: 如果方法上有com.jyy.dynamicdatasource.DataSource注解,那么这个方法就会被拦截。
* {@code @within(com.jyy.dynamicdatasource.annotation.DataSource}: 如果方法所在的类上有com.jyy.dynamicdatasource.DataSource注解,那么这个方法也会被拦截。
*/
@Pointcut("@annotation(com.jyy.dynamicdatasource.annotation.DataSource) || @within(com.jyy.dynamicdatasource.annotation.DataSource)")
public void dataSourcePointCut() {
}
/**
* 环绕通知
* {@code @Around("dataSourcePointCut()")}: 这个环绕通知将应用于定义在dataSourcePointCut()中的方法
*
* @param point ProceedingJoinPoint对象可以获取方法签名和目标类
* @return 方法
* @throws Throwable 抛出异常
*/
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 获取方法信息
MethodSignature signature = (MethodSignature) point.getSignature();
// 获取目标方法
Method method = signature.getMethod();
// 获取方法参数名称数组
String[] parameterNames = signature.getParameterNames();
// 获取方法参数值数组
Object[] args = point.getArgs();
// 用于存储startYearFlag参数的值
String startYearFlag = null;
// 遍历参数,找到名为startYearFlag的参数
for (int i = 0; i < parameterNames.length; i++) {
// 假设args[i]是DynamicBaseQuery类型,它继承自DynamicBaseQuery
if (args[i] instanceof DynamicBaseQuery) {
DynamicBaseQuery query = (DynamicBaseQuery) args[i];
// 直接获取startYearFlag属性
startYearFlag = query.getStartYearFlag();
break;
}
}
// 当前年份
String year = String.valueOf(LocalDate.now().getYear());
// 如果找到了startYearFlag参数,则使用它的值
String value;
if (isValidYear(startYearFlag)) {
value = startYearFlag;
} else {
// 如果没有找到,保持原来的逻辑
// 获取目标类上的注解
DataSource targetDataSource = (DataSource) point.getTarget().getClass().getAnnotation(DataSource.class);
// 获取方法上的注解
DataSource methodDataSource = method.getAnnotation(DataSource.class);
if (null != methodDataSource && StringUtils.isNotEmpty(methodDataSource.value())) {
value = methodDataSource.value();
} else if (null != targetDataSource && StringUtils.isNotEmpty(targetDataSource.value())) {
value = targetDataSource.value();
} else {
// 当前年份
value = year;
}
}
// 判断获取的年份是否大于当前年份
if (Integer.parseInt(value) > Integer.parseInt(year)) {
log.error("{}{} 数据源不存在,使用当前{}年份数据源", DynamicConstant.SQLSERVER_PREFIX, value, year);
value = year;
} else if (Integer.parseInt(value) < 2023) {
log.error("{}{} 数据源不存在,使用当前{}年份数据源", DynamicConstant.SQLSERVER_PREFIX, value, year);
value = year;
}
// 根据dbKey动态设置数据源
String dataBaseName = DynamicConstant.SQLSERVER_PREFIX + value;
DynamicDataSource.setDataSource(dataBaseName, dynamicProperties);
log.debug("设置数据源为: {}", dataBaseName);
try {
// 执行方法
return point.proceed();
} finally {
DynamicContextHolder.poll();
log.debug("clean datasource");
}
}
/**
* 校验字符串是否是年份
*
* @param year 年份字符串
* @return true 是年份,false 不是年份
*/
public static boolean isValidYear(String year) {
if (StringUtils.isEmpty(year)) {
return false;
}
return YEAR_PATTERN.matcher(year).matches();
}
}
⑩接收前端固定入参
package com.jyy.dynamicdatasource;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 动态数据源基类 query
*
* @author jyy
* @date 2025-01-15
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DynamicBaseQuery implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 开始年份标识
*/
private String startYearFlag;
/**
* 结束年份标识
*/
private String endYearFlag;
}
三、测试代码
我分别在sqlserver中,新建了Water2023、Water2025、Water2026三个数据源,每个数据源中都新建了,device表,device表中有id和name字段,name的值分别是2023、2025、2026。在mysql中新建了Water2025库,里面也是device表,name的值是mysql2025。
①controller
package com.jyy.demo.controller;
import com.jyy.demo.serivce.DemoService;
import com.jyy.utils.dynamicdatasource.DynamicBaseQuery;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* demo
*
* @author jyy
* @date 2025-02-05
*/
@Slf4j
@RestController
@RequestMapping("/demo")
@RequiredArgsConstructor
public class DemoController {
private final DemoService demoService;
/**
* 测试多数据源
*
* @param query 入参
* @return String
*/
@GetMapping("/testDynamic")
public String testDynamic(DynamicBaseQuery query) {
// 测试多数据源
String name = demoService.testDynamic(query);
log.info("name:{}", name);
return name;
}
}
②service
package com.jyy.demo.serivce;
import com.jyy.utils.dynamicdatasource.DynamicBaseQuery;
/**
* demo
*
* @author jyy
* @date 2025-02-05
*/
public interface DemoService {
/**
* 测试多数据源
*
* @param query 入参
* @return String
*/
String testDynamic(DynamicBaseQuery query);
}
③serviceImpl
package com.jyy.demo.serivce.impl;
import com.jyy.demo.mapper.DemoMapper;
import com.jyy.demo.serivce.DemoService;
import com.jyy.utils.dynamicdatasource.DynamicBaseQuery;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* demo
*
* @author jyy
* @date 2025-02-05
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DemoServiceImpl implements DemoService {
private final DemoMapper demoMapper;
/**
* 测试多数据源
*
* @param query 入参
* @return String
*/
@Override
public String testDynamic(DynamicBaseQuery query) {
// 测试多数据源
return demoMapper.testDynamic();
}
}
④mapper
package com.jyy.demo.mapper;
import org.apache.ibatis.annotations.Mapper;
/**
* demo
*
* @author jyy
* @date 2025-02-05
*/
@Mapper
public interface DemoMapper {
/**
* 测试多数据源
*
* @return String
*/
String testDynamic();
}
⑤mapper.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.jyy.demo.mapper.DemoMapper">
<select id="testDynamic" resultType="java.lang.String">
select name from device
</select>
</mapper>
四、测试结果
注意:自定义注解可以加在controller、serivce、serviceImpl的类上或者方法上,但是不能加在mapper层,因为mybatis本来就走了代理,我们再去代理的话,代理的对象属性是空的。
项目启动成功,并且多数据源也初始化成功了
①没加自定义注解,查询默认mysql数据源
能查询查出,证明默认数据源没问题。
②加自定义注解,默认查询sqlserver,今年2025年数据。
③加自定义注解,加入参,查询sqlserver,2023年历史数据。
④加自定义注解,加入参,查询2026数据,因为项目需求,我这里做了校验,大于本年度和小于2023年的,在项目中都是没有真实数据源的,没有兜底会报错。这里2026年还没到,所以没有这个数据源,兜底
五、总结
我们处理动态多数据源的核心,不管是兜底还是数据源逻辑处理,其实还是在⑨aop中,这个是我在项目开发过程中遇到的需求,大家可以借鉴下,其实还可以用redis来缓存,增加性能,但是我这个项目查询的逻辑很单一,没必要用到redis。
修改数据源的逻辑在,第二节的序号⑨aop中的,环绕通知方法,因为我这个项目的修改数据源的需求比较简单,也没有使用到自定义注解的value值,但是传value也是可以被接收的,大家可以根据自身的需求,修改aop中的逻辑
写的很粗略,大家将就着看看吧。哈哈。
六、源码地址
gitee: https://gitee.com/jiyaya/jyy-dynamic.git