SpringBoot 中 AOP 实现多数据源切换

前面我们用 AOP 实现了操作日志、接口权限校验、接口限流,核心都是「请求增强」场景,不侵入业务代码、优雅解耦。今天我们进入 AOP 另一大经典实战场景------利用 AOP 实现动态多数据源切换,真正做到「业务代码零侵入、注解一键切换主从库/多业务库」。

做过企业级项目的同学都清楚,单数据源根本满足不了中大型项目的需求:随着业务增长,数据量激增,单库的读写压力会越来越大;多业务模块(用户、订单、商品)共用一个数据库,不仅耦合度高,还会出现锁竞争、性能瓶颈;多租户场景下,不同租户的数据需要隔离存储,避免数据泄露。

如果手动在 Service 层来回切换数据源(比如写代码手动切换 Connection),不仅代码冗余、难以维护,还容易出现线程安全问题,一旦切换逻辑出错,就会导致数据查询/写入错误,引发生产事故。

而用「AOP + 自定义注解 + ThreadLocal + AbstractRoutingDataSource」的组合,能完美解决这些问题:只需在方法或类上添加一行注解,就能自动切换到指定数据源,全程不侵入业务代码,切换逻辑统一管理,扩展性极强。

一、核心适用场景

动态多数据源切换不是炫技,而是企业项目的刚需,以下是最常见的4种场景,本篇实战将逐一适配,让你一次学会,终身可用:

    1. 读写分离:主库(master)负责写入操作(新增、修改、删除),从库(slave)负责查询操作,分散数据库读写压力,提升系统性能。比如电商项目中,下单、支付走主库,商品列表查询、订单历史查询走从库。
    1. 多业务库隔离:大型项目中,将不同业务模块的数据库分离,比如用户库(db_user)、订单库(db_order)、商品库(db_goods),降低模块间耦合,避免单库故障影响全系统,同时便于单独维护和扩容。
    1. 多租户架构:SaaS 系统中,不同租户的数据存储在不同的数据库(或不同 Schema),通过租户ID动态切换数据源,实现数据隔离,保障租户数据安全,比如企业管理系统、CRM系统。
    1. 历史库/实时库分离:核心业务走实时库(存储近期数据,性能优先),历史数据归档到历史库(存储远期数据,容量优先),查询历史数据时切换到历史库,避免历史数据查询影响实时业务性能。

补充说明:本篇实战以「读写分离(1主2从)」为基础,同时提供多业务库、多租户的扩展方案,代码可灵活适配不同场景,无需大量修改。

二、整体架构思路

动态数据源切换的核心是「路由」------根据注解标记,将当前请求路由到指定的数据源。整体架构基于 Spring 提供的 AbstractRoutingDataSource 类,结合 AOP 和 ThreadLocal 实现,步骤清晰、逻辑连贯,具体流程如下:

    1. 配置多数据源:在 application.yml 中配置多个数据源(主库、从库、业务库等),指定每个数据源的 URL、用户名、密码、驱动类。
    1. 实现动态数据源路由:继承 Spring 提供的 AbstractRoutingDataSource 类,重写 determineCurrentLookupKey 方法,该方法的返回值就是当前要使用的数据源标识(如 master、slave1)。
    1. 线程安全存储数据源标识:用 ThreadLocal 保存当前线程要使用的数据源标识,避免多线程环境下数据源错乱(ThreadLocal 是线程隔离的,每个线程有独立的存储空间)。
    1. 自定义切换注解:创建 @DS 注解,用于标记类或方法需要使用的数据源,注解值为数据源标识(如 @DS("slave1"))。
    1. AOP 切面拦截处理:创建 AOP 切面,拦截所有添加了 @DS 注解的类或方法,在方法执行前,从注解中获取数据源标识,存入 ThreadLocal;方法执行完毕后,清空 ThreadLocal,避免线程复用导致的数据源污染。
    1. 配置数据源Bean:将所有数据源注入 Spring 容器,通过 DynamicDataSource 整合所有数据源,设置默认数据源(如主库),并将其作为 Spring 的主数据源。
    1. 测试与优化:覆盖读写分离、多库切换等场景,测试数据源切换是否正常,同时处理事务兼容、线程安全等问题,优化切换性能。

