Implementing Dynamic MySQL Master‑Slave Data Source Switching in SpringBoot with AOP and Custom Annotations
This tutorial explains how to use SpringBoot, AOP, and a custom @DataSource annotation to dynamically switch between MySQL master and slave databases—covering configuration, code implementation, multiple slave handling, and an Oracle example—to ensure high availability and flexible data source management.
This guide, based on the RuoYi source, shows how to use AOP and a custom @DataSource annotation in a SpringBoot project to achieve dynamic switching between MySQL master and slave databases, ensuring high availability when a slave fails.
Why switch data sources: Scenarios include read/write separation, multi‑tenant architecture, sharding, environment isolation, flexible database management, and fault tolerance for high availability.
How to switch: The project uses SpringBoot 3.0.4 and JDK 17. The pom.xml must include Lombok, spring-boot-starter-aop , druid-spring-boot-starter , MySQL driver, and MyBatis‑Plus.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<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>Configuration files: application.yml defines the active profile, while application-druid.yml configures the master and slave data sources and connection‑pool parameters.
# master datasource
spring:
datasource:
druid:
master:
url: jdbc:mysql://localhost:3306/study?...
username: root
password: 123456
# slave datasource (enabled)
slave:
enabled: true
url: jdbc:mysql://localhost:3306/t_lyj?...
username: root
password: 123456
initialSize: 5
minIdle: 10
maxActive: 20
maxWait: 60000
...Data source enumeration:
public enum DataSourceType { MASTER, SLAVE }SpringUtils bean helper provides static methods to obtain beans, check bean existence, and access environment properties.
@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware {
private static ConfigurableListableBeanFactory beanFactory;
private static ApplicationContext applicationContext;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) { beanFactory = bf; }
@Override
public void setApplicationContext(ApplicationContext ctx) { applicationContext = ctx; }
@SuppressWarnings("unchecked")
public static
T getBean(String name) { return (T) beanFactory.getBean(name); }
// other utility methods omitted for brevity
}Custom @DataSource annotation allows specifying the target data source at class or method level, with method‑level priority.
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
DataSourceType value() default DataSourceType.MASTER;
}DruidConfig defines beans for master and slave DataSource objects and registers a DynamicDataSource bean that holds the routing map.
@Configuration
public class DruidConfig {
@Bean @ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties props) { ... }
@Bean @ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix="spring.datasource.druid.slave", name="enabled", havingValue="true")
public DataSource slaveDataSource(DruidProperties props) { ... }
@Bean(name="dynamicDataSource") @Primary
public DynamicDataSource dataSource(DataSource masterDataSource) {
Map
target = new HashMap<>();
target.put(DataSourceType.MASTER.name(), masterDataSource);
// slave will be added later via setDataSource()
return new DynamicDataSource(masterDataSource, target);
}
// setDataSource helper omitted for brevity
}DynamicDataSource extends AbstractRoutingDataSource and determines the current lookup key from DynamicDataSourceContextHolder .
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTarget, Map
targets) {
super.setDefaultTargetDataSource(defaultTarget);
super.setTargetDataSources(targets);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}DynamicDataSourceContextHolder uses a ThreadLocal to store the current data source name, with methods to set, get (defaulting to MASTER ), and clear the value.
public class DynamicDataSourceContextHolder {
private static final ThreadLocal
CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String 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(); }
}AOP aspect ( DataSourceAspect ) defines a pointcut on @DataSource annotations, switches the data source before method execution, and clears it afterwards.
@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);
}
}Usage: Annotate service or mapper classes/methods with @DataSource(value = DataSourceType.MASTER) or SLAVE . The example shows a UserServiceImpl using the annotation.
@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); }
}For multiple slaves, extend the DataSourceType enum (e.g., SLAVE2 ) and add corresponding sections in application-druid.yml and bean definitions in DruidConfig .
Oracle example: Add the Oracle JDBC driver dependency and configure an additional slave ( slave3 ) in application-druid.yml . Remove the MySQL driver if it is not needed.
<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: passwordNote: Switching between MySQL and Oracle may cause SQL syntax differences that can lead to startup errors.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
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.