1. 概述
本文将深入探讨 Spring 的 AbstractRoutingDatasource
,实现根据当前上下文动态路由到实际数据源的功能。
通过这种方式,我们可以将数据源查找逻辑完全从数据访问代码中剥离,让代码更清爽、更解耦。
2. Maven 依赖
首先在 pom.xml
中添加核心依赖:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
</dependencies>
最新版本可在 Maven 仓库 查询
如果是 Spring Boot 项目,使用 starter 更简单:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>test</scope>
</dependency>
</dependencies>
3. 数据源上下文
AbstractRoutingDatasource
需要上下文信息来确定实际数据源。这个上下文可以是任何对象,但枚举是最佳实践。
我们定义 ClientDatabase
枚举作为上下文标识:
public enum ClientDatabase {
CLIENT_A, CLIENT_B
}
实际场景中,上下文设计要贴合业务。例如:
- 多租户场景:用租户 ID 作为上下文
- 环境隔离:用
PRODUCTION
/DEVELOPMENT
/TESTING
枚举
4. 上下文持有者
上下文持有者使用 ThreadLocal
存储当前线程的上下文,必须实现三个核心操作:
public class ClientDatabaseContextHolder {
private static ThreadLocal<ClientDatabase> CONTEXT = new ThreadLocal<>();
public static void set(ClientDatabase clientDatabase) {
Assert.notNull(clientDatabase, "clientDatabase cannot be null");
CONTEXT.set(clientDatabase);
}
public static ClientDatabase getClientDatabase() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
⚠️ 关键点:
- 必须用
ThreadLocal
保证线程隔离 - 跨数据源操作时,上下文必须与当前线程绑定
- 事务操作中尤其要确保上下文一致性
5. 数据源路由器
创建路由器继承 AbstractRoutingDatasource
,重写 determineCurrentLookupKey()
方法:
public class ClientDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return ClientDatabaseContextHolder.getClientDatabase();
}
}
✅ Spring 会自动处理:
- 从上下文持有者获取当前数据源标识
- 根据标识查找对应
DataSource
- 返回实际数据源连接
6. 配置实现
6.1 基础配置
需要配置数据源映射关系和默认数据源:
@Configuration
public class RoutingTestConfiguration {
@Bean
public ClientService clientService() {
return new ClientService(new ClientDao(clientDatasource()));
}
@Bean
public DataSource clientDatasource() {
Map<Object, Object> targetDataSources = new HashMap<>();
DataSource clientADatasource = clientADatasource();
DataSource clientBDatasource = clientBDatasource();
targetDataSources.put(ClientDatabase.CLIENT_A, clientADatasource);
targetDataSources.put(ClientDatabase.CLIENT_B, clientBDatasource);
ClientDataSourceRouter router = new ClientDataSourceRouter();
router.setTargetDataSources(targetDataSources);
router.setDefaultTargetDataSource(clientADatasource); // 默认数据源
return router;
}
// ... 数据源创建方法见下节
}
6.2 Spring Boot 配置
在 application.properties
定义多数据源属性:
# CLIENT_A 数据源配置
client-a.datasource.name=CLIENT_A
client-a.datasource.script=schema_a.sql
# CLIENT_B 数据源配置
client-b.datasource.name=CLIENT_B
client-b.datasource.script=schema_b.sql
创建配置类绑定属性:
@Component
@ConfigurationProperties(prefix = "client-a.datasource")
public class ClientADetails {
private String name;
private String script;
// getters & setters
}
动态构建数据源:
@Autowired
private ClientADetails clientADetails;
@Autowired
private ClientBDetails clientBDetails;
private DataSource clientADatasource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.setName(clientADetails.getName())
.addScript(clientADetails.getScript())
.build();
}
private DataSource clientBDatasource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.setName(clientBDetails.getName())
.addScript(clientBDetails.getScript())
.build();
}
7. 使用方式
在业务层使用时遵循三步操作:
public class ClientService {
private ClientDao clientDao;
public String getClientName(ClientDatabase clientDb) {
// 1. 设置上下文
ClientDatabaseContextHolder.set(clientDb);
try {
// 2. 执行数据操作
return this.clientDao.getClientName();
} finally {
// 3. 清理上下文(必须!)
ClientDatabaseContextHolder.clear();
}
}
}
⚠️ 踩坑提醒:
- 上下文是线程绑定的,异步场景需特殊处理
- 务必在
finally
块清理上下文,避免内存泄漏 - 事务操作中上下文必须保持一致
进阶技巧:可用 AOP 统一处理上下文设置和清理,避免模板代码
8. 总结
通过 AbstractRoutingDatasource
我们实现了:
✅ 动态数据源路由
✅ 业务代码与数据源解耦
✅ 线程安全的上下文管理
这种方案特别适合:
- 多租户系统
- 读写分离场景
- 分库分表需求
完整示例代码见 GitHub 仓库。