核心原理:AbstractRoutingDataSource 会在获取数据库连接时,调用 determineCurrentLookupKey 方法获取当前数据源标识,然后从配置的数据源集合中找到对应的数据源,实现动态路由。

三、完整代码

本次实战基于 SpringBoot 2.7.x 版本,使用 MySQL 数据库、HikariCP 连接池(性能最优的连接池之一),全程无复杂依赖,所有代码都经过企业项目验证,可直接复制到项目中,只需修改数据源配置和包路径,就能快速落地。

步骤1:导入核心依赖(pom.xml)

需要导入 SpringBoot 核心依赖、AOP 依赖、JDBC 依赖、MySQL 驱动、连接池依赖,无需额外导入其他包,pom.xml 如下:

go 复制代码
<!-- SpringBoot 核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- JDBC 依赖(操作数据库必备) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- AOP 依赖(核心,用于拦截注解,实现数据源切换) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- MySQL 驱动(适配 MySQL 8.0+) -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- HikariCP 连接池(性能最优,SpringBoot 默认连接池) -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
</dependency>

<!-- Lombok(简化代码,可选,推荐) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!-- 测试依赖(用于测试数据源切换效果) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

说明:如果项目中使用 MyBatis/MyBatis-Plus,只需额外导入对应的依赖,数据源切换逻辑完全不变,本篇实战兼容 MyBatis/MyBatis-Plus 场景。

步骤2:application.yml 多数据源配置

配置3个数据源(1主2从),指定每个数据源的连接信息、连接池参数,同时配置默认数据源和 AOP 切面相关参数,application.yml 如下:

go 复制代码
server:
  port: 8080 # 服务器端口

spring:
  datasource:
    # 连接池全局配置(所有数据源共用)
    hikari:
      maximum-pool-size: 10 # 最大连接数
      minimum-idle: 5 # 最小空闲连接
      idle-timeout: 300000 # 空闲连接超时时间(5分钟)
      connection-timeout: 30000 # 连接超时时间(30秒)
      connection-test-query: SELECT 1 # 连接测试语句(避免连接失效)
    # 主库(master,负责写入操作)
    master:
      jdbc-url: jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
      username: root # 数据库用户名
      password: root # 数据库密码
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 从库1(slave1,负责查询操作)
    slave1:
      jdbc-url: jdbc:mysql://localhost:3306/db_slave1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 从库2(slave2,负责查询操作,实现负载均衡)
    slave2:
      jdbc-url: jdbc:mysql://localhost:3306/db_slave2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

# 自定义数据源配置(可选,用于扩展)
dynamic:
  datasource:
    default: master # 默认数据源
    slave-list: slave1,slave2 # 从库列表(用于负载均衡)

注意事项:1. 数据源 URL 必须用 jdbc-url(SpringBoot 2.x+ 多数据源配置要求),不能用 url,否则会报错;2. 确保每个数据库(db_master、db_slave1、db_slave2)已创建,且表结构一致(读写分离场景);3. 连接池参数可根据项目实际压力调整,避免连接数过多导致数据库负载过高。

步骤3:核心工具类

这部分是动态数据源切换的核心,包含两个工具类:DataSourceContextHolder(用 ThreadLocal 保存数据源标识)和 DynamicDataSource(实现数据源路由),代码注释详细,可直接复用。

3.1 数据源上下文(DataSourceContextHolder)

用 ThreadLocal 保存当前线程的数据源标识,确保多线程环境下数据源不错乱,同时提供设置、获取、清空数据源标识的方法,必须在方法执行完毕后清空,避免线程复用污染。

go 复制代码
import lombok.extern.slf4j.Slf4j;

/**
 * 数据源上下文(ThreadLocal 保存当前线程的数据源标识)
 * 线程安全:ThreadLocal 是线程隔离的,每个线程有独立的存储空间
 */
@Slf4j
public class DataSourceContextHolder {

