Backend Development 14 min read

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

This article explains how to use SpringBoot, AOP, and a custom @DataSource annotation to dynamically switch between MySQL master and slave databases, automatically falling back to the master when a slave fails, thereby achieving high availability for backend services.

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

The guide demonstrates how to integrate dynamic master‑slave database switching into a SpringBoot project by leveraging AOP and a custom @DataSource annotation, ensuring automatic failover to the master when a slave becomes unavailable.

Why switch data sources? Dynamic switching supports read‑write separation, multi‑tenant architectures, sharding, environment isolation, flexible database management, and fault‑tolerant high availability.

Read‑write separation: writes go to the master, reads to slaves.

Multi‑tenant: different tenants can be routed to different databases.

Sharding: distribute large data sets across multiple databases.

Environment isolation: separate dev, test, and prod databases.

Flexible management: choose data sources at runtime based on business logic.

Failover: automatically switch to a backup when the primary fails.

Implementation steps :

1. pom.xml dependencies

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid-spring-boot-starter</artifactId>
  <version>1.2.20</version>
</dependency>

<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
</dependency>

<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

# Data source configuration
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      master:
        url: jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      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
      initialSize: 5
      minIdle: 10
      maxActive: 20
      maxWait: 60000
      connectTimeout: 30000
      socketTimeout: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      maxEvictableIdleTimeMillis: 900000

3. DataSourceType enum

public enum DataSourceType {
    MASTER,
    SLAVE,
    SLAVE2 // additional slaves can be added here
}

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 String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getAliases(name);
    }
    public static String[] getActiveProfiles() {
        return applicationContext.getEnvironment().getActiveProfiles();
    }
    public static String getActiveProfile() {
        String[] active = getActiveProfiles();
        return active.length > 0 ? active[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 for master and slave beans

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

7. DynamicDataSource implementation

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

8. DynamicDataSourceContextHolder

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 ds) {
        log.info("Switching to {} datasource", ds);
        CONTEXT_HOLDER.set(ds);
    }
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER.name() : CONTEXT_HOLDER.get();
    }
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

9. DataSourceAspect for AOP interception

@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 ds = getDataSource(joinPoint);
        if (ds != null) {
            DynamicDataSourceContextHolder.setDataSourceType(ds.value().name());
        }
        try {
            return joinPoint.proceed();
        } finally {
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }
    private DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature sig = (MethodSignature) point.getSignature();
        DataSource ds = AnnotationUtils.findAnnotation(sig.getMethod(), DataSource.class);
        if (ds != null) return ds;
        return AnnotationUtils.findAnnotation(sig.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 same annotation can be placed on mapper classes or individual methods to control which datasource is used.

Adding multiple slaves – extend DataSourceType with SLAVE2 , SLAVE3 , etc., add corresponding sections in application-druid.yml , and register beans in DruidConfig using the same setDataSource helper.

Switching to Oracle – add the Oracle driver dependency and a new datasource configuration:

<dependency>
  <groupId>com.oracle</groupId>
  <artifactId>ojdbc6</artifactId>
  <version>11.2.0.3</version>
</dependency>
slave3:
  enabled: true
  url: jdbc:oracle:thin:@127.0.0.1:1521:oracle
  username: root
  password: password

Remember that SQL dialect differences may cause startup errors when mixing MySQL and Oracle.

JavaAOPMySQLSpringBootDynamic DataSource
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.