fix(mybatis): @IgnoreTenant 支持嵌套引用计数,避免内层 clear 拆掉外层 ignore

InterceptorIgnoreHelper 的 ThreadLocal 不支持嵌套,内层 @IgnoreTenant 的 finally 会提前 clear,导致外层后续 SQL 重新拼 mch_no。MpUtil 改为线程内 depth 计数,仅最外层 enter/exit 时 handle/clear;请求结束 Filter 强制清理防线程池泄漏。
This commit is contained in:
DaxPay Dev
2026-07-10 16:22:02 +08:00
parent a31def2079
commit 835e062ffe
4 changed files with 79 additions and 7 deletions

View File

@@ -1,10 +1,12 @@
package cn.daxpay.open.payment.common.runtime;
import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
@@ -16,6 +18,8 @@ import java.io.IOException;
///
/// 在 HTTP 请求开始时开启线程级身份作用域,请求结束时关闭。
/// 仅管理 [PaymentContext] 的 open/close,不负责身份填充(由切面 / 装载器完成)。
/// 请求结束时额外强制清理 `@IgnoreTenant` 引用计数,防止 depth 泄漏污染线程池。
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@@ -29,6 +33,12 @@ public class PaymentContextFilter extends OncePerRequestFilter implements Ordere
filterChain.doFilter(request, response);
} finally {
paymentContext.close();
// 防 @IgnoreTenant depth 泄漏污染线程池
int ignoreDepth = MpUtil.getIgnoreTenantDepth();
if (ignoreDepth > 0) {
log.error("IgnoreTenant depth leaked at request end: {}, force clearing", ignoreDepth);
}
MpUtil.forceClearIgnoreTenant();
}
}

View File

@@ -11,6 +11,9 @@ import org.springframework.stereotype.Component;
/// # 忽略租户(商户)数据权限切面
///
/// 与 [MpUtil.ignoreTenant]/[MpUtil.clearIgnoreTenant] 的线程内引用计数配合,
/// 支持 `@IgnoreTenant` 嵌套:内层返回后不会清掉外层 ignore直到最外层 finally 才恢复过滤。
///
@Slf4j
@Aspect
@Component
@@ -29,15 +32,15 @@ public class IgnoreTenantAspectHandler {
return doHandle(pjp);
}
/// 忽略租户处理逻辑
/// 可重入忽略租户:内层 @IgnoreTenant 不会清掉外层 ignore见 MpUtil 引用计数)
private Object doHandle(ProceedingJoinPoint pjp) throws Throwable {
// 设置忽略租户插件
// 进入忽略租户作用域(可重入)
MpUtil.ignoreTenant();
try {
// 执行逻辑
return pjp.proceed();
} finally {
// 关闭忽略策略
// 退出忽略租户作用域depth 归零时才真正 clear
MpUtil.clearIgnoreTenant();
}
}

View File

@@ -23,6 +23,8 @@ import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapp
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.experimental.UtilityClass;
import org.apache.ibatis.reflection.property.PropertyNamer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.lang.reflect.Method;
@@ -40,6 +42,15 @@ import cn.daxpay.open.platform.core.code.CommonCode;
@UtilityClass
public class MpUtil {
private static final Logger log = LoggerFactory.getLogger(MpUtil.class);
/// 当前线程 @IgnoreTenant 嵌套深度null/0 = 未忽略)
private static final ThreadLocal<Integer> IGNORE_TENANT_DEPTH = new ThreadLocal<>();
/// 忽略租户行拦截策略tenantLine = true
private static final IgnoreStrategy IGNORE_TENANT_STRATEGY =
IgnoreStrategy.builder().tenantLine(true).build();
/// 获取分页对象 MyBatis-Plus
public <T> Page<T> getMpPage(PageParam page) {
return Page.of(page.getCurrent(), page.getSize());
@@ -172,13 +183,54 @@ public class MpUtil {
return tableInfo.getPropertyValue(object, keyProperty);
}
/// 忽略租户拦截
/// 进入忽略租户作用域(可重入,与 [clearIgnoreTenant] 成对使用)
///
/// 基于线程内引用计数:仅 depth 从 0→1 时真正打开 MP 忽略策略;
/// 嵌套调用不会互相 clear直到最外层 exit 才恢复租户过滤。
public void ignoreTenant() {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
int depth = Optional.ofNullable(IGNORE_TENANT_DEPTH.get()).orElse(0) + 1;
IGNORE_TENANT_DEPTH.set(depth);
if (depth == 1) {
InterceptorIgnoreHelper.handle(IGNORE_TENANT_STRATEGY);
}
}
/// 清除忽略租户拦截
/// 退出忽略租户作用域可重入depth 归零时才真正 clear
///
/// 不配对的 exitdepth 已为 0会强制清空并打错误日志避免脏状态。
public void clearIgnoreTenant() {
Integer cur = IGNORE_TENANT_DEPTH.get();
if (cur == null || cur <= 0) {
// 不配对 exit强制清干净避免脏状态
IGNORE_TENANT_DEPTH.remove();
InterceptorIgnoreHelper.clearIgnoreStrategy();
log.error("clearIgnoreTenant without matching ignoreTenant (depth underflow), force cleared");
return;
}
int depth = cur - 1;
if (depth == 0) {
IGNORE_TENANT_DEPTH.remove();
InterceptorIgnoreHelper.clearIgnoreStrategy();
} else {
IGNORE_TENANT_DEPTH.set(depth);
// 防御depth>0 但 MP 标志被硬 clear 时补回
if (!InterceptorIgnoreHelper.hasIgnoreStrategy()) {
InterceptorIgnoreHelper.handle(IGNORE_TENANT_STRATEGY);
log.warn("IgnoreStrategy missing while ignoreTenant depth={}, re-applied", depth);
}
}
}
/// 当前线程忽略租户嵌套深度0 表示未忽略;测试 / 巡检用)
public int getIgnoreTenantDepth() {
return Optional.ofNullable(IGNORE_TENANT_DEPTH.get()).orElse(0);
}
/// 强制清空 depth 与 MP 忽略标志
///
/// 仅用于请求结束兜底(如 Filter finally业务中间勿调用以免打断外层 ignore 作用域。
public void forceClearIgnoreTenant() {
IGNORE_TENANT_DEPTH.remove();
InterceptorIgnoreHelper.clearIgnoreStrategy();
}
}

View File

@@ -5,7 +5,14 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/// # 忽略租户数据隔离注解, 根据是实际情况发现添加注解后, 有多条语句只会第一条生效, 这个所以需要最好放在Manager层使用
/// # 忽略租户(商户)数据隔离
///
/// 在标注的方法/类执行期间关闭 TenantLine 租户行拦截(不拼 mch_no
/// 基于线程内引用计数,支持嵌套:外层 + 内层同时标注时,内层返回后外层仍保持忽略,
/// 直到最外层退出才恢复过滤。
///
/// 默认态为开启租户过滤;仅需局部忽略时在对应方法上标注本注解即可。
/// 不跨线程:异步任务需在子线程入口再次标注或手动 MpUtil.ignoreTenant/clearIgnoreTenant。
///
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)