Spring Boot 3.1.4如何基于Token租户属性配置DB2多Schema多数据源
实现基于Token的动态Schema匹配(Spring Boot 3.1.4 + DB2 + JPA)
1. 租户与Schema后缀映射配置
先定义租户标识到Schema后缀的映射关系,可通过配置文件静态配置,也可从数据库动态加载:
配置文件示例(application.yml)
tenant: schema-mappings: TENANT_1: "1" TENANT_2: "2" # 按需添加更多租户映射
映射配置类
@ConfigurationProperties(prefix = "tenant") @Component public class TenantSchemaMapping { private Map<String, String> schemaMappings; // getter、setter public String getSuffixByTenantId(String tenantId) { String suffix = schemaMappings.get(tenantId); if (suffix == null) { throw new IllegalArgumentException("Unknown tenant: " + tenantId); } return suffix; } }
2. 租户上下文管理
用ThreadLocal存储当前请求的租户信息,确保请求线程内的Schema后缀可全局访问:
租户上下文类
public class TenantContext { private static final ThreadLocal<String> CURRENT_TENANT_ID = new ThreadLocal<>(); private static final ThreadLocal<String> CURRENT_SCHEMA_SUFFIX = new ThreadLocal<>(); public static void setTenantId(String tenantId) { CURRENT_TENANT_ID.set(tenantId); } public static String getTenantId() { return CURRENT_TENANT_ID.get(); } public static void setSchemaSuffix(String suffix) { CURRENT_SCHEMA_SUFFIX.set(suffix); } public static String getSchemaSuffix() { return CURRENT_SCHEMA_SUFFIX.get(); } public static void clear() { CURRENT_TENANT_ID.remove(); CURRENT_SCHEMA_SUFFIX.remove(); } }
请求拦截器解析Token并设置上下文
实现拦截器从请求Token中提取租户标识,关联对应Schema后缀并写入上下文:
@Component public class TenantTokenInterceptor implements HandlerInterceptor { private final TenantSchemaMapping schemaMapping; public TenantTokenInterceptor(TenantSchemaMapping schemaMapping) { this.schemaMapping = schemaMapping; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头/参数中提取Token,解析租户标识(示例逻辑,按需调整) String token = request.getHeader("Authorization"); String tenantId = parseTenantFromToken(token); // 自定义Token解析逻辑 // 获取对应Schema后缀并设置上下文 String schemaSuffix = schemaMapping.getSuffixByTenantId(tenantId); TenantContext.setTenantId(tenantId); TenantContext.setSchemaSuffix(schemaSuffix); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求结束清理上下文,避免内存泄漏 TenantContext.clear(); } // 自定义Token解析方法 private String parseTenantFromToken(String token) { // 示例:从JWT或自定义Token中提取TENANT属性 // 实际需根据你的Token格式实现 return "TENANT_1"; } }
注册拦截器
@Configuration public class WebMvcConfig implements WebMvcConfigurer { private final TenantTokenInterceptor tenantTokenInterceptor; public WebMvcConfig(TenantTokenInterceptor tenantTokenInterceptor) { this.tenantTokenInterceptor = tenantTokenInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tenantTokenInterceptor) .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns("/login", "/health"); // 排除无需租户验证的接口 } }
3. 自定义Hibernate物理命名策略(核心)
通过扩展Hibernate的PhysicalNamingStrategy,动态替换实体类中Schema的占位符为当前租户的后缀:
public class TenantAwarePhysicalNamingStrategy extends PhysicalNamingStrategyStandardImpl { @Override public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) { String schema = name.getSchema(); if (schema != null && schema.contains("{suffix}")) { String suffix = TenantContext.getSchemaSuffix(); if (suffix == null) { throw new IllegalStateException("No schema suffix found in tenant context"); } // 替换占位符为当前租户的Schema后缀 String newSchema = schema.replace("{suffix}", suffix); return Identifier.toIdentifier(name.getText(), newSchema); } // 对于SCHEMA_COMMON这类固定Schema,直接返回原标识 return super.toPhysicalTableName(name, context); } }
配置Hibernate使用自定义命名策略
在application.yml中指定物理命名策略:
spring: jpa: hibernate: naming: physical-strategy: com.yourpackage.TenantAwarePhysicalNamingStrategy properties: hibernate: show_sql: true # 开启SQL日志,方便验证动态Schema是否生效
4. 实体类配置示例
为不同前缀的Schema实体添加带占位符的Schema注解,固定Schema直接写死:
SCHEMA_A_n 实体示例
@Entity @Table(name = "USER", schema = "SCHEMA_A_{suffix}") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; // 其他字段、getter、setter }
SCHEMA_B_n 实体示例
@Entity @Table(name = "ORDER", schema = "SCHEMA_B_{suffix}") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long userId; private BigDecimal amount; // 其他字段、getter、setter }
SCHEMA_COMMON 实体示例
@Entity @Table(name = "DICT", schema = "SCHEMA_COMMON") public class Dict { @Id private String code; private String value; // 其他字段、getter、setter }
5. JPA Repository使用
直接使用标准JpaRepository即可,无需修改或原生查询:
public interface UserRepository extends JpaRepository<User, Long> { // 自定义查询方法,自动适配动态Schema List<User> findByUsername(String username); }
关键注意事项
- ThreadLocal清理:必须在请求结束后调用
TenantContext.clear(),避免内存泄漏。 - DB2权限:确保应用数据库用户拥有所有对应Schema(SCHEMA_COMMON、SCHEMA_A_n、SCHEMA_B_n)的读写权限。
- 异常处理:在映射配置类和拦截器中添加租户不存在的异常处理,避免请求崩溃。
- 动态映射扩展:若租户与后缀的映射需动态更新,可将
TenantSchemaMapping改为从数据库定时加载,而非静态配置。
内容的提问来源于stack exchange,提问作者Donald Duck
相关产品推荐
相关产品推荐

