背景
在项目开发中,只要同时使用同步和异步执行逻辑,很容易出现代码混乱的问题,一开始大家觉得这只是"执行方式不同",没什么大不了。但随着业务复杂度上升,问题逐渐暴露:
1、同步能实时拿到结果、捕获异常,异步失败了只能靠日志排查;
2、请求ID、操作人这些上下文信息,在异步执行时经常"丢失";
3、新入队同学需要刻意记住每个方法是同步还是异步,稍不注意就踩坑;
4、日志、审计、监控这些通用逻辑,同步和异步要各加一遍。
其实根源不是"不能用异步",而是大家把同步和异步当成了两套完全独立的执行体系------但对业务来说,它们本质上都是"一次业务执行"。
解决方案
换个思路:先把"业务执行"本身抽象出来,再用统一的引擎来处理"执行方式"。
也就是说,业务只需要关心"做什么",不需要关心"怎么执行";至于是同步还是异步,交给底层引擎决定。
这样做的好处很明确:
- 业务代码更聚焦,不再混杂执行细节;
- 同步/异步用同一种模型,上下文、异常、日志自然统一;
- 未来切换执行方式或扩展能力(重试、限流等),无需修改业务代码。
技术实现
1. 定义"执行单元":业务的最小执行单位
先抽象出一个最简洁的接口,代表"一次业务执行":
java
public interface Execution<R> {
R execute(ExecutionContext context) throws Exception;
}
这个接口只包含业务关心的元素:有上下文(ExecutionContext)、有执行结果(R)、可能抛出异常。它不关心任何执行细节------同步还是异步、用哪个线程池、怎么调度,都和它没关系。
2. 统一"执行上下文":让信息在执行中流动
上下文是业务执行的"环境变量",我们用ThreadLocal实现上下文的跨线程传递:
java
public class ExecutionContext {
// 用 ThreadLocal 存储当前上下文
private static final ThreadLocal<ExecutionContext> CONTEXT_HOLDER =
ThreadLocal.withInitial(ExecutionContext::new);
private String requestId; // 链路跟踪用的请求ID
private String operator; // 操作人,用于审计
private Map<String, Object> attributes = new HashMap<>(); // 业务参数
/** 获取当前上下文 */
public static ExecutionContext current() {
return CONTEXT_HOLDER.get();
}
/** 设置上下文 */
public static void set(ExecutionContext context) {
CONTEXT_HOLDER.set(context);
}
/** 清理上下文 */
public static void clear() {
CONTEXT_HOLDER.remove();
}
// getter/setter 省略
}
上下文的实际使用流程
1. 请求入口设置上下文
在请求入口(如 Controller/Filter)统一初始化和设置上下文:
java
@RestController
public class OrderController {
@PostMapping("/order")
public String createOrder(@RequestBody Order order) {
// 1. 构建上下文
ExecutionContext context = new ExecutionContext();
context.setRequestId(UUID.randomUUID().toString()); // 生成或从请求头获取
context.setOperator("currentUser"); // 从登录信息获取
context.getAttributes().put("order", order);
// 2. 绑定到当前线程
ExecutionContext.set(context);
try {
// 3. 提交业务执行(同步/异步由引擎决定)
ExecutionResult<Boolean> result = executorEngine.submit(ctx -> {
Order orderFromCtx = ctx.getAttribute("order");
orderService.createOrder(orderFromCtx);
return true;
});
return "success";
} finally {
// 4. 请求结束清理上下文,避免内存泄漏
ExecutionContext.clear();
}
}
}
3. 实现"执行引擎":统一的执行入口
有了执行单元和上下文,接下来需要一个统一的入口来执行它们:
java
public interface ExecutorEngine {
<R> ExecutionResult<R> submit(Execution<R> execution);
}
业务侧只需要提交执行单元,不需要关心执行方式。执行引擎会根据配置自动处理同步或异步。
同步执行引擎:最简单的实现,直接在当前线程执行
java
public class SyncExecutorEngine implements ExecutorEngine {
@Override
public <R> ExecutionResult<R> submit(Execution<R> execution) {
try {
R result = execution.execute(ExecutionContext.current());
return ExecutionResult.success(result);
} catch (Exception e) {
return ExecutionResult.failure(e);
}
}
}
异步执行引擎:复用线程池,支持上下文传递
java
public class AsyncExecutorEngine implements ExecutorEngine {
private final Executor executor;
public AsyncExecutorEngine(Executor executor) {
this.executor = executor;
}
@Override
public <R> ExecutionResult<R> submit(Execution<R> execution) {
// 捕获当前线程上下文并传递到异步线程
ExecutionContext currentCtx = ExecutionContext.current();
CompletableFuture<R> future = CompletableFuture.supplyAsync(() -> {
try {
ExecutionContext.set(currentCtx);
return execution.execute(ExecutionContext.current());
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 清理上下文避免内存泄漏
ExecutionContext.clear();
}
}, executor);
return ExecutionResult.async(future);
}
}
4. 业务侧的使用方式
业务只需要把原来的代码封装成执行单元,交给引擎处理:
java
// 原来的同步/异步代码无需再维护两套
executorEngine.submit(ctx -> {
Order order = ctx.getAttribute("order");
orderService.createOrder(order);
return true;
});
这段代码:
- 不关心是同步还是异步执行;
- 上下文自动传递;
- 异常由引擎统一处理;
- 日志、审计等通用逻辑在引擎层一次性处理,自动生效。
总结
通过统一执行模型的设计,我们可以将 业务逻辑 与 执行方式清晰分离,这套模型在项目中能解决几个实际问题:
1. 代码可控 :不再到处是 @Async 和 CompletableFuture,所有执行都有统一入口;
2. 上下文不丢:请求ID、操作人等信息在同步/异步中都能正确传递;
3. 扩展方便:要加重试、限流、熔断?在引擎层统一实现即可,业务代码无需修改;
4. 降低认知成本:新同学不需要刻意区分同步/异步方法,调用方式完全一致。