Databases 14 min read

Implementing Dynamic MySQL Master‑Slave Switching in SpringBoot Using AOP and Custom Annotations

This article demonstrates how to use SpringBoot 3.0.4 with AOP and a custom @DataSource annotation to dynamically switch between MySQL master and slave databases, ensuring high availability by automatically falling back to the master when a slave fails, and provides configuration and code examples for multiple data sources.

Java Captain
Java Captain
Java Captain
Implementing Dynamic MySQL Master‑Slave Switching in SpringBoot Using AOP and Custom Annotations

Introduction: The article explains a method, based on the RuoYi source code, to achieve dynamic switching of MySQL master‑slave databases in a SpringBoot project using AOP and a custom annotation, guaranteeing service high availability when a slave node becomes unavailable.

Why switch data sources: Dynamic switching addresses several scenarios such as read‑write separation, multi‑tenant architectures, sharding, environment isolation, flexible database management, and fault‑tolerance for high availability.

How to switch data sources:

SpringBoot version: 3.0.4

JDK version: JDK 17

1. pom.xml dependencies

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
<!-- AOP support -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Druid connection pool -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid-spring-boot-starter</artifactId>
  <version>1.2.20</version>
</dependency>
<!-- MySQL driver -->
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- MyBatis‑Plus -->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.5.3.1</version>
</dependency>

2. Configuration files

application.yml

server:
  port: 8000
spring:
  profiles:
    active: druid

application-druid.yml (partial)

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # Master datasource
      master:
        url: jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      # Slave datasource (switchable)
      slave:
        enabled: true
        url: jdbc:mysql://localhost:3306/t_lyj?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      # Connection pool settings
      initialSize: 5
      minIdle: 10
      maxActive: 20
      maxWait: 60000
      connectTimeout: 30000
      socketTimeout: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      maxEvictableIdleTimeMillis: 900000

3. DataSourceType enum

/**
 * Data source type enumeration
 */
public enum DataSourceType {
    /** Master */
    MASTER,
    /** Slave */
    SLAVE
}

4. SpringUtils bean helper

@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware {
    private static ConfigurableListableBeanFactory beanFactory;
    private static ApplicationContext applicationContext;
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SpringUtils.beanFactory = beanFactory;
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtils.applicationContext = applicationContext;
    }
    @SuppressWarnings("unchecked")
    public static
T getBean(String name) throws BeansException {
        return (T) beanFactory.getBean(name);
    }
    public static
T getBean(Class
clz) throws BeansException {
        return beanFactory.getBean(clz);
    }
    public static boolean containsBean(String name) {
        return beanFactory.containsBean(name);
    }
    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.isSingleton(name);
    }
    public static Class
getType(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getType(name);
    }
    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getAliases(name);
    }
    @SuppressWarnings("unchecked")
    public static
T getAopProxy(T invoker) {
        return (T) AopContext.currentProxy();
    }
    public static String[] getActiveProfiles() {
        return applicationContext.getEnvironment().getActiveProfiles();
    }
    public static String getActiveProfile() {
        final String[] activeProfiles = getActiveProfiles();
        return StringUtils.isNotEmpty(Arrays.toString(activeProfiles)) ? activeProfiles[0] : null;
    }
    public static String getRequiredProperty(String key) {
        return applicationContext.getEnvironment().getRequiredProperty(key);
    }
}

5. Custom @DataSource annotation

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
    DataSourceType value() default DataSourceType.MASTER;
}

6. DruidConfig – datasource bean definitions

@Configuration
public class DruidConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties){
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }
    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }
    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource) {
        Map
targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }
    public void setDataSource(Map
targetDataSources, String sourceName, String beanName) {
        try {
            DataSource dataSource = SpringUtils.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        } catch (Exception e) {
            // ignore if bean not found
        }
    }
}

7. DynamicDataSource – routing datasource

public class DynamicDataSource extends AbstractRoutingDataSource {
    public DynamicDataSource(DataSource defaultTargetDataSource, Map
targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

8. DynamicDataSourceContextHolder – ThreadLocal holder

public class DynamicDataSourceContextHolder {
    private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
    private static final ThreadLocal
CONTEXT_HOLDER = new ThreadLocal<>();
    public static void setDataSourceType(String dsType) {
        log.info("切换到{}数据源", dsType);
        CONTEXT_HOLDER.set(dsType);
    }
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER.name() : CONTEXT_HOLDER.get();
    }
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

9. DataSourceAspect – AOP interceptor

@Aspect
@Order(1)
@Component
public class DataSourceAspect {
    @Pointcut("@annotation(com.LYJ.study.DynamicDataSource.annocation.DataSource) || @within(com.LYJ.study.DynamicDataSource.annocation.DataSource)")
    public void dsPointCut(){}
    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        DataSource dataSource = getDataSource(joinPoint);
        if (dataSource != null) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }
        try {
            return joinPoint.proceed();
        } finally {
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }
    public DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource)) {
            return dataSource;
        }
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

10. Usage in service layer

@Service
@RequiredArgsConstructor
@DataSource(DataSourceType.MASTER)
public class UserServiceImpl extends ServiceImpl
implements UserService {
    private final UserMapper userMapper;
    @Override
    @DataSource(DataSourceType.MASTER)
    public List
queryAll() {
        return userMapper.selectList(null);
    }
}

The annotation can be placed on classes or individual methods, and the same approach works for mapper interfaces.

11. Extending to multiple slaves

To add more slave data sources, extend the DataSourceType enum (e.g., SLAVE2 , SLAVE3 ) and add corresponding configuration sections in application‑druid.yml . The DruidConfig helper will automatically load beans named slaveDataSource , slave2DataSource , etc., when the enabled flag is true.

12. Oracle example

com.oracle
ojdbc6
11.2.0.3
slave3:
  enabled: true
  url: jdbc:oracle:thin:@127.0.0.1:1521:oracle
  username: root
  password: password

After adding the Oracle datasource, remove the MySQL driver if it is no longer needed; SpringBoot will auto‑detect the appropriate driver.

Notes

SQL dialect differences between MySQL and Oracle may cause startup errors; adjust queries accordingly.

Author: 小杰不秃头 (source: blog.csdn.net/cyuyanya__/article/details/139605809).

AOPMySQLSpringBootDatabase SwitchingDynamic DataSource
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.