Implementing Dynamic MySQL Master‑Slave Switching in SpringBoot Using AOP and Custom Annotations
This tutorial explains how to implement dynamic master‑slave MySQL switching in a SpringBoot 3.0.4 project using AOP and a custom @DataSource annotation, covering configuration, enum definitions, utility classes, aspect handling, and usage examples for both single and multiple data sources, including Oracle integration.
Preface
This article introduces how to use AOP and a custom annotation in a SpringBoot project to achieve dynamic switching of MySQL master‑slave databases, automatically falling back to the master when a slave fails to ensure high availability.
Why Switch Data Sources? What Scenarios?
Read‑Write Separation: Master handles writes, slaves handle reads to improve performance and availability.
Multi‑Tenant Architecture: Different tenants may need to operate on different databases; dynamic switching selects the appropriate source based on tenant identity.
Sharding (Database Partitioning): Large‑scale data can be split across databases; dynamic switching directs queries to the correct shard.
Environment Isolation: Development, testing, and production may use different databases; dynamic switching ensures proper isolation.
Flexible Database Management: Business logic can decide which data source to use at runtime.
Failover and High Availability: When the primary database is unavailable, the system can automatically switch to a standby source.
How to Switch Data Sources?
SpringBoot version: 3.0.4
JDK version: JDK 17
1. pom.xml
<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 & application‑druid.yml
# application.yml
server:
port: 8000
spring:
profiles:
active: druid # application‑druid.yml (MySQL master & slave example)
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 (enabled)
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
/**
* Data source enumeration
*/
public enum DataSourceType {
/** Master */
MASTER,
/** Slave */
SLAVE,
/** Additional slave */
SLAVE2
}4. Bean Utility Class: SpringUtils
@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);
}
public static
T getAopProxy(T invoker) {
return (T) AopContext.currentProxy();
}
public static String[] getActiveProfiles() {
return applicationContext.getEnvironment().getActiveProfiles();
}
public static String getActiveProfile() {
String[] active = getActiveProfiles();
return (active != null && active.length > 0) ? active[0] : null;
}
public static String getRequiredProperty(String key) {
return applicationContext.getEnvironment().getRequiredProperty(key);
}
}5. Custom Annotation: @DataSource
/**
* Custom annotation for dynamic data source switching.
* Method level overrides class level.
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
DataSourceType value() default DataSourceType.MASTER;
}6. DruidConfig Class
@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);
}
public void setDataSource(Map
target, String sourceName, String beanName) {
try {
DataSource ds = SpringUtils.getBean(beanName);
target.put(sourceName, ds);
} catch (Exception e) {
// ignore if bean not found
}
}
}7. DynamicDataSource Core Class
/**
* Dynamic 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
/**
* Thread‑local holder for the current datasource key.
*/
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("Switching to {} datasource", 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. AOP Aspect: DataSourceAspect
@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();
}
}
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);
}
}10. Using the Annotation in Business Code
@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);
}
}Both service classes and mapper methods can be annotated to specify the target datasource.
Additional: Multiple Slave Configuration
To add more slaves, extend application‑druid.yml with new datasource sections, add corresponding enum values (e.g., SLAVE2 ), and register them in DruidConfig using the same setDataSource helper.
How to Switch to Oracle
com.oracle
ojdbc6
11.2.0.3 # application‑druid.yml (Oracle slave example)
spring:
datasource:
druid:
slave3:
enabled: true
url: jdbc:oracle:thin:@127.0.0.1:1521:oracle
username: root
password: passwordRemove the MySQL driver when switching to Oracle; SpringBoot will auto‑detect the appropriate driver. Be aware of SQL syntax differences that may cause startup errors.
Note: Switching databases may cause startup errors due to differences between MySQL and Oracle SQL syntax.
Author & Source
Author: 小杰不秃头 (Xiao Jie)
Source: blog.csdn.net/cyuyanya__/article/details/139605809
Promotional Content (Non‑Technical)
Free IDEA/PyCharm activation codes can be obtained by replying "C1" to the public account. JetBrains paid licenses, plugins, and themes are also offered; contact the author for details.
Top Architecture Tech Stack
Sharing Java and Python tech insights, with occasional practical development tool tips.
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.