Elegant Dynamic Data Source Switching in SpringBoot Using ThreadLocal and AbstractRoutingDataSource

This article walks through building an elegant dynamic data‑source switch in SpringBoot by combining ThreadLocal with AbstractRoutingDataSource, covering the context holder, custom routing class, YAML configuration, testing, annotation‑driven switching, and runtime addition of new data sources.

Java Companion
Java Companion
Java Companion
Elegant Dynamic Data Source Switching in SpringBoot Using ThreadLocal and AbstractRoutingDataSource

Problem

Business logic requires reading from multiple databases and writing back to the current one, so a dynamic data‑source switch is needed. The built‑in dynamic-datasource-spring-boot-starter could not be used due to environment constraints, so a custom solution based on ThreadLocal and AbstractRoutingDataSource was implemented.

ThreadLocal context holder

A utility class stores the current data‑source name in a thread‑local variable.

public class DataSourceContextHolder {
    private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();

    /** Set the current data source */
    public static void setDataSource(String dataSourceName) {
        DATASOURCE_HOLDER.set(dataSourceName);
    }

    /** Get the current data source */
    public static String getDataSource() {
        return DATASOURCE_HOLDER.get();
    }

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

Dynamic routing data source

The class extends AbstractRoutingDataSource and delegates the lookup key to the thread‑local holder.

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://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

Bean registration

A configuration class creates beans for the master and slave data sources, builds the DynamicDataSource, and marks it as @Primary so that Spring injects it wherever a DataSource is required.

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

Testing the switch

Two identical tables test_user (column user_name) are created in the master and slave databases, each containing a single row with values 'master' and 'slave'. A controller method receives a path variable {datasourceName}, sets the thread‑local data source, queries the table, and then removes the thread‑local value.

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

Calling the endpoint with master returns master; calling it with slave returns slave, confirming that the dynamic switch works.

master result
master result
slave result
slave result

Annotation‑based switching

To avoid manually invoking DataSourceContextHolder.setDataSource() in every service, a custom annotation @DS and an AOP aspect are introduced. The aspect intercepts methods annotated with @DS, extracts the annotation value, sets the thread‑local data source before method execution, and removes it afterwards.

@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 methods can now be annotated, e.g. @DS("slave"), to switch to the slave data source without explicit code.

@GetMapping("/getSlaveData.do")
@DS("slave")
public String getSlaveData() {
    TestUser testUser = testUserMapper.selectOne(null);
    return testUser.getUserName();
}

Dynamic addition of data sources

Business scenarios may require adding new data sources at runtime. A DataSourceEntity class stores connection parameters and a key.

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

The DynamicDataSource class is extended with a createDataSource(List<DataSourceEntity>) method that validates each entry, builds a DruidDataSource, and inserts it into the internal targetDataSourceMap. After updating the map, super.setTargetDataSources() and super.afterPropertiesSet() are called to refresh the routing configuration.

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();
                    targetDataSourceMap.put(ds.getKey(), dataSource);
                }
                super.setTargetDataSources(targetDataSourceMap);
                super.afterPropertiesSet();
            }
        } catch (ClassNotFoundException | SQLException e) {
            log.error("---程序报错---:{}", e.getMessage());
        }
    }

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

A CommandLineRunner implementation reads a table test_db_info that stores data‑source definitions, converts each row to a 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 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 data sources become available for the same dynamic routing mechanism. The test screenshot confirms that the runtime‑added slave data source is correctly used.

dynamic addition test
dynamic addition test

Additional notes

When starting the Spring Boot application, exclude the auto‑configuration of data sources to avoid circular‑dependency errors:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

The implementation demonstrates a complete solution for dynamic data‑source switching in Spring Boot, covering basic thread‑local handling, annotation‑driven routing, and runtime data‑source registration.

AOPSpringBootMyBatis-PlusAnnotationThreadLocalabstractroutingdatasourcedynamic-datasourceRuntime DataSource Registration
Java Companion
Written by

Java Companion

A highly professional Java public account

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.