Master Dynamic Multi-DataSource Switching in Spring Boot 2.7
This tutorial explains why enterprise applications need multiple data sources, outlines the scenarios such as business isolation, migration, performance and maintainability, and provides a step‑by‑step implementation of dynamic data source routing in Spring Boot using custom context holders, routing classes, annotations, aspects, configuration, and web‑request control.
Environment
SpringBoot 2.7.16
1. Introduction
Complex enterprise applications often require multiple data sources, each serving a different business module. Common scenarios include:
Business isolation : Separate databases ensure data integrity and security across modules.
Data migration and backup : Easily move or back up data between sources.
Performance and scalability : Deploy modules on different databases for load balancing and horizontal scaling; switch when a database becomes a bottleneck.
Maintainability : Switch to another source when a database fails, and simplify upgrades.
Spring provides dynamic data source switching via AbstractRoutingDataSource , which selects the appropriate source at runtime.
2. Practical Example
2.1 Custom Multi‑DataSource
Thread‑local holder for the current data source:
<code>public class DataSourceContextHolder {
private static final ThreadLocal<String> HOLDER = new InheritableThreadLocal<>();
public static void set(String key) { HOLDER.set(key); }
public static String get() { return HOLDER.get(); }
public static void clear() { HOLDER.remove(); }
}</code>Routing datasource extending AbstractRoutingDataSource :
<code>public class PackRoutingDataSource extends AbstractRoutingDataSource {
private final String defaultDs;
public PackRoutingDataSource(String defaultDs) { this.defaultDs = defaultDs; }
@Override
protected Object determineCurrentLookupKey() {
String key = DataSourceContextHolder.get();
if (key == null || "".equals(key)) { key = this.defaultDs; }
return key;
}
}</code>Properties class inheriting from Spring Boot's DataSourceProperties :
<code>public class PackDataSourceProperties extends DataSourceProperties { }</code>Configuration holder for all data sources:
<code>@Component
@ConfigurationProperties(prefix = "pack.datasource")
public class MultiDataSourceProperties {
private Map<String, PackDataSourceProperties> config = new HashMap<>();
private String defaultDs;
}</code>Spring configuration that creates the routing datasource and registers target datasources:
<code>@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource(MultiDataSourceProperties properties) {
PackRoutingDataSource dataSource = new PackRoutingDataSource(properties.getDefaultDs());
Map<Object, Object> targetDataSources = new HashMap<>();
properties.getConfig().forEach((key, props) -> {
targetDataSources.put(key, createDataSource(props, HikariDataSource.class));
});
dataSource.setTargetDataSources(targetDataSources);
return dataSource;
}
private static <T> T createDataSource(PackDataSourceProperties props, Class<? extends DataSource> type) {
return (T) props.initializeDataSourceBuilder().type(type).build();
}
}</code>Custom Annotation and Aspect
Annotation to specify a datasource:
<code>@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Ds { String value() default ""; }</code>Aspect that switches the datasource before method execution and clears the context afterwards:
<code>@Component
@Aspect
public class DataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
@Resource
private MultiDataSourceProperties properties;
@Pointcut("@annotation(ds)")
private void dsPointcut(Ds ds) {}
@Around("dsPointcut(ds)")
public Object around(ProceedingJoinPoint pjp, Ds ds) throws Throwable {
String key = ds.value();
if (key == null || "".equals(key)) { key = this.properties.getDefaultDs(); }
logger.info("current datasource【{}】", key);
DataSourceContextHolder.set(key);
try { return pjp.proceed(); }
finally { DataSourceContextHolder.clear(); }
}
}</code>YAML Configuration
<code>pack:
datasource:
defaultDs: ds1
config:
ds1:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ds1?serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&useSSL=false
username: root
password: xxoo
type: com.zaxxer.hikari.HikariDataSource
ds2:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ds2?serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&useSSL=false
username: root
password: xxoo
type: com.zaxxer.hikari.HikariDataSource</code>Testing with JPA
Entity definition:
<code>@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}</code>Repository and service using the @Ds annotation:
<code>public interface UserRepository extends JpaRepository<User, Long> {}
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) { this.userRepository = userRepository; }
public User queryUserDefault(Long id) { return userRepository.findById(id).orElse(null); }
@Ds("ds1")
public User queryUserDs1(Long id) { return userRepository.findById(id).orElse(null); }
@Ds("ds2")
public User queryUserDs2(Long id) { return userRepository.findById(id).orElse(null); }
@Transactional
@Ds
public void saveUser(User user) { userRepository.save(user); }
}</code>Test cases:
<code>@Resource
private UserService us;
@Test
public void testSave() {
User user = new User();
user.setName("张三");
us.saveUser(user);
}
@Test
public void testQuery() {
System.out.println(us.queryUserDs1(1L));
System.out.println(us.queryUserDs2(1L));
System.out.println(us.queryUserDefault(1L));
}</code>2.3 Controlling via Web Request
Interceptor that reads a custom header x-ds to set the datasource for the current request:
<code>public class RequestDataSourceInterceptor implements HandlerInterceptor {
public static final ThreadLocal<String> HOLDER = new InheritableThreadLocal<>();
private static final String HEADER_KEY = "x-ds";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
HOLDER.set(request.getHeader(HEADER_KEY));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
HOLDER.remove();
}
}</code>Aspect is updated to prioritize the header value before falling back to the annotation or default datasource:
<code>@Around("dsPointcut(ds)")
public Object around(ProceedingJoinPoint pjp, Ds ds) throws Throwable {
String key = RequestDataSourceInterceptor.HOLDER.get();
if (key == null || "".equals(key)) { key = ds.value();
if (key == null || "".equals(key)) { key = this.properties.getDefaultDs(); }
}
// set context and proceed as before
DataSourceContextHolder.set(key);
try { return pjp.proceed(); }
finally { DataSourceContextHolder.clear(); }
}</code>Example controller that uses the service:
<code>@RestController
public class UserController {
@Resource
private UserService us;
@GetMapping("/{id}")
public User query(@PathVariable("id") Long id) {
return us.queryUserDs1(id);
}
}</code>The response correctly returns data from ds2 , confirming the interceptor works.
2.4 Underlying Mechanism of AbstractRoutingDataSource
<code>public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Map<Object, DataSource> resolvedDataSources;
public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); }
protected DataSource determineTargetDataSource() {
Object lookupKey = determineCurrentLookupKey();
DataSource ds = this.resolvedDataSources.get(lookupKey);
if (ds == null && (this.lenientFallback || lookupKey == null)) { ds = this.resolvedDefaultDataSource; }
if (ds == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); }
return ds;
}
@Override
public void afterPropertiesSet() {
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource ds = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, ds);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}</code>The overall implementation is straightforward and demonstrates how to achieve dynamic multi‑datasource routing in a Spring Boot backend.
Hope this guide helps you master multi‑datasource switching.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.