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> 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 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<Object, Object> target = new HashMap<>();
target.put(DataSourceType.MASTER.name(), masterDataSource);
setDataSource(target, DataSourceType.SLAVE.name(), "slaveDataSource");
return new DynamicDataSource(masterDataSource, target);
}
private void setDataSource(Map<Object, Object> 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<Object, Object> 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<String> 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<UserMapper, User> implements UserService {
private final UserMapper userMapper;
@Override
@DataSource(DataSourceType.MASTER)
public List<User> 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