    // 存储当前线程的数据源标识(如 master、slave1、slave2)
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置当前线程的数据源标识
     * @param dataSource 数据源标识
     */
    public static void setDataSource(String dataSource) {
        log.info("当前线程[{}]切换数据源至:{}", Thread.currentThread().getId(), dataSource);
        CONTEXT_HOLDER.set(dataSource);
    }

    /**
     * 获取当前线程的数据源标识
     * @return 数据源标识(null 则使用默认数据源)
     */
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空当前线程的数据源标识
     * 必须在方法执行完毕后调用(finally 中),避免线程复用导致数据源错乱
     */
    public static void clear() {
        log.info("当前线程[{}]清空数据源标识", Thread.currentThread().getId());
        CONTEXT_HOLDER.remove();
    }
}
3.2 动态数据源路由(DynamicDataSource)

继承 Spring 提供的 AbstractRoutingDataSource 类,重写 determineCurrentLookupKey 方法,该方法会在获取数据库连接时被调用,返回当前要使用的数据源标识,从而实现数据源动态路由。

go 复制代码
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 动态数据源路由类(核心)
 * 继承 AbstractRoutingDataSource,实现数据源动态切换
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 重写数据源路由方法,返回当前要使用的数据源标识
     * @return 数据源标识(与 application.yml 中配置的数据源名称一致)
     */
    @Override
    protected Object determineCurrentLookupKey() {
        // 从 ThreadLocal 中获取当前线程的数据源标识
        String dataSource = DataSourceContextHolder.getDataSource();
        // 若未设置数据源标识,返回 null,将使用默认数据源(master)
        return dataSource;
    }
}

说明:AbstractRoutingDataSource 内部维护了一个 Map<Object, Object>; targetDataSources,用于存储所有数据源(key 是数据源标识,value 是数据源对象),同时还有一个 defaultTargetDataSource(默认数据源),当 determineCurrentLookupKey 返回 null 时,会使用默认数据源。

步骤4:多数据源配置类(DataSourceConfig)

将主库、从库1、从库2 注入 Spring 容器,整合到 DynamicDataSource 中,设置默认数据源,同时指定 MyBatis 的 mapper 扫描路径(如果使用 MyBatis/MyBatis-Plus),确保 Spring 能正确识别数据源。

go 复制代码
import org.springframework.beans.factory.annotation.Qualifier;
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 org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 多数据源配置类(将所有数据源注入 Spring 容器,整合动态数据源)
 */
@Configuration
// 扫描 MyBatis 的 mapper 接口(如果使用 MyBatis/MyBatis-Plus,必须添加)
// @MapperScan("com.xxx.**.mapper")
public class DataSourceConfig {

    /**
     * 注入主库数据源(@ConfigurationProperties 自动绑定 application.yml 中的配置)
     */
    @Bean(name = "masterDataSource")
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        // 使用 DataSourceBuilder 构建数据源,自动适配连接池
        return DataSourceBuilder.create().build();
    }

    /**
     * 注入从库1数据源
     */
    @Bean(name = "slave1DataSource")
    @ConfigurationProperties("spring.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 注入从库2数据源
     */
    @Bean(name = "slave2DataSource")
    @ConfigurationProperties("spring.datasource.slave2")
    public DataSource slave2DataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 整合动态数据源(核心 Bean)
     * @Primary:标记为默认数据源,避免 Spring 容器中存在多个 DataSource 时报错
     */
    @Bean(name = "dynamicDataSource")
    @Primary
    public DataSource dynamicDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slave1DataSource") DataSource slave1DataSource,
            @Qualifier("slave2DataSource") DataSource slave2DataSource) {
        // 1. 构建数据源映射(key:数据源标识,value:数据源对象)
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", masterDataSource);
        dataSourceMap.put("slave1", slave1DataSource);
        dataSourceMap.put("slave2", slave2DataSource);

        // 2. 初始化动态数据源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 设置所有数据源
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        // 设置默认数据源(主库)
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);

        return dynamicDataSource;
    }

    /**
     * 配置事务管理器(重要!否则事务不生效)
     * 事务管理器需要绑定动态数据源,确保事务能跟随数据源切换
     */
    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}

