|
|
@@ -6,76 +6,55 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import java.util.function.Supplier;
|
|
|
|
|
|
/**
|
|
|
- * 当前线程的「案件 + 用户」上下文。
|
|
|
+ * 当前作用域的「案件 + 用户」上下文(JDK 25 ScopedValue,JEP 506)。
|
|
|
*
|
|
|
- * <p><b>为什么需要它</b>:原 {@link com.zsjz.ai.common.utils.StateManager} 是 JVM 级单例,
|
|
|
- * 服务端多人同时使用时,A 用户开案会覆盖 B 用户的当前案件,导致串案。本类把上下文下沉到线程级:
|
|
|
- * <ul>
|
|
|
- * <li><b>请求线程</b>:由 {@code CaseContextInterceptor} 在 preHandle 写入,afterCompletion 清理;</li>
|
|
|
- * <li><b>异步线程</b>(清洗虚拟线程、{@code GlobalPool.EXC_POOL}、Reactor 的 boundedElastic):
|
|
|
- * ThreadLocal 与 {@code RequestContextHolder} 都会丢失,必须由调用方用
|
|
|
- * {@link #runWith(Long, Long, Runnable)} / {@link #callWith(Long, Long, Supplier)} 显式包裹。</li>
|
|
|
- * </ul>
|
|
|
+ * <p><b>为什么用 ScopedValue 替换 ThreadLocal</b>:绑定随
|
|
|
+ * {@code ScopedValue.where(...).run(...)} 作用域结束自动消失——
|
|
|
+ * 「忘清理导致池线程读到上一个请求案件」的串案风险从结构上消除;
|
|
|
+ * 绑定不可变、读取比 ThreadLocalMap 快,对清洗链路海量虚拟线程更友好。
|
|
|
+ *
|
|
|
+ * <p><b>请求线程</b>:由 {@code CaseContextFilter} 把整条 filter chain 包进作用域;
|
|
|
+ * <b>异步线程</b>(清洗虚拟线程、{@code GlobalPool.EXC_POOL}、Reactor 的 boundedElastic):
|
|
|
+ * 作用域绑定不会跨线程传播,必须由调用方用
|
|
|
+ * {@link #runWith(Long, Long, Runnable)} / {@link #callWith(Long, Long, Supplier)} 显式包裹。
|
|
|
*
|
|
|
* <p><b>为什么连 userId 一起放进来</b>:{@code SseService} 现在按用户维护连接,
|
|
|
- * 而清洗/治理链路整条跑在异步线程上 —— 池线程里既没有 ThreadLocal 也没有 sa-token 上下文,
|
|
|
+ * 而清洗/治理链路整条跑在异步线程上 —— 池线程里既没有作用域也没有 sa-token 上下文,
|
|
|
* 只传 caseId 的话进度消息会推到 {@code ANONYMOUS_USER},用户什么都收不到。
|
|
|
*
|
|
|
- * <p><b>解析顺序</b>:ThreadLocal → sa-token Token-Session(仅请求线程可用)。
|
|
|
+ * <p><b>解析顺序</b>:作用域绑定 → sa-token Token-Session(仅请求线程可用)。
|
|
|
* 与 {@code StateManager.getCaseId()} 不同,解析不出来时返回 {@code null}(不是哨兵 888888)。
|
|
|
*/
|
|
|
@Slf4j
|
|
|
public final class CaseContextHolder {
|
|
|
|
|
|
- /** sa-token Token-Session 中存放当前案件 ID 的键 */
|
|
|
+ /**
|
|
|
+ * sa-token Token-Session 中存放当前案件 ID 的键
|
|
|
+ */
|
|
|
public static final String SESSION_KEY_CASE_ID = "caseId";
|
|
|
|
|
|
- /** 线程上下文载体 */
|
|
|
- public record Context(Long caseId, Long userId) {
|
|
|
- }
|
|
|
-
|
|
|
- private static final ThreadLocal<Context> HOLDER = new ThreadLocal<>();
|
|
|
+ private static final ScopedValue<Long> CASE_ID = ScopedValue.newInstance();
|
|
|
+ private static final ScopedValue<Long> USER_ID = ScopedValue.newInstance();
|
|
|
|
|
|
private CaseContextHolder() {
|
|
|
}
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
- // 写
|
|
|
- // ------------------------------------------------------------------
|
|
|
-
|
|
|
- /** 绑定案件 ID,<b>保留</b>当前线程已有的 userId */
|
|
|
- public static void set(Long caseId) {
|
|
|
- Context current = HOLDER.get();
|
|
|
- set(caseId, current == null ? null : current.userId());
|
|
|
- }
|
|
|
-
|
|
|
- /** 绑定案件 + 用户 ID;两者都为 null 时等价于清理 */
|
|
|
- public static void set(Long caseId, Long userId) {
|
|
|
- if (caseId == null && userId == null) {
|
|
|
- HOLDER.remove();
|
|
|
- } else {
|
|
|
- HOLDER.set(new Context(caseId, userId));
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- public static void clear() {
|
|
|
- HOLDER.remove();
|
|
|
- }
|
|
|
-
|
|
|
- // ------------------------------------------------------------------
|
|
|
- // 读(只读 ThreadLocal,不做兜底)
|
|
|
+ // 读(只读当前作用域,不做兜底)
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
- /** 本线程显式绑定的案件 ID;未绑定时返回 null */
|
|
|
+ /**
|
|
|
+ * 当前作用域绑定的案件 ID;未绑定时返回 null
|
|
|
+ */
|
|
|
public static Long get() {
|
|
|
- Context ctx = HOLDER.get();
|
|
|
- return ctx == null ? null : ctx.caseId();
|
|
|
+ return CASE_ID.orElse(null);
|
|
|
}
|
|
|
|
|
|
- /** 本线程显式绑定的用户 ID;未绑定时返回 null */
|
|
|
+ /**
|
|
|
+ * 当前作用域绑定的用户 ID;未绑定时返回 null
|
|
|
+ */
|
|
|
public static Long getUserId() {
|
|
|
- Context ctx = HOLDER.get();
|
|
|
- return ctx == null ? null : ctx.userId();
|
|
|
+ return USER_ID.orElse(null);
|
|
|
}
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
@@ -83,7 +62,7 @@ public final class CaseContextHolder {
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
/**
|
|
|
- * 解析当前案件 ID:ThreadLocal 优先,其次从 sa-token Token-Session 兜底。
|
|
|
+ * 解析当前案件 ID:作用域绑定优先,其次从 sa-token Token-Session 兜底。
|
|
|
*
|
|
|
* @return 案件 ID;无法解析(未登录 / 未开案 / 非请求线程)时返回 {@code null}
|
|
|
*/
|
|
|
@@ -105,7 +84,7 @@ public final class CaseContextHolder {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 解析当前用户 ID:ThreadLocal 优先,其次取 sa-token 登录态。
|
|
|
+ * 解析当前用户 ID:作用域绑定优先,其次取 sa-token 登录态。
|
|
|
*
|
|
|
* @return 用户 ID;无法解析时返回 {@code null}
|
|
|
*/
|
|
|
@@ -126,55 +105,80 @@ public final class CaseContextHolder {
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
/**
|
|
|
- * 在指定案件上下文中执行并返回结果(执行完毕后恢复原上下文)。
|
|
|
- *
|
|
|
- * @deprecated 异步链路请用 {@link #callWith(Long, Long, Supplier)} 一并带上 userId,
|
|
|
+ * 在指定案件上下文中执行并返回结果(作用域结束后绑定自动恢复/消失),
|
|
|
+ * <b>保留</b>当前作用域已有的 userId。
|
|
|
+ * <p>
|
|
|
+ * 异步链路请用 {@link #callWith(Long, Long, Supplier)} 一并带上 userId,
|
|
|
* 否则进度类 SSE 消息找不到接收者。
|
|
|
*/
|
|
|
- @Deprecated
|
|
|
public static <T> T callWith(Long caseId, Supplier<T> action) {
|
|
|
- Context previous = HOLDER.get();
|
|
|
- try {
|
|
|
- set(caseId);
|
|
|
- return action.get();
|
|
|
- } finally {
|
|
|
- restore(previous);
|
|
|
- }
|
|
|
+ return callWith(caseId, getUserId(), action);
|
|
|
}
|
|
|
|
|
|
- /** 在指定「案件 + 用户」上下文中执行并返回结果 */
|
|
|
+ /**
|
|
|
+ * 在指定「案件 + 用户」作用域中执行并返回结果
|
|
|
+ */
|
|
|
public static <T> T callWith(Long caseId, Long userId, Supplier<T> action) {
|
|
|
- Context previous = HOLDER.get();
|
|
|
- try {
|
|
|
- set(caseId, userId);
|
|
|
+ if (caseId == null && userId == null) {
|
|
|
return action.get();
|
|
|
- } finally {
|
|
|
- restore(previous);
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return carrier(caseId, userId).call(action::get);
|
|
|
+ } catch (RuntimeException e) {
|
|
|
+ throw e;
|
|
|
+ } catch (Exception e) {
|
|
|
+ // Supplier.get() 不抛受检异常,理论不可达
|
|
|
+ throw new IllegalStateException(e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** {@link #callWith(Long, Supplier)} 的无返回值版本(已废弃,见其说明) */
|
|
|
- @Deprecated
|
|
|
- public static void runWith(Long caseId, Runnable action) {
|
|
|
- callWith(caseId, () -> {
|
|
|
+ /**
|
|
|
+ * 在指定「案件 + 用户」作用域中执行
|
|
|
+ */
|
|
|
+ public static void runWith(Long caseId, Long userId, Runnable action) {
|
|
|
+ if (caseId == null && userId == null) {
|
|
|
action.run();
|
|
|
- return null;
|
|
|
- });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ carrier(caseId, userId).run(action);
|
|
|
}
|
|
|
|
|
|
- /** 在指定「案件 + 用户」上下文中执行 */
|
|
|
- public static void runWith(Long caseId, Long userId, Runnable action) {
|
|
|
- callWith(caseId, userId, () -> {
|
|
|
+ /**
|
|
|
+ * 在指定「案件 + 用户」作用域中执行可能抛受检异常的动作。
|
|
|
+ * {@code CaseContextFilter} 包裹 {@code FilterChain.doFilter} 用——链路里要透传
|
|
|
+ * ServletException/IOException,而 Runnable 无法声明受检异常。
|
|
|
+ */
|
|
|
+ public static <E extends Exception> void runWithThrowing(Long caseId, Long userId,
|
|
|
+ ThrowingRunnable<E> action) throws E {
|
|
|
+ if (caseId == null && userId == null) {
|
|
|
+ action.run();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ carrier(caseId, userId).call(() -> {
|
|
|
action.run();
|
|
|
return null;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private static void restore(Context previous) {
|
|
|
- if (previous == null) {
|
|
|
- HOLDER.remove();
|
|
|
- } else {
|
|
|
- HOLDER.set(previous);
|
|
|
+ /**
|
|
|
+ * 可抛受检异常的 Runnable
|
|
|
+ */
|
|
|
+ @FunctionalInterface
|
|
|
+ public interface ThrowingRunnable<E extends Exception> {
|
|
|
+ void run() throws E;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ScopedValue 不允许绑定 null,按非空字段逐段拼接绑定。
|
|
|
+ * 调用前提:caseId 与 userId 不同时为 null。
|
|
|
+ */
|
|
|
+ private static ScopedValue.Carrier carrier(Long caseId, Long userId) {
|
|
|
+ if (caseId == null) {
|
|
|
+ return ScopedValue.where(USER_ID, userId);
|
|
|
+ }
|
|
|
+ if (userId == null) {
|
|
|
+ return ScopedValue.where(CASE_ID, caseId);
|
|
|
}
|
|
|
+ return ScopedValue.where(CASE_ID, caseId).where(USER_ID, userId);
|
|
|
}
|
|
|
}
|