Implementing Dynamic Data Source Switching in Spring Boot with ThreadLocal and AbstractRoutingDataSource
This article demonstrates how to build a dynamic data source switching mechanism in Spring Boot by leveraging ThreadLocal for thread‑scoped context and AbstractRoutingDataSource for routing, covering manual implementation, annotation‑driven switching, and runtime addition of new data sources.
When a business requirement involves reading from multiple databases and writing into a single one, dynamic data source switching becomes essential. The article first explains why the built‑in dynamic-datasource-spring-boot-starter may not be usable and proposes a custom solution based on ThreadLocal and AbstractRoutingDataSource .
1. Introduction
ThreadLocal provides a separate variable instance per thread, eliminating concurrency issues, while AbstractRoutingDataSource determines the current lookup key via determineCurrentLookupKey() to select the appropriate datasource.
2. Code Implementation
2.1 ThreadLocal Helper
public class DataSourceContextHolder {
private static final ThreadLocal
DATASOURCE_HOLDER = new ThreadLocal<>();
public static void setDataSource(String dataSourceName) { DATASOURCE_HOLDER.set(dataSourceName); }
public static String getDataSource() { return DATASOURCE_HOLDER.get(); }
public static void removeDataSource() { DATASOURCE_HOLDER.remove(); }
}2.2 AbstractRoutingDataSource Extension
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource, Map
targetDataSources) {
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}2.3 Configuration (application.yml)
spring:
datasource:
druid:
master:
url: jdbc:mysql://xxxxxx:3306/test1?...
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://xxxxx:3306/test2?...
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
...2.4 Bean Registration
@Configuration
public class DateSourceConfig {
@Bean @ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); }
@Bean @ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); }
@Bean(name = "dynamicDataSource") @Primary
public DynamicDataSource createDynamicDataSource() {
Map
map = new HashMap<>();
DataSource defaultDs = masterDataSource();
map.put("master", defaultDs);
map.put("slave", slaveDataSource());
return new DynamicDataSource(defaultDs, map);
}
}2.5 Service Usage
@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName) {
DataSourceContextHolder.setDataSource(datasourceName);
TestUser user = testUserMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return user.getUserName();
}The above method shows how passing different datasource names (e.g., master or slave ) yields different query results, confirming the dynamic switch.
2.5 Optimization
2.5.1 Annotation‑Based Switching
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented @Inherited
public @interface DS { String value() default "master"; }Using an AOP aspect, the datasource is set before method execution and cleared afterwards:
@Aspect @Component @Slf4j
public class DSAspect {
@Pointcut("@annotation(com.jiashn.dynamic_datasource.dynamic.aop.DS)")
public void dynamicDataSource() {}
@Around("dynamicDataSource()")
public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
Method method = ((MethodSignature) point.getSignature()).getMethod();
DS ds = method.getAnnotation(DS.class);
if (Objects.nonNull(ds)) { DataSourceContextHolder.setDataSource(ds.value()); }
try { return point.proceed(); } finally { DataSourceContextHolder.removeDataSource(); }
}
}Controller methods can now be annotated with @DS("slave") to automatically switch the datasource.
2.5.2 Dynamic Addition of Data Sources
A DataSourceEntity class stores connection details and a unique key. The enhanced DynamicDataSource keeps a mutable targetDataSourceMap and provides createDataSource(List ) to add new datasources at runtime, validating connections via DriverManager and configuring Druid pools.
public void createDataSource(List
dataSources) {
for (DataSourceEntity ds : dataSources) {
Class.forName(ds.getDriverClassName());
DriverManager.getConnection(ds.getUrl(), ds.getUserName(), ds.getPassWord());
DruidDataSource dataSource = new DruidDataSource();
BeanUtils.copyProperties(ds, dataSource);
dataSource.setTestOnBorrow(true);
dataSource.setTestWhileIdle(true);
dataSource.setValidationQuery("select 1");
dataSource.init();
targetDataSourceMap.put(ds.getKey(), dataSource);
}
super.setTargetDataSources(targetDataSourceMap);
super.afterPropertiesSet();
}A CommandLineRunner reads datasource definitions from a table ( test_db_info ) at application startup and invokes dynamicDataSource.createDataSource(dsList) , making the new connections immediately available for the existing switching logic.
3. Summary
The tutorial walks through building a complete dynamic datasource solution from scratch, covering the core concepts of thread‑local context, routing datasource, annotation‑driven switching, and runtime datasource registration, providing a lightweight alternative to third‑party starters while illustrating practical patterns for multi‑tenant or read/write‑split architectures.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.