关键说明:1. @Primary 注解必须添加,因为 Spring 容器中会有多个 DataSource Bean(master、slave1、slave2、dynamicDataSource),标记 dynamicDataSource 为默认数据源,避免注入时冲突;2. 事务管理器必须绑定动态数据源,否则事务会失效,尤其是读写分离场景下,主库写入的事务无法正常提交/回滚;3. 如果使用 MyBatis-Plus,只需添加 @MapperScan 注解,指定 mapper 路径即可。

步骤5:自定义数据源切换注解(@DS)

创建自定义注解 @DS,用于标记类或方法需要使用的数据源,注解值为数据源标识(如 master、slave1),支持类级别和方法级别注解,方法级别注解优先级高于类级别注解(灵活适配不同场景)。

go 复制代码
import java.lang.annotation.*;

/**
 * 自定义数据源切换注解
 * @Target:注解作用范围(类、方法)
 * @Retention:注解保留策略(运行时保留,AOP 切面可获取注解属性)
 * @Documented:生成 API 文档时,显示该注解
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {

    /**
     * 数据源标识(与 application.yml 中配置的数据源名称一致)
     * 默认值为 master,即不添加注解时,默认使用主库
     */
    String value() default "master";
}

注解使用说明:

  • • 类级别注解:@DS("slave1"),表示该类中所有方法都使用 slave1 数据源;

  • • 方法级别注解:@DS("slave2"),表示该方法使用 slave2 数据源,优先级高于类级别注解;

  • • 不添加注解:默认使用 master 数据源(主库)。

步骤6:AOP 切面实现自动切换

创建 AOP 切面,拦截所有添加了 @DS 注解的类或方法,在方法执行前,从注解中获取数据源标识,存入 ThreadLocal;方法执行完毕后,清空 ThreadLocal,确保线程安全。同时设置切面优先级(@Order(1)),保证切面在事务之前执行,否则数据源切换会失效。

go 复制代码
import lombok.extern.slf4j.Slf4j;
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.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 数据源切换 AOP 切面(核心,实现注解驱动的数据源切换)
 * @Aspect:标记此类为 AOP 切面
 * @Component:交给 Spring 管理,确保 Spring 能扫描到该切面
 * @Order(1):设置切面优先级,1 表示优先执行(必须在事务切面之前执行,否则数据源切换失效)
 * @Slf4j:日志输出,便于排查问题
 */
@Aspect
@Component
@Order(1)
@Slf4j
public class DataSourceAspect {

    /**
     * 定义切点:拦截所有添加了 @DS 注解的类或方法
     * @annotation(com.xxx.annotation.DS):拦截方法上有 @DS 注解的方法
     * @within(com.xxx.annotation.DS):拦截类上有 @DS 注解的所有方法
     */
    @Pointcut("@annotation(com.xxx.annotation.DS) || @within(com.xxx.annotation.DS)")
    public void dsPointcut() {}

    /**
     * 环绕通知:包裹目标方法,在方法执行前切换数据源,执行后清空数据源标识
     * @param joinPoint 切入点(获取目标方法、类的信息)
     * @return 目标方法的执行结果
     * @throws Throwable 异常抛出
     */
    @Around("dsPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取目标方法/类上的 @DS 注解
        DS dsAnnotation = getDataSourceAnnotation(joinPoint);
        // 2. 如果注解不为空,设置数据源标识
        if (dsAnnotation != null) {
            String dataSource = dsAnnotation.value();
            DataSourceContextHolder.setDataSource(dataSource);
        }
        try {
            // 3. 执行目标方法(核心业务逻辑)
            return joinPoint.proceed();
        } finally {
            // 4. 无论方法是否执行成功,都要清空数据源标识(避免线程复用污染)
            DataSourceContextHolder.clear();
        }
    }

