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

This tutorial explains how to switch between multiple databases in a Spring Boot application by implementing ThreadLocal and AbstractRoutingDataSource, covering code implementation, configuration, testing, annotation-based switching, and dynamic addition of data sources with detailed examples and practical guidance.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Implement Dynamic Data Source Switching in Spring Boot with ThreadLocal and AbstractRoutingDataSource

Introduction

When a business requirement needs to read from multiple databases and write to the current one, dynamic data‑source switching is required. The solution uses a ThreadLocal together with a custom AbstractRoutingDataSource to emulate the behavior of the unavailable dynamic-datasource-spring-boot-starter.

Implementation

ThreadLocal holder

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

Dynamic routing datasource

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://<host1>: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://<host2>: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
@Configuration
public class DataSourceConfig {
    @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> dsMap = new HashMap<>();
        DataSource master = masterDataSource();
        dsMap.put("master", master);
        dsMap.put("slave", slaveDataSource());
        return new DynamicDataSource(master, dsMap);
    }
}

Basic switching test

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

Calling the endpoint with master or slave returns the username stored in the corresponding database, confirming the switch works.

master result
master result
slave result
slave result

Annotation‑based switching

@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 around(ProceedingJoinPoint point) throws Throwable {
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        DS ds = method.getAnnotation(DS.class);
        if (ds != null) {
            DataSourceContextHolder.setDataSource(ds.value());
        }
        try {
            return point.proceed();
        } finally {
            DataSourceContextHolder.removeDataSource();
        }
    }
}
@GetMapping("/getMasterData.do")
public String getMasterData() {
    return testUserMapper.selectOne(null).getUserName();
}

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

Runtime addition of data sources

@Data
@Accessors(chain = true)
public class DataSourceEntity {
    private String url;
    private String userName;
    private String passWord;
    private String driverClassName;
    private String key;
}
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();
                    this.targetDataSourceMap.put(ds.getKey(), dataSource);
                }
                super.setTargetDataSources(this.targetDataSourceMap);
                super.afterPropertiesSet();
            }
        } catch (ClassNotFoundException | SQLException e) {
            log.error("---program error---:{}", e.getMessage());
        }
    }

    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<TestDbInfo> infos = testDbInfoMapper.selectList(null);
        if (CollectionUtils.isNotEmpty(infos)) {
            List<DataSourceEntity> entities = new ArrayList<>();
            for (TestDbInfo info : infos) {
                DataSourceEntity entity = new DataSourceEntity();
                BeanUtils.copyProperties(info, entity);
                entity.setKey(info.getName());
                entities.add(entity);
            }
            dynamicDataSource.createDataSource(entities);
        }
    }
}

Table test_db_info stores external datasource definitions (id, url, username, password, driver_class_name, name). On application startup the runner reads this table and registers each entry as a new datasource, making it immediately available to the existing routing logic.

dynamic addition result
dynamic addition result

To avoid circular‑dependency errors, exclude Spring Boot’s auto‑configuration for data sources:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
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.

JavaSpringBootmybatis-plusThreadLocalDynamicDataSource
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

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.