Implementing Dynamic Data Source Routing with Spring Boot
Add Required Dependencies
Add the following dependencies to your project's pom.xml file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
Configure Multiple Data Sources
Define your data source connections in application.yml:
spring:
datasource:
main:
url: jdbc:mysql://localhost:3306/main_db
username: root
password: your_db_password
driver-class-name: com.mysql.cj.jdbc.Driver
replica:
url: jdbc:mysql://localhost:3306/replica_db
username: root
password: your_db_password
driver-class-name: com.mysql.cj.jdbc.Driver
Define Data Source Configuration Class
Create a configuration class to register individual data source beans:
package com.example.dynamicds.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class MultiDataSourceConfig {
@Primary
@Bean(name = "mainDataSource")
@ConfigurationProperties("spring.datasource.main")
public HikariDataSource mainDataSource() {
return new HikariDataSource();
}
@Bean(name = "replicaDataSource")
@ConfigurationProperties("spring.datasource.replica")
public HikariDataSource replicaDataSource() {
return new HikariDataSource();
}
}
Implemant Data Source Context Holder
Create a thread-local storage class to track the data source associated with the current request thread:
package com.example.dynamicds.routing;
public class DataSourceContext {
private static final ThreadLocal<String> CURRENT_DATA_SOURCE = new ThreadLocal<>();
public static void setActiveDataSource(String dataSourceKey) {
CURRENT_DATA_SOURCE.set(dataSourceKey);
}
public static String getActiveDataSource() {
return CURRENT_DATA_SOURCE.get();
}
public static void clearActiveDataSource() {
CURRENT_DATA_SOURCE.remove();
}
}
Create Dynamic Routing Data Source
Extend Spring's AbstractRoutingDataSource to implement dynamic lookup of active data sources:
package com.example.dynamicds.routing;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContext.getActiveDataSource();
}
}
Register Dynamic Data Source Bean
Create a configuration class to wrap the individual data sources into the dynamic routing implementation:
package com.example.dynamicds.config;
import com.example.dynamicds.routing.DynamicDataSourceRouter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DynamicDataSourceConfig {
@Bean
public DynamicDataSourceRouter dynamicDataSource(
@Qualifier("mainDataSource") DataSource mainDataSource,
@Qualifier("replicaDataSource") DataSource replicaDataSource) {
DynamicDataSourceRouter router = new DynamicDataSourceRouter();
Map<Object, Object> dataSourceRegistry = new HashMap<>();
dataSourceRegistry.put("main", mainDataSource);
dataSourceRegistry.put("replica", replicaDataSource);
router.setTargetDataSources(dataSourceRegistry);
router.setDefaultTargetDataSource(mainDataSource);
return router;
}
@Bean
public PlatformTransactionManager transactionManager(DynamicDataSourceRouter dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
Define Routing Annotation
Create a custom annotation to mark methods or classes that need to use a specific data source:
package com.example.dynamicds.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UseDataSource {
String value();
}
Implement Routing Aspect
Create an AOP aspect to intercept annotated methods and switch the active data source before execution:
package com.example.dynamicds.aspect;
import com.example.dynamicds.annotation.UseDataSource;
import com.example.dynamicds.routing.DataSourceContext;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Order(-10)
@Component
public class DataSourceRoutingAspect {
@Before("@annotation(useDataSource)")
public void switchDataSourceBeforeExecution(UseDataSource useDataSource) {
DataSourceContext.setActiveDataSource(useDataSource.value());
}
@After("@annotation(useDataSource)")
public void clearDataSourceAfterExecution(UseDataSource useDataSource) {
DataSourceContext.clearActiveDataSource();
}
}
Use Dynamic Data Source in Business Logic
Add the @UseDataSource annotation to methods that require a specific data source:
package com.example.dynamicds.service;
import com.example.dynamicds.annotation.UseDataSource;
import com.example.dynamicds.entity.Customer;
import com.example.dynamicds.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
@UseDataSource("main")
public List<Customer> fetchAllCustomersFromMain() {
return customerRepository.findAll();
}
@UseDataSource("replica")
public List<Customer> fetchAllCustomersFromReplica() {
return customerRepository.findAll();
}
}
Verify Dynamic Routing Functionality
Write a test case to confirm data source switching works as expected:
package com.example.dynamicds;
import com.example.dynamicds.entity.Customer;
import com.example.dynamicds.service.CustomerService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class DynamicDataSourceRoutingTest {
@Autowired
private CustomerService customerService;
@Test
public void testDataSourceSwitching() {
List<Customer> mainCustomers = customerService.fetchAllCustomersFromMain();
System.out.println("Main datasource customer count: " + mainCustomers.size());
List<Customer> replicaCustomers = customerService.fetchAllCustomersFromReplica();
System.out.println("Replica datasource customer count: " + replicaCustomers.size());
}
}