Implementing Dynamic Data Source Switching in Spring Boot with ThreadLocal and AbstractRoutingDataSource
This tutorial demonstrates how to build a custom dynamic data source solution in Spring Boot by combining ThreadLocal and AbstractRoutingDataSource, covering the implementation of context holders, routing logic, configuration, testing, annotation‑driven switching, and runtime addition of new data sources.
1 Introduction
When a business requirement needs to read data from multiple databases and write back to the current one, dynamic data source switching becomes necessary. Instead of using the dynamic-datasource-spring-boot-starter (which was unavailable due to project constraints), we implement the functionality ourselves using ThreadLocal and AbstractRoutingDataSource .
2 Code Implementation
2.1 Implement ThreadLocal
We create a holder class that provides set , get and remove methods for the current thread's data source name.
/**
* @author: jiangjs
* @description:
* @date: 2023/7/27 11:21
*/
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 Implement AbstractRoutingDataSource
A custom DynamicDataSource extends Spring's AbstractRoutingDataSource and delegates the lookup key to the ThreadLocal holder.
/**
* @author: jiangjs
* @description: Implement dynamic data source routing
* @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 Configure Databases
Database connections are defined in application.yml and bound to beans in a configuration class.
# application.yml snippet
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
# pool settings omitted for brevity
/**
* @author: jiangjs
* @description: DataSource configuration
* @date: 2023/7/27 11:34
*/
@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 Testing the Switch
Two tables test_user are created in master and slave databases. A controller method sets the desired data source via the holder, performs a query, and then clears 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();
}Running the endpoint with master returns the master record, while slave returns the slave record, confirming the dynamic routing works.
2.5 Optimizations
2.5.1 Annotation‑Based Switching
Define a custom @DS annotation and an AOP aspect that sets the data source before method execution and removes it afterward.
/**
* @author: jiangjs
*/
@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 example:
@GetMapping("/getMasterData.do")
public String getMasterData(){
TestUser testUser = testUserMapper.selectOne(null);
return testUser.getUserName();
}
@GetMapping("/getSlaveData.do")
@DS("slave")
public String getSlaveData(){
TestUser testUser = testUserMapper.selectOne(null);
return testUser.getUserName();
}2.5.2 Dynamic Addition of Data Sources
Define a DataSourceEntity to hold connection parameters, extend DynamicDataSource with a createDataSource method that validates and registers new data sources at runtime, and expose a CommandLineRunner that loads entries from a database table on startup.
@Data
@Accessors(chain = true)
public class DataSourceEntity {
private String url;
private String userName;
private String passWord;
private String driverClassName;
private String key;
} /**
* @author: jiangjs
*/
@Slf4j
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();
}
public Boolean 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();
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));
}
} @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 data sources defined in the test_db_info table are automatically registered, allowing subsequent API calls to switch to them just like the static master/slave sources.
Conclusion
The article walks through a complete solution for dynamic data source routing in Spring Boot, from low‑level ThreadLocal handling to high‑level annotation‑driven switching and runtime addition of new data sources, providing a solid reference for developers facing multi‑database scenarios.
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.