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 会自动处理:

  1. 从上下文持有者获取当前数据源标识
  2. 根据标识查找对应 DataSource
  3. 返回实际数据源连接

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 仓库


原始标题:A Guide to Spring AbstractRoutingDatasource | Baeldung

« 上一篇: Java Weekly, 第204期