Backend Development 16 min read

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.

Architect's Guide
Architect's Guide
Architect's Guide
Implementing Dynamic Data Source Switching in Spring Boot with ThreadLocal and AbstractRoutingDataSource

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.

JavaMyBatisSpringBootthreadlocalDynamicDataSource
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

login 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.