Elegant Dynamic Data Source Switching in Spring Boot with ThreadLocal & AbstractRoutingDataSource

This tutorial demonstrates how to implement dynamic data source switching in Spring Boot using ThreadLocal and AbstractRoutingDataSource, covering configuration, annotation-based routing via AOP, and runtime data source registration from a database.

Architect's Guide
Architect's Guide
Architect's Guide
Elegant Dynamic Data Source Switching in Spring Boot with ThreadLocal & AbstractRoutingDataSource

When a business requirement demanded fetching data from multiple databases and writing to the current one, the author explored dynamic data source switching in Spring Boot. After the MyBatis-Plus dynamic-datasource-spring-boot-starter proved incompatible with the project environment, a custom solution using ThreadLocal and AbstractRoutingDataSource was built to replicate the starter's thread‑level data source routing.

Core Concepts

ThreadLocal

ThreadLocal

(thread local variable) solves concurrency‑induced data inconsistency by giving each thread its own variable copy. This isolates threads, increases memory slightly, but greatly reduces synchronization overhead and concurrency control complexity. Key points:

Role: shared within a thread, isolated across threads.

Principle: when storing a value, the current thread instance is used as the key in a map held by the thread object.

AbstractRoutingDataSource

This Spring class selects the current data source based on user‑defined rules. Its abstract method determineCurrentLookupKey() is invoked before each database operation to decide which data source to use.

Implementation Steps

2.1 ThreadLocal Holder – DataSourceContextHolder

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/7/27 11:21
 **/
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();
    }
}

2.2 Dynamic Data Source – DynamicDataSource

/**
 * @author: jiangjs
 * @description: Implements dynamic data source, routes 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();
    }
}

The constructor sets the default data source and a map of target data sources (key = data source name, value = DataSource instance).

2.3 Data Source Configuration

application.yml

defines two Druid data sources ( master and slave) with connection pool settings. A @Configuration class DateSourceConfig uses @ConfigurationProperties to bind each to a DataSource bean, then builds a DynamicDataSource bean (marked @Primary) that holds both in a map.

@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<Object,Object> dataSourceMap = new HashMap<>();
        DataSource defaultDataSource = masterDataSource();
        dataSourceMap.put("master", defaultDataSource);
        dataSourceMap.put("slave", slaveDataSource());
        return new DynamicDataSource(defaultDataSource, dataSourceMap);
    }
}

Note: Exclude Spring Boot's auto‑configured data source to avoid circular dependency:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

2.4 Testing the Basic Switch

Two test databases each contain a test_user table with a single user_name column. Master holds 'master', slave holds 'slave'. A controller method accepts a data source name, sets it via DataSourceContextHolder.setDataSource(), queries via MyBatis‑Plus, then removes 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();
}

Results confirm that passing master returns master and passing slave returns slave.

Test result for master data source
Test result for master data source
Test result for slave data source
Test result for slave data source

The author notes that MyBatis‑Plus's DynamicDataSourceContextHolder uses a stack internally to support nested data source switching, a feature not replicated here.

Optimization: Annotation‑Driven Switching

2.5.1.1 Define @DS Annotation

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
    String value() default "master";
}

2.5.1.2 AOP Aspect

@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();
        }
    }
}

The aspect intercepts methods annotated with @DS, reads the value, sets the data source, proceeds, and ensures cleanup in finally.

2.5.1.3 Annotation Test

Two endpoints: /getMasterData.do (no annotation, defaults to master) and /getSlaveData.do (annotated with @DS("slave")). Both return the expected user name.

Annotation test master result
Annotation test master result
Annotation test slave result
Annotation test slave result

Dynamic Data Source Addition at Runtime

2.5.2.1 Data Source Entity

@Data
@Accessors(chain = true)
public class DataSourceEntity {
    private String url;
    private String userName;
    private String passWord;
    private String driverClassName;
    private String key; // map key in DynamicDataSource
}

2.5.2.2 Enhanced DynamicDataSource

The class now retains a reference to the target data source map and exposes createDataSource(List<DataSourceEntity>) and existsDataSource(String).

@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();
    }

    public void createDataSource(List<DataSourceEntity> dataSources){
        try {
            if (CollectionUtils.isNotEmpty(dataSources)){
                for (DataSourceEntity ds : dataSources) {
                    // Validate connectivity
                    Class.forName(ds.getDriverClassName());
                    DriverManager.getConnection(ds.getUrl(), ds.getUserName(), ds.getPassWord());
                    // Build Druid data source
                    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));
    }
}

2.5.2.3 Startup Loader

A CommandLineRunner bean reads a test_db_info table (columns: id, url, username, password, driver_class_name, name) at startup, maps rows to DataSourceEntity, and calls dynamicDataSource.createDataSource().

@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);
        }
    }
}

2.5.2.4 Dynamic Addition Test

After inserting the former slave database into test_db_info with name add_slave, the application loads it on startup. Calling the test endpoint with add_slave returns the slave data, proving the new data source was registered dynamically.

Dynamic data source addition test result
Dynamic data source addition test result

The article concludes that this approach provides a deeper understanding of dynamic data source switching and encourages readers to experiment further.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AOPSpring BootDynamic Data SourceMyBatis-PlusThreadLocalDruidDataSource RoutingAbstractRoutingDataSource
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.