mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-08 22:25:32 +08:00
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
///
|
||||
/// 不配对的 exit(depth 已为 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user