    /**
     * 获取目标方法/类上的 @DS 注解
     * 优先级:方法上的注解 > 类上的注解
     * @param joinPoint 切入点
     * @return @DS 注解(null 表示没有添加注解)
     */
    private DS getDataSourceAnnotation(ProceedingJoinPoint joinPoint) {
        // 获取目标方法的签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = signature.getMethod();
        // 先获取方法上的 @DS 注解
        DS methodAnnotation = targetMethod.getAnnotation(DS.class);
        if (methodAnnotation != null) {
            return methodAnnotation;
        }
        // 方法上没有注解,获取类上的 @DS 注解
        Class<?> targetClass = joinPoint.getTarget().getClass();
        return targetClass.getAnnotation(DS.class);
    }
}

避坑重点:1. @Order(1) 必须设置,因为 Spring 的事务切面默认优先级是 Ordered.LOWEST_PRECEDENCE(最低),数据源切换必须在事务之前执行,否则事务会绑定默认数据源,切换失效;2. finally 块中必须调用 DataSourceContextHolder.clear(),否则线程池复用线程时,会携带上一个线程的数据源标识,导致数据源错乱;3. 切点必须同时拦截方法和类上的注解,确保两种场景都能生效。

步骤7:使用示例

配置完成后,只需在 Service 类或方法上添加 @DS 注解,就能实现数据源切换,业务代码无需做任何修改,真正做到零侵入。以下是3种高频使用场景,覆盖读写分离、多库切换,可直接参考。

场景1:类级别切换(整个 Service 走从库1)

适合整个 Service 都是查询操作的场景(如用户查询、商品查询),直接在类上添加 @DS("slave1"),所有方法都将使用 slave1 数据源。

go 复制代码
import com.xxx.annotation.DS;
import com.xxx.entity.User;
import com.xxx.mapper.UserMapper;
import com.xxx.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 用户 Service(查询操作,走从库1)
 * @DS("slave1"):类级别注解,所有方法都使用 slave1 数据源
 */
@Service
@DS("slave1")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 查询所有用户(自动走 slave1 从库)
     */
    @Override
    public List<User> listAll() {
        return userMapper.selectList(null);
    }

    /**
     * 根据 ID 查询用户(自动走 slave1 从库)
     */
    @Override
    public User getById(Long id) {
        return userMapper.selectById(id);
    }
}
场景2:方法级别切换(读写分离,最常用)

适合 Service 中既有写入操作(主库),又有查询操作(从库)的场景,在写入方法上添加 @DS("master"),查询方法上添加 @DS("slave2"),实现读写分离。

go 复制代码
import com.xxx.annotation.DS;
import com.xxx.entity.Order;
import com.xxx.mapper.OrderMapper;
import com.xxx.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * 订单 Service(读写分离场景)
 * 无类级别注解,默认走 master 主库
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 新增订单(写入操作,走主库)
     * @DS("master"):方法级别注解,指定使用 master 数据源
     * @Transactional:事务注解,确保写入操作的原子性
     */
    @DS("master")
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void addOrder(Order order) {
        orderMapper.insert(order);
    }

    /**
     * 修改订单(写入操作,走主库)
     */
    @DS("master")
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateOrder(Order order) {
        orderMapper.updateById(order);
    }

    /**
     * 根据用户 ID 查询订单(查询操作,走从库2)
     */
    @DS("slave2")
    @Override
    public List<Order> queryByUserId(Long userId) {
        return orderMapper.selectByUserId(userId);
    }

    /**
     * 查询所有订单(查询操作,走从库2)
     */
    @DS("slave2")
    @Override
    public List<Order> listAll() {
        return orderMapper.selectList(null);
    }
}
场景3:不加注解(默认走主库)

如果方法或类上没有添加 @DS 注解,将自动使用默认数据源(master 主库),适合写入操作或不需要切换数据源的场景。

go 复制代码
import com.xxx.entity.User;
import com.xxx.mapper.UserMapper;
import com.xxx.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 用户 Service(无注解,默认走主库)
 */
@Service
public class UserServiceImpl2 implements UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 修改用户信息(无注解,自动走 master 主库)
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateUser(User user) {
        userMapper.updateById(user);
    }
}

步骤8:测试验证

