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.
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: druidapplication-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: 9000003. 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: passwordRemember that SQL dialect differences may cause startup errors when mixing MySQL and Oracle.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.