必须用beforeExecute+afterExecute,因其由JVM保证执行,可在线程执行任务前绑定数据源标识、执行后强制清理,避免手动cleanup遗漏或异常导致的上下文污染,尤其适配异步/定时/消息等无Web调用栈场景。

在动态多数据源场景中,利用线程池的 beforeExecute 和 afterExecute 钩子实现“路由染色”,本质是在线程执行任务前将数据源标识(如 tenantId、dbKey)绑定到当前线程,并在线程执行结束后及时清理,避免线程复用导致的上下文污染。关键不在于钩子本身,而在于它与 ThreadLocal 的协同时机控制。
为什么必须用 beforeExecute + afterExecute?
普通方式(如在业务方法开头 set、结尾 remove)依赖开发者手动保障 cleanup,极易遗漏;而线程池钩子由 JVM 级别保证执行,只要任务提交进线程池,就一定能触发绑定与清理。尤其适合异步任务、定时任务、消息消费等非 Web 请求链路场景。
- beforeExecute:在任务真正 run() 前执行,此时线程已确定、任务上下文(如 MDC、ThreadLocal)可安全写入
- afterExecute:在任务 run() 完成(无论正常结束或抛异常)后执行,是清理 ThreadLocal 的黄金位置
- 不能只用
afterExecute—— 没有前置绑定,任务根本不知道该切哪个库 - 不能依赖
finally或 AOP —— 异步任务可能跨线程、无调用栈可拦截
如何安全绑定与清理数据源标识
核心是封装一个线程安全、可继承、支持嵌套的路由上下文容器,例如:
public class RoutingContext {
private static final ThreadLocal<deque>> ROUTE_STACK = ThreadLocal.withInitial(ArrayDeque::new);
<pre class="brush:php;toolbar:false;">public static void push(String routeKey) {
ROUTE_STACK.get().push(routeKey);
}
public static String peek() {
Deque<string> stack = ROUTE_STACK.get();
return stack.isEmpty() ? null : stack.peek();
}
public static void pop() {
Deque<string> stack = ROUTE_STACK.get();
if (!stack.isEmpty()) stack.pop();
}
public static void clear() {
ROUTE_STACK.get().clear();
}</string></string>
}
再配合自定义线程池:
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
public class RoutingAwareThreadPool extends ThreadPoolExecutor {
private final Supplier<string> routeKeySupplier;
<pre class="brush:php;toolbar:false;">public RoutingAwareThreadPool(int corePoolSize, int maxPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<runnable> workQueue,
Supplier<string> routeKeySupplier) {
super(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue);
this.routeKeySupplier = routeKeySupplier;
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
String key = routeKeySupplier.get(); // 从上游(如 MDC、RequestContextHolder、参数传递)获取
if (key != null) RoutingContext.push(key);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
try {
super.afterExecute(r, t);
} finally {
RoutingContext.clear(); // 或 pop(),取决于是否支持嵌套
}
}</string></runnable>
}
数据源路由层如何读取染色结果
在动态数据源 AbstractRoutingDataSource 的 determineCurrentLookupKey() 中直接读取:
@Override
protected Object determineCurrentLookupKey() {
String key = RoutingContext.peek();
return key != null ? key : "default";
}
注意:
- 若使用 MyBatis,需确保 SqlSession 创建发生在 beforeExecute 之后(即事务/DAO 调用在线程池内),否则会取不到值
- 若存在嵌套异步(如 submit 后再 submit),建议用 push/pop 替代 set/clear,支持上下文栈
常见陷阱与规避方式
-
线程复用导致残留:务必在
afterExecute中clear()或pop(),不能只靠业务代码 remove -
异常中断未清理:
afterExecute的 finally 语义天然覆盖异常路径,比 try-finally 更可靠 - 父子线程不传递:钩子只作用于工作线程,若任务内部新建线程(如 ForkJoinPool、CompletableFuture.runAsync),需显式传递,例如用 InheritableThreadLocal 或手动 copy
-
Spring @Async 不生效:默认
SimpleAsyncTaskExecutor每次 new 线程,不走钩子;需替换为自定义 RoutingAwareThreadPool 并配置到TaskExecutor
这套机制把“染色”彻底下沉到执行基础设施层,业务代码零侵入,且对异步模型友好。只要线程池是统一入口,就能守住数据源路由边界。










