Dynamic Data Source Switching in Spring Boot Using ThreadLocal & AbstractRoutingDataSource
This tutorial explains how to implement dynamic data source switching in Spring Boot by combining ThreadLocal with AbstractRoutingDataSource, covering custom context holders, configuration, annotation‑driven switching, dynamic addition of data sources, and complete code examples with test results.
1 Introduction
When a business requirement needs to read from different databases and write to the current one, data source switching is required. The author first tried the dynamic-datasource-spring-boot-starter but it failed, so a custom solution using ThreadLocal and AbstractRoutingDataSource was implemented.
2 Code Implementation
2.1 Implement ThreadLocal
DataSourceContextHolderprovides get, set and remove methods to store the current thread's data source name in a ThreadLocal variable.
/**
* @author: jiangjs
* @description:
* @date: 2023/7/27 11:21
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* Set data source name
* @param dataSourceName data source name
*/
public static void setDataSource(String dataSourceName){
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* Get current thread's data source name
* @return data source name
*/
public static String getDataSource(){
return DATASOURCE_HOLDER.get();
}
/**
* Remove current data source
*/
public static void removeDataSource(){
DATASOURCE_HOLDER.remove();
}
}2.2 Implement AbstractRoutingDataSource
DynamicDataSourceextends AbstractRoutingDataSource and overrides determineCurrentLookupKey to return the data source name from DataSourceContextHolder.
/**
* @author: jiangjs
* @description: Implement dynamic data source, route to different data sources 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();
}
}2.3 Configure Databases
The application.yml defines master and slave data sources using Druid.
# Set data sources
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
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: ""
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
connection-properties: false2.4 Test
SQL scripts create a test_user table with a single column user_name in both master and slave databases, then insert distinct rows.
create table test_user(
user_name varchar(255) not null comment '用户名'
); insert into test_user (user_name) value ('master'); insert into test_user (user_name) value ('slave');A controller method sets the data source, queries the table, and removes the data source.
@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 show different values are returned when master or slave is passed.
2.5 Optimization and Extension
2.5.1 Annotation‑Based Data Source Switching
Define a @DS annotation and an AOP aspect that sets the data source before method execution and clears it afterwards.
/**
* @author: jiangjs
* @description:
* @date: 2023/7/27 14:39
*/
@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 examples:
@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 Data Source Addition
DataSourceEntityholds connection parameters and a key used in the dynamic map.
/**
* @author: jiangjs
* @description: Data source entity
* @date: 2023/7/27 15:55
*/
@Data
@Accessors(chain = true)
public class DataSourceEntity {
private String url;
private String userName;
private String passWord;
private String driverClassName;
private String key;
}The extended DynamicDataSource maintains a targetDataSourceMap and provides createDataSource to validate, instantiate, and add new Druid data sources at runtime.
@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();
}
/**
* Add data sources dynamically
*/
public void createDataSource(List<DataSourceEntity> 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));
}
}A CommandLineRunner loads rows from a test_db_info table, converts them to DataSourceEntity, and calls dynamicDataSource.createDataSource at application startup.
@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);
}
}
}After the application starts, the newly added data sources are available for queries just like the static ones.
Note: Exclude DataSourceAutoConfiguration when starting the Spring Boot application to avoid circular dependency errors. <code>@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)</code>
Overall, the article demonstrates a complete workflow for custom dynamic data source routing, annotation‑driven switching, and runtime addition of new data sources in a Spring Boot environment.
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.
