Implementing Dynamic Data Source Switching in Spring Boot with ThreadLocal and AbstractRoutingDataSource
This article demonstrates how to build a custom dynamic data‑source solution for Spring Boot by using ThreadLocal to store the current datasource key, extending AbstractRoutingDataSource for routing, configuring multiple datasources in application.yml, and optionally adding annotation‑driven switching and runtime datasource registration.
When a business requirement needs to read from multiple databases and write into a target database, developers often turn to dynamic datasource solutions. The article first explains the concepts of ThreadLocal and AbstractRoutingDataSource , then provides a step‑by‑step implementation.
1. Introduction
ThreadLocal provides a separate variable instance for each thread, eliminating data‑consistency problems in concurrent environments. AbstractRoutingDataSource determines the current datasource by invoking determineCurrentLookupKey() before each query.
2. Code Implementation
2.1 ThreadLocal Helper
/**
* @author: jiangjs
* @description: ThreadLocal holder for datasource name
*/
public class DataSourceContextHolder {
private static final ThreadLocal
DATASOURCE_HOLDER = new ThreadLocal<>();
/** Set datasource name */
public static void setDataSource(String dataSourceName) {
DATASOURCE_HOLDER.set(dataSourceName);
}
/** Get current datasource name */
public static String getDataSource() {
return DATASOURCE_HOLDER.get();
}
/** Remove current datasource */
public static void removeDataSource() {
DATASOURCE_HOLDER.remove();
}
}2.2 Dynamic Routing Datasource
/**
* @author: jiangjs
* @description: Dynamic datasource based on AbstractRoutingDataSource
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Map
targetDataSourceMap;
public DynamicDataSource(DataSource defaultDataSource, Map
targetDataSources) {
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
this.targetDataSourceMap = targetDataSources;
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
/** Add new datasources at runtime */
public void createDataSource(List
dataSources) {
try {
if (CollectionUtils.isNotEmpty(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();
this.targetDataSourceMap.put(ds.getKey(), dataSource);
}
super.setTargetDataSources(this.targetDataSourceMap);
super.afterPropertiesSet();
}
} catch (ClassNotFoundException | SQLException e) {
log.error("---程序报错---:{}", e.getMessage());
}
}
public boolean existsDataSource(String key) {
return Objects.nonNull(this.targetDataSourceMap.get(key));
}
}2.3 application.yml Configuration
# datasource configuration
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 15
min-idle: 15
max-active: 200
max-wait: 60000
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false2.4 Using the Dynamic Datasource
Controller method that switches datasource by path variable:
@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName) {
DataSourceContextHolder.setDataSource(datasourceName);
TestUser testUser = testUserMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return testUser.getUserName();
}2.5 Annotation‑Based Switching (Optional)
Define @DS annotation:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
String value() default "master";
}Aspect that applies the annotation:
@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();
}
}
}3. Testing Results
Requests to /getData.do/master return data from the master database, while /getData.do/slave returns data from the slave database, confirming that the routing works correctly.
4. Dynamic Addition of Datasources
By storing datasource definitions in a table ( test_db_info ) and loading them at application startup, new datasources can be added without restarting the service. The LoadDataSourceRunner reads the table, builds DataSourceEntity objects, and calls dynamicDataSource.createDataSource() to register them.
4.1 Sample Runner
@Component
public class LoadDataSourceRunner implements CommandLineRunner {
@Resource
private DynamicDataSource dynamicDataSource;
@Resource
private TestDbInfoMapper testDbInfoMapper;
@Override
public void run(String... args) throws Exception {
List
testDbInfos = testDbInfoMapper.selectList(null);
if (CollectionUtils.isNotEmpty(testDbInfos)) {
List
ds = new ArrayList<>();
for (TestDbInfo info : testDbInfos) {
DataSourceEntity entity = new DataSourceEntity();
BeanUtils.copyProperties(info, entity);
entity.setKey(info.getName());
ds.add(entity);
}
dynamicDataSource.createDataSource(ds);
}
}
}After the application starts, the newly added datasource becomes immediately usable through the same routing mechanisms.
5. Conclusion
The tutorial shows how to replace third‑party dynamic datasource starters with a lightweight custom implementation based on ThreadLocal and AbstractRoutingDataSource , how to simplify usage with an annotation, and how to add datasources at runtime, providing a flexible solution for multi‑database scenarios in Spring Boot.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.