Backend Development 16 min read

Master Dynamic MySQL Master‑Slave Switching in Spring Boot with AOP

This guide explains how to use Spring Boot, AOP, and custom annotations to implement dynamic MySQL master‑slave data source switching, automatically falling back to the master when a slave fails, covering configuration, Maven dependencies, enum definitions, data source beans, routing logic, and usage examples.

macrozheng
macrozheng
macrozheng
Master Dynamic MySQL Master‑Slave Switching in Spring Boot with AOP

Preface

This article shows how to use

SpringBoot

together with AOP and a custom annotation to achieve dynamic switching of MySQL master‑slave databases. When a slave becomes unavailable, the system automatically switches to the master to ensure high availability.

If the server has one master and multiple slaves, the master handles writes while slaves handle reads. When a read method is annotated to use a slave that fails, the system automatically switches to another available server.

Why switch data sources? Common scenarios

Read‑write separation : Improves performance by routing writes to the master and reads to slaves.

Multi‑tenant architecture : Different tenants may need separate databases; dynamic switching selects the appropriate source.

Sharding : Distributes large data sets across multiple databases; dynamic routing selects the correct shard.

Environment isolation : Development, testing, and production can each use different databases.

Flexible DB management : Allows runtime selection of the most suitable data source.

Failover and high availability : Automatically switches to a standby database when the primary is down.

How to switch data sources?

SpringBoot version: 3.0.4

JDK version: JDK17

1. pom.xml dependencies

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

2. Configuration files

application.yml

<code>#application.yml
server:
  port: 8000
spring:
  profiles:
    active: druid</code>

application-druid.yml

<code># Data source configuration
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # Master data source
      master:
        url: jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      # Slave data source
      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</code>

3. Data source type enum

<code>/**
 * Data source type
 */
public enum DataSourceType {
    /** Master */
    MASTER,
    /** Slave */
    SLAVE
}</code>

4. Bean utility class (SpringUtils)

<code>@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> T getBean(String name) throws BeansException {
        return (T) beanFactory.getBean(name);
    }
    public static <T> T getBean(Class<T> 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> 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);
    }
}</code>

5. Custom annotation for data source switching

<code>/**
 * Custom annotation for multi‑data‑source switching.
 * Priority: method overrides class.
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
    DataSourceType value() default DataSourceType.MASTER;
}</code>

6. Druid configuration class

<code>@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<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }
    private void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
        try {
            DataSource dataSource = SpringUtils.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        } catch (Exception e) {
            // ignore if bean not found
        }
    }
}</code>

7. Dynamic data source class

<code>/**
 * Dynamic routing data source.
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}</code>

8. Context holder for data source routing

<code>/**
 * Holds the current data source type using ThreadLocal.
 */
public class DynamicDataSourceContextHolder {
    private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    public static void setDataSourceType(String dsType) {
        log.info("Switching to {} data source", 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();
    }
}</code>

9. AOP aspect for data source switching

<code>@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 ds = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (ds != null) {
            return ds;
        }
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}</code>

10. Using the annotation in business code

<code>@Service
@RequiredArgsConstructor
@DataSource(value = DataSourceType.MASTER)
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private final UserMapper userMapper;
    @Override
    @DataSource(value = DataSourceType.MASTER)
    public List<User> queryAll() {
        return userMapper.selectList(null);
    }
}</code>

You can place

@DataSource

on service classes, mapper classes, or individual methods.

Additional: Handling multiple slave data sources

To add more slaves, extend

DataSourceType

with new enum values (e.g.,

SLAVE2

), add corresponding configurations in

application-druid.yml

, and register additional beans in

DruidConfig

. The routing logic will automatically recognize the new sources.

Oracle example

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

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

JavaAOPSpring BootMySQLDynamic Data Source
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

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.