Backend Development 15 min read

Implementing Dynamic Data Source Switching in Spring Boot with ThreadLocal and AbstractRoutingDataSource

This article demonstrates how to build a custom dynamic data‑source solution for Spring Boot by using ThreadLocal to store the current datasource key, extending AbstractRoutingDataSource for routing, configuring multiple datasources in application.yml, and optionally adding annotation‑driven switching and runtime datasource registration.

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

When a business requirement needs to read from multiple databases and write into a target database, developers often turn to dynamic datasource solutions. The article first explains the concepts of ThreadLocal and AbstractRoutingDataSource , then provides a step‑by‑step implementation.

1. Introduction

ThreadLocal provides a separate variable instance for each thread, eliminating data‑consistency problems in concurrent environments. AbstractRoutingDataSource determines the current datasource by invoking determineCurrentLookupKey() before each query.

2. Code Implementation

2.1 ThreadLocal Helper

/**
 * @author: jiangjs
 * @description: ThreadLocal holder for datasource name
 */
public class DataSourceContextHolder {
    private static final ThreadLocal
DATASOURCE_HOLDER = new ThreadLocal<>();

    /** Set datasource name */
    public static void setDataSource(String dataSourceName) {
        DATASOURCE_HOLDER.set(dataSourceName);
    }

    /** Get current datasource name */
    public static String getDataSource() {
        return DATASOURCE_HOLDER.get();
    }

    /** Remove current datasource */
    public static void removeDataSource() {
        DATASOURCE_HOLDER.remove();
    }
}

2.2 Dynamic Routing Datasource

/**
 * @author: jiangjs
 * @description: Dynamic datasource based on AbstractRoutingDataSource
 */
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();
    }

    /** Add new datasources at runtime */
    public void 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();
            }
        } catch (ClassNotFoundException | SQLException e) {
            log.error("---程序报错---:{}", e.getMessage());
        }
    }

    public boolean existsDataSource(String key) {
        return Objects.nonNull(this.targetDataSourceMap.get(key));
    }
}

2.3 application.yml Configuration

# datasource configuration
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
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: false

2.4 Using the Dynamic Datasource

Controller method that switches datasource by path variable:

@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName) {
    DataSourceContextHolder.setDataSource(datasourceName);
    TestUser testUser = testUserMapper.selectOne(null);
    DataSourceContextHolder.removeDataSource();
    return testUser.getUserName();
}

2.5 Annotation‑Based Switching (Optional)

Define @DS annotation:

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

Aspect that applies the annotation:

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

3. Testing Results

Requests to /getData.do/master return data from the master database, while /getData.do/slave returns data from the slave database, confirming that the routing works correctly.

4. Dynamic Addition of Datasources

By storing datasource definitions in a table ( test_db_info ) and loading them at application startup, new datasources can be added without restarting the service. The LoadDataSourceRunner reads the table, builds DataSourceEntity objects, and calls dynamicDataSource.createDataSource() to register them.

4.1 Sample Runner

@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 info : testDbInfos) {
                DataSourceEntity entity = new DataSourceEntity();
                BeanUtils.copyProperties(info, entity);
                entity.setKey(info.getName());
                ds.add(entity);
            }
            dynamicDataSource.createDataSource(ds);
        }
    }
}

After the application starts, the newly added datasource becomes immediately usable through the same routing mechanisms.

5. Conclusion

The tutorial shows how to replace third‑party dynamic datasource starters with a lightweight custom implementation based on ThreadLocal and AbstractRoutingDataSource , how to simplify usage with an annotation, and how to add datasources at runtime, providing a flexible solution for multi‑database scenarios in Spring Boot.

JavadatabaseSpringBootThreadLocalMyBatisPlusAbstractRoutingDataSourceDynamicDataSource
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.