Elegant Dynamic Data Source Switching in Spring Boot with ThreadLocal & AbstractRoutingDataSource
This tutorial demonstrates how to implement dynamic data source switching in Spring Boot using ThreadLocal and AbstractRoutingDataSource, covering configuration, annotation-based routing via AOP, and runtime data source registration from a database.
When a business requirement demanded fetching data from multiple databases and writing to the current one, the author explored dynamic data source switching in Spring Boot. After the MyBatis-Plus dynamic-datasource-spring-boot-starter proved incompatible with the project environment, a custom solution using ThreadLocal and AbstractRoutingDataSource was built to replicate the starter's thread‑level data source routing.
Core Concepts
ThreadLocal
ThreadLocal(thread local variable) solves concurrency‑induced data inconsistency by giving each thread its own variable copy. This isolates threads, increases memory slightly, but greatly reduces synchronization overhead and concurrency control complexity. Key points:
Role: shared within a thread, isolated across threads.
Principle: when storing a value, the current thread instance is used as the key in a map held by the thread object.
AbstractRoutingDataSource
This Spring class selects the current data source based on user‑defined rules. Its abstract method determineCurrentLookupKey() is invoked before each database operation to decide which data source to use.
Implementation Steps
2.1 ThreadLocal Holder – DataSourceContextHolder
/**
* @author: jiangjs
* @description:
* @date: 2023/7/27 11:21
**/
public class DataSourceContextHolder {
private static final ThreadLocal<String> 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 Dynamic Data Source – DynamicDataSource
/**
* @author: jiangjs
* @description: Implements dynamic data source, routes via AbstractRoutingDataSource
* @date: 2023/7/27 11:18
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}The constructor sets the default data source and a map of target data sources (key = data source name, value = DataSource instance).
2.3 Data Source Configuration
application.ymldefines two Druid data sources ( master and slave) with connection pool settings. A @Configuration class DateSourceConfig uses @ConfigurationProperties to bind each to a DataSource bean, then builds a DynamicDataSource bean (marked @Primary) that holds both in a map.
@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<Object,Object> dataSourceMap = new HashMap<>();
DataSource defaultDataSource = masterDataSource();
dataSourceMap.put("master", defaultDataSource);
dataSourceMap.put("slave", slaveDataSource());
return new DynamicDataSource(defaultDataSource, dataSourceMap);
}
}Note: Exclude Spring Boot's auto‑configured data source to avoid circular dependency:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)2.4 Testing the Basic Switch
Two test databases each contain a test_user table with a single user_name column. Master holds 'master', slave holds 'slave'. A controller method accepts a data source name, sets it via DataSourceContextHolder.setDataSource(), queries via MyBatis‑Plus, then removes the holder.
@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
DataSourceContextHolder.setDataSource(datasourceName);
TestUser testUser = testUserMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return testUser.getUserName();
}Results confirm that passing master returns master and passing slave returns slave.
The author notes that MyBatis‑Plus's DynamicDataSourceContextHolder uses a stack internally to support nested data source switching, a feature not replicated here.
Optimization: Annotation‑Driven Switching
2.5.1.1 Define @DS Annotation
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
String value() default "master";
}2.5.1.2 AOP Aspect
@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 {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
if (Objects.nonNull(ds)){
DataSourceContextHolder.setDataSource(ds.value());
}
try {
return point.proceed();
} finally {
DataSourceContextHolder.removeDataSource();
}
}
}The aspect intercepts methods annotated with @DS, reads the value, sets the data source, proceeds, and ensures cleanup in finally.
2.5.1.3 Annotation Test
Two endpoints: /getMasterData.do (no annotation, defaults to master) and /getSlaveData.do (annotated with @DS("slave")). Both return the expected user name.
Dynamic Data Source Addition at Runtime
2.5.2.1 Data Source Entity
@Data
@Accessors(chain = true)
public class DataSourceEntity {
private String url;
private String userName;
private String passWord;
private String driverClassName;
private String key; // map key in DynamicDataSource
}2.5.2.2 Enhanced DynamicDataSource
The class now retains a reference to the target data source map and exposes createDataSource(List<DataSourceEntity>) and existsDataSource(String).
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Map<Object, Object> targetDataSourceMap;
public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
this.targetDataSourceMap = targetDataSources;
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
public void createDataSource(List<DataSourceEntity> dataSources){
try {
if (CollectionUtils.isNotEmpty(dataSources)){
for (DataSourceEntity ds : dataSources) {
// Validate connectivity
Class.forName(ds.getDriverClassName());
DriverManager.getConnection(ds.getUrl(), ds.getUserName(), ds.getPassWord());
// Build Druid data source
DruidDataSource dataSource = new DruidDataSource();
BeanUtils.copyProperties(ds, dataSource);
dataSource.setTestOnBorrow(true);
dataSource.setTestWhileIdle(true);
dataSource.setValidationQuery("select 1");
dataSource.init();
this.targetDataSourceMap.put(ds.getKey(), dataSource);
}
super.setTargetDataSources(this.targetDataSourceMap);
super.afterPropertiesSet();
return Boolean.TRUE;
}
} catch (ClassNotFoundException | SQLException e) {
log.error("---程序报错---:{}", e.getMessage());
}
return Boolean.FALSE;
}
public boolean existsDataSource(String key){
return Objects.nonNull(this.targetDataSourceMap.get(key));
}
}2.5.2.3 Startup Loader
A CommandLineRunner bean reads a test_db_info table (columns: id, url, username, password, driver_class_name, name) at startup, maps rows to DataSourceEntity, and calls dynamicDataSource.createDataSource().
@Component
public class LoadDataSourceRunner implements CommandLineRunner {
@Resource
private DynamicDataSource dynamicDataSource;
@Resource
private TestDbInfoMapper testDbInfoMapper;
@Override
public void run(String... args) throws Exception {
List<TestDbInfo> testDbInfos = testDbInfoMapper.selectList(null);
if (CollectionUtils.isNotEmpty(testDbInfos)) {
List<DataSourceEntity> ds = new ArrayList<>();
for (TestDbInfo testDbInfo : testDbInfos) {
DataSourceEntity sourceEntity = new DataSourceEntity();
BeanUtils.copyProperties(testDbInfo, sourceEntity);
sourceEntity.setKey(testDbInfo.getName());
ds.add(sourceEntity);
}
dynamicDataSource.createDataSource(ds);
}
}
}2.5.2.4 Dynamic Addition Test
After inserting the former slave database into test_db_info with name add_slave, the application loads it on startup. Calling the test endpoint with add_slave returns the slave data, proving the new data source was registered dynamically.
The article concludes that this approach provides a deeper understanding of dynamic data source switching and encourages readers to experiment further.
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.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
