Spring Boot中使用JPA TenantID列实现多租户时遭遇Bean作用域异常及相关疑问
我来帮你一步步拆解和解决这些问题:
一、先说说你遇到的Scope 'request' is not active错误原因
你的TenantIdentifierResolver是单例Bean(Spring默认@Component是单例),但它直接依赖了RequestScope的TenantContext。单例Bean在应用启动时就会初始化,这时候还没有任何请求上下文,Spring无法创建RequestScope的Bean实例,自然就抛出了这个异常。另外,Hibernate在启动初始化SessionFactory时会调用resolveCurrentTenantIdentifier方法,这时候同样没有租户上下文,也会触发报错。
二、核心解决方案:懒加载RequestScope Bean + 处理启动阶段的调用
我们需要修改TenantIdentifierResolver,不要直接持有TenantContext实例,而是通过ObjectFactory懒加载它,同时在启动阶段返回一个默认值避免异常:
import java.util.Map; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.hibernate.cfg.AvailableSettings; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; import org.hibernate.jpa.HibernatePropertiesCustomizer; @Component public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver<String>, HibernatePropertiesCustomizer { private final ObjectFactory<TenantContext> tenantContextFactory; // 用ObjectFactory懒加载TenantContext,避免单例直接持有RequestScope Bean @Autowired public TenantIdentifierResolver(ObjectFactory<TenantContext> tenantContextFactory) { this.tenantContextFactory = tenantContextFactory; } @Override public String resolveCurrentTenantIdentifier() { try { TenantContext tenantContext = tenantContextFactory.getObject(); String tenantId = tenantContext.getCurrentTenant(); // 1. 如果是不需要租户的请求/操作,返回特殊标识(后续配合实体注解忽略租户过滤) if (tenantId == null) { return "PUBLIC"; } // 2. 正常租户请求,返回租户ID return tenantId; } catch (IllegalStateException e) { // 应用启动阶段没有请求上下文,返回默认值(不会影响业务,因为启动时Hibernate不会访问业务表) return "DEFAULT_INIT"; } } @Override public void customize(Map<String, Object> hibernateProperties) { hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this); } @Override public boolean validateExistingCurrentSessions() { return false; } }
你的TenantContext可以保持不变,注意要确保它是真正的RequestScope代理:
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Component; import lombok.Getter; import lombok.Setter; @Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) @Getter @Setter public class TenantContext { private String currentTenant = null; }
三、优化你的业务流程:提前在拦截器中设置租户上下文
你原来的流程是在MeetingService中设置TenantContext,这样会把租户逻辑耦合到业务服务里。更好的做法是在请求进入时就解析JWT并设置租户:
- 写一个请求拦截器,专门处理JWT解析和租户设置:
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.ObjectFactory; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @Component public class JwtTenantInterceptor implements HandlerInterceptor { private final JwtTokenProvider jwtTokenProvider; private final ObjectFactory<TenantContext> tenantContextFactory; private final UserService userService; // 你用来根据UID查租户的服务 public JwtTenantInterceptor(JwtTokenProvider jwtTokenProvider, ObjectFactory<TenantContext> tenantContextFactory, UserService userService) { this.jwtTokenProvider = jwtTokenProvider; this.tenantContextFactory = tenantContextFactory; this.userService = userService; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 从请求头提取JWT Token String token = extractTokenFromHeader(request); if (token != null) { // 2. 解析UID String uid = jwtTokenProvider.extractUid(token); // 3. 根据UID获取租户ID String tenantId = userService.getTenantIdByUid(uid); // 4. 设置到RequestScope的TenantContext TenantContext tenantContext = tenantContextFactory.getObject(); tenantContext.setCurrentTenant(tenantId); } // 不需要租户的请求,这里不设置TenantContext,保持null即可 return true; } private String extractTokenFromHeader(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }
- 注册拦截器,指定哪些路径需要处理租户,哪些不需要:
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { private final JwtTenantInterceptor jwtTenantInterceptor; public WebMvcConfig(JwtTenantInterceptor jwtTenantInterceptor) { this.jwtTenantInterceptor = jwtTenantInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtTenantInterceptor) .addPathPatterns("/api/tenant/**") // 需要租户的业务接口 .excludePathPatterns("/api/public/**", "/auth/**"); // 不需要租户的公开接口 } }
四、处理不需要租户的表和实体
对于不需要租户ID的表,在对应的实体类上添加@TenantDiscriminatorIgnore注解(Hibernate 6及以上版本支持),这样Hibernate会自动跳过这些实体的租户过滤:
import jakarta.persistence.Entity; import org.hibernate.annotations.TenantDiscriminatorIgnore; @Entity @TenantDiscriminatorIgnore public class PublicConfig { // 实体字段... }
而需要租户的实体,要加上租户鉴别器列的注解:
import jakarta.persistence.Entity; import org.hibernate.annotations.TenantDiscriminatorColumn; import jakarta.persistence.DiscriminatorType; @Entity @TenantDiscriminatorColumn(name = "tenant_id", discriminatorType = DiscriminatorType.STRING) public class Meeting { // 实体字段... private String tenantId; }
五、解答你的几个疑问
为什么启动时
resolveCurrentTenantIdentifier会被调用?
Hibernate在启动初始化SessionFactory时,会校验多租户配置,所以会触发这个方法调用。这时候没有请求上下文,我们返回一个默认值(比如DEFAULT_INIT)即可,因为启动阶段Hibernate不会访问你的业务数据表,不会影响数据。如何确保冲突查询只查当前租户的会议?
只要你的Meeting实体加了@TenantDiscriminatorColumn,Hibernate会自动在所有CRUD操作中拼接tenant_id = 当前租户ID的条件,不需要你手动写SQL或JPQL添加这个条件,非常省心。不需要租户的端点怎么处理?
通过拦截器的excludePathPatterns排除这些路径,这些请求不会设置TenantContext,TenantIdentifierResolver会返回PUBLIC,配合实体上的@TenantDiscriminatorIgnore,Hibernate就不会对这些实体应用租户过滤。
备注:内容来源于stack exchange,提问作者user2741831

