Implement Dynamic Data Source Switching in Spring Boot with ThreadLocal and AbstractRoutingDataSource
This tutorial explains how to switch between multiple databases in a Spring Boot application by implementing ThreadLocal and AbstractRoutingDataSource, covering code implementation, configuration, testing, annotation-based switching, and dynamic addition of data sources with detailed examples and practical guidance.
Introduction
When a business requirement needs to read from multiple databases and write to the current one, dynamic data‑source switching is required. The solution uses a ThreadLocal together with a custom AbstractRoutingDataSource to emulate the behavior of the unavailable dynamic-datasource-spring-boot-starter.
Implementation
ThreadLocal holder
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();
}
}Dynamic routing datasource
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();
}
}Database configuration
# application.yml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://<host1>: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://<host2>: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: false @Configuration
public class DataSourceConfig {
@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> dsMap = new HashMap<>();
DataSource master = masterDataSource();
dsMap.put("master", master);
dsMap.put("slave", slaveDataSource());
return new DynamicDataSource(master, dsMap);
}
}Basic switching test
@GetMapping("/getData.do/{datasourceName}")
public String getData(@PathVariable String datasourceName) {
DataSourceContextHolder.setDataSource(datasourceName);
TestUser user = testUserMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return user.getUserName();
}Calling the endpoint with master or slave returns the username stored in the corresponding database, confirming the switch works.
Annotation‑based switching
@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 around(ProceedingJoinPoint point) throws Throwable {
Method method = ((MethodSignature) point.getSignature()).getMethod();
DS ds = method.getAnnotation(DS.class);
if (ds != null) {
DataSourceContextHolder.setDataSource(ds.value());
}
try {
return point.proceed();
} finally {
DataSourceContextHolder.removeDataSource();
}
}
} @GetMapping("/getMasterData.do")
public String getMasterData() {
return testUserMapper.selectOne(null).getUserName();
}
@GetMapping("/getSlaveData.do")
@DS("slave")
public String getSlaveData() {
return testUserMapper.selectOne(null).getUserName();
}Runtime addition of data sources
@Data
@Accessors(chain = true)
public class DataSourceEntity {
private String url;
private String userName;
private String passWord;
private String driverClassName;
private String key;
} 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) {
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("---program error---:{}", e.getMessage());
}
}
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<TestDbInfo> infos = testDbInfoMapper.selectList(null);
if (CollectionUtils.isNotEmpty(infos)) {
List<DataSourceEntity> entities = new ArrayList<>();
for (TestDbInfo info : infos) {
DataSourceEntity entity = new DataSourceEntity();
BeanUtils.copyProperties(info, entity);
entity.setKey(info.getName());
entities.add(entity);
}
dynamicDataSource.createDataSource(entities);
}
}
}Table test_db_info stores external datasource definitions (id, url, username, password, driver_class_name, name). On application startup the runner reads this table and registers each entry as a new datasource, making it immediately available to the existing routing logic.
To avoid circular‑dependency errors, exclude Spring Boot’s auto‑configuration for data sources:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)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.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.
