You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 11:25:09