Spring Boot按租户分库多租户:如何取消默认数据源并延迟EntityManager创建?
问题描述
我正在评估采用按租户分库的多租户解决方案,参考Spring Data官方的多租户分库示例实现了基于AbstractRoutingDataSource的原型,但当前实现要求配置默认数据库——这仅仅是为了让Spring Boot(Spring Data JPA)能启动并初始化EntityManager。实现代码如下:
@Component public class TenantRoutingDatasource extends AbstractRoutingDataSource { @Autowired private TenantIdentifierResolver tenantIdentifierResolver; TenantRoutingDatasource() { setDefaultTargetDataSource(createEmbeddedDatabase("default")); HashMap<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("VMWARE", createEmbeddedDatabase("VMWARE")); targetDataSources.put("PIVOTAL", createEmbeddedDatabase("PIVOTAL")); setTargetDataSources(targetDataSources); } @Override protected String determineCurrentLookupKey() { return tenantIdentifierResolver.resolveCurrentTenantIdentifier(); } private EmbeddedDatabase createEmbeddedDatabase(String name) { return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).setName(name).addScript("manual-schema.sql") .build(); } }
请问是否有办法避免配置默认数据源?能否让Spring Boot延迟EntityManager的创建,直到仓库等组件调用时再执行?
解决方案
一、避免配置实际默认数据源
1. 用占位符+异常拦截替代实际默认数据源
你不需要配置真实的默认嵌入式数据库,而是可以给AbstractRoutingDataSource设置一个空的数据源占位符,同时在租户标识未找到时抛出明确的业务异常,防止无效请求继续执行:
修改后的TenantRoutingDatasource示例:
@Component public class TenantRoutingDatasource extends AbstractRoutingDataSource { @Autowired private TenantIdentifierResolver tenantIdentifierResolver; TenantRoutingDatasource() { // 设置空数据源占位符,仅用于Spring初始化流程 setDefaultTargetDataSource(new DataSource() { @Override public Connection getConnection() throws SQLException { throw new IllegalStateException("未指定有效的租户标识"); } @Override public Connection getConnection(String username, String password) throws SQLException { throw new IllegalStateException("未指定有效的租户标识"); } @Override public <T> T unwrap(Class<T> iface) throws SQLException { throw new UnsupportedOperationException(); } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; } }); HashMap<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("VMWARE", createEmbeddedDatabase("VMWARE")); targetDataSources.put("PIVOTAL", createEmbeddedDatabase("PIVOTAL")); setTargetDataSources(targetDataSources); } @Override protected String determineCurrentLookupKey() { String tenantId = tenantIdentifierResolver.resolveCurrentTenantIdentifier(); // 如果租户标识为空,直接抛出异常,避免触发默认数据源 if (tenantId == null || tenantId.isBlank()) { throw new IllegalStateException("当前请求未携带租户标识"); } return tenantId; } private EmbeddedDatabase createEmbeddedDatabase(String name) { return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).setName(name).addScript("manual-schema.sql") .build(); } }
这种方式既满足了Spring初始化AbstractRoutingDataSource的要求,又不会创建无用的默认数据库,同时能在租户标识缺失时快速报错。
2. 启动时预加载所有租户数据源(适用于租户固定场景)
如果你的系统在启动时就能获取所有租户的配置信息,可以提前初始化所有租户数据源,同时在Spring启动阶段临时设置一个有效的租户上下文,让EntityManager初始化时能绑定到一个真实的数据源。这种方式仅适用于租户数量固定且启动前已知的场景。
二、延迟EntityManager的创建
通过以下配置和代码调整,可以让Spring Boot延迟EntityManager的初始化,直到首次调用仓库组件时再执行:
1. 配置JPA参数禁用启动时元数据加载
在application.properties中添加以下配置:
# 禁止Hibernate在启动时加载JDBC元数据 spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false # 延迟数据源初始化到应用启动后 spring.jpa.defer-datasource-initialization=true # 禁用自动DDL,避免启动时执行数据库脚本 spring.jpa.hibernate.ddl-auto=none
2. 手动配置EntityManagerFactory并开启懒初始化
自定义LocalContainerEntityManagerFactoryBean,设置懒初始化属性,让Spring只在首次使用EntityManager时创建该Bean:
@Configuration public class JpaConfig { @Bean @Lazy public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); em.setPackagesToScan("com.your.project.entity"); // 替换为你的实体类包路径 em.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); // 配置JPA属性 Properties jpaProperties = new Properties(); jpaProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); em.setJpaProperties(jpaProperties); // 开启懒初始化 em.setLazyInit(true); return em; } }
3. 给Spring Data JPA仓库添加懒加载注解
在你的仓库接口上添加@Lazy注解,确保仓库Bean也是懒初始化的,只有当首次调用仓库方法时才会触发EntityManager的创建:
@Lazy public interface TenantEntityRepository extends JpaRepository<TenantEntity, Long> { // 仓库方法定义 }
内容的提问来源于stack exchange,提问作者Martin Ahrer