为了确保数据源切换正常,我们通过单元测试和接口测试,覆盖读写分离、多库切换等场景,验证切换效果。以下是详细的测试流程,可直接复制测试代码。

8.1 单元测试
go 复制代码
import com.xxx.entity.Order;
import com.xxx.entity.User;
import com.xxx.service.OrderService;
import com.xxx.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

/**
 * 动态数据源切换单元测试
 */
@SpringBootTest
public class DynamicDataSourceTest {

    @Autowired
    private UserService userService;

    @Autowired
    private OrderService orderService;

    /**
     * 测试1:用户查询(走 slave1 从库)
     */
    @Test
    public void testUserList() {
        List<User> userList = userService.listAll();
        System.out.println("用户列表(slave1 从库):" + userList);
    }

    /**
     * 测试2:订单新增(走 master 主库)+ 订单查询(走 slave2 从库)
     */
    @Test
    public void testOrderCRUD() {
        // 1. 新增订单(主库)
        Order order = new Order();
        order.setUserId(1001L);
        order.setOrderNo("ORDER20260416001");
        orderService.addOrder(order);
        System.out.println("新增订单成功(master 主库)");

        // 2. 查询订单(从库2)
        List<Order> orderList = orderService.queryByUserId(1001L);
        System.out.println("订单列表(slave2 从库):" + orderList);
    }

    /**
     * 测试3:无注解方法(走 master 主库)
     */
    @Test
    public void testNoAnnotation() {
        User user = new User();
        user.setId(1L);
        user.setUsername("test");
        userService.updateUser(user);
        System.out.println("修改用户成功(master 主库)");
    }
}
8.2 测试结果验证

运行单元测试,查看控制台日志,若出现以下日志,说明数据源切换成功:

go 复制代码
当前线程[1]切换数据源至:slave1
用户列表(slave1 从库):[User(id=1, username=xxx)...]
当前线程[1]清空数据源标识

当前线程[2]切换数据源至:master
新增订单成功(master 主库)
当前线程[2]清空数据源标识
当前线程[3]切换数据源至:slave2
订单列表(slave2 从库):[Order(id=1, orderNo=ORDER20260416001)...]
当前线程[3]清空数据源标识

当前线程[4]清空数据源标识(无注解,使用默认数据源 master)
修改用户成功(master 主库)

同时,可通过数据库查询验证:新增的订单会出现在 db_master 库中,查询时会从 db_slave2 库中获取数据,说明读写分离生效。

文末小结

SpringBoot + AOP 实现动态多数据源切换,是企业级项目中最优雅、最常用的方案之一,核心优势就是「业务代码零侵入、切换逻辑统一管理、扩展性极强」。

如果你在实战中遇到问题(如数据源切换失效、事务不生效、多租户适配困难),欢迎在评论区留言交流,一起避坑、一起进步!

别忘了点赞+在看+收藏三连,关注我,解锁更多 SpringBoot AOP 实战干货,下期再见❤️

相关推荐
qq_206901392 小时前
如何在 React 中正确使用 onClick 事件避免类型错误
jvm·数据库·python
2401_871696522 小时前
如何防止SQL注入利用存储过程_确保存储过程不拼字符串
jvm·数据库·python
2301_796588502 小时前
如何在 macOS 中使用 launchd 每分钟执行一次 PHP 脚本
jvm·数据库·python
m0_748920362 小时前
HTML函数在笔记本上卡顿怎么办_笔记本运行HTML函数优化操作【操作】
jvm·数据库·python
广师大-Wzx2 小时前
JavaWeb:前端部分
java·前端·javascript·css·vue.js·前端框架·html
生万千欢喜心2 小时前
Linux 安装金蝶天燕中间件 AAS-V9.0.zip
java·linux
2601_949814692 小时前
如何使用C#与SQL Server数据库进行交互
数据库·c#·交互
WJB-DavidWang2 小时前
MongoDB-非关系型数据库-文档数据库(三) Kafka测试MongoDB性能
数据库·mongodb·nosql
m0_678485452 小时前
c++如何提取系统环境变量并直接保存到txt日志中_getenv与ofstream【实战】
jvm·数据库·python