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.

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

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

DataSourceContextHolder

provides 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

DynamicDataSource

extends 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: false

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

DataSourceEntity

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

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.

Spring BootDynamic Data Sourcemybatis-plusThreadLocalabstractroutingdatasource
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.