Implementing Dynamic Data Source Switching in Spring Boot Using ThreadLocal and AbstractRoutingDataSource
This article explains how to build a custom dynamic data‑source solution for Spring Boot by leveraging ThreadLocal and AbstractRoutingDataSource, provides complete Java code for context holders, routing datasource, configuration, annotation‑driven switching, and runtime addition of new data sources.
The author needed to fetch data from multiple databases within a single business request and therefore implemented a custom dynamic data‑source mechanism because the existing dynamic-datasource-spring-boot-starter could not be used.
1. Introduction
ThreadLocal provides a per‑thread variable to avoid data inconsistency in concurrent environments, while AbstractRoutingDataSource selects the current datasource based on a lookup key before each query.
2. Code Implementation
2.1 ThreadLocal holder
/**
* @author: jiangjs
* @description:
* @date: 2023/7/27 11:21
*/
public class DataSourceContextHolder {
// Thread‑local variable storing the datasource name
private static final ThreadLocal
DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* Set datasource name
*/
public static void setDataSource(String dataSourceName) {
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* Get current thread's datasource name
*/
public static String getDataSource() {
return DATASOURCE_HOLDER.get();
}
/**
* Remove the datasource from the current thread
*/
public static void removeDataSource() {
DATASOURCE_HOLDER.remove();
}
}2.2 AbstractRoutingDataSource subclass
/**
* @author: jiangjs
* @description: Dynamic datasource routing implementation
* @date: 2023/7/27 11:18
*/
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 Spring configuration (application.yml and @Configuration class)
# application.yml snippet
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
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
... @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
dataSourceMap = new HashMap<>();
DataSource defaultDataSource = masterDataSource();
dataSourceMap.put("master", defaultDataSource);
dataSourceMap.put("slave", slaveDataSource());
return new DynamicDataSource(defaultDataSource, dataSourceMap);
}
}2.4 Controller usage
@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
DataSourceContextHolder.setDataSource(datasourceName);
TestUser testUser = testUserMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return testUser.getUserName();
}The above endpoint demonstrates manual switching by setting the datasource name in ThreadLocal before the query.
2.5 Annotation‑driven switching
Define a custom @DS annotation and an AOP aspect that sets and clears the datasource automatically.
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
String value() default "master";
} @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();
}
}
}Controller methods can now use @DS("slave") to switch to the slave datasource without manual code.
2.6 Dynamic addition of new datasources at runtime
A DataSourceEntity class stores connection parameters, and DynamicDataSource is extended with a createDataSource(List ) method that builds a DruidDataSource for each entry, validates the connection, and updates the internal map.
@Data
@Accessors(chain = true)
public class DataSourceEntity {
private String url;
private String userName;
private String passWord;
private String driverClassName;
private String key;
} 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());
}
}A CommandLineRunner component reads datasource definitions from a table ( test_db_info ) at application startup and calls dynamicDataSource.createDataSource(...) to register them.
@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 testDbInfo : testDbInfos) {
DataSourceEntity sourceEntity = new DataSourceEntity();
BeanUtils.copyProperties(testDbInfo, sourceEntity);
sourceEntity.setKey(testDbInfo.getName());
ds.add(sourceEntity);
}
dynamicDataSource.createDataSource(ds);
}
}
}After the application starts, the newly added datasources can be accessed via the same @DS annotation or the manual DataSourceContextHolder.setDataSource(...) approach, as demonstrated by the test results shown in the article.
Overall, the article provides a complete, production‑ready guide for implementing, testing, and extending dynamic datasource switching in a Spring Boot backend project.
IT Xianyu
We share common IT technologies (Java, Web, SQL, etc.) and practical applications of emerging software development techniques. New articles are posted daily. Follow IT Xianyu to stay ahead in tech. The IT Xianyu series is being regularly updated.
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.