Spring Boot 中统一同步与异步执行模型

背景

在项目开发中,只要同时使用同步和异步执行逻辑,很容易出现代码混乱的问题,一开始大家觉得这只是"执行方式不同",没什么大不了。但随着业务复杂度上升,问题逐渐暴露:

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. 代码可控 :不再到处是 @AsyncCompletableFuture,所有执行都有统一入口;

2. 上下文不丢:请求ID、操作人等信息在同步/异步中都能正确传递;

3. 扩展方便:要加重试、限流、熔断?在引擎层统一实现即可,业务代码无需修改;

4. 降低认知成本:新同学不需要刻意区分同步/异步方法,调用方式完全一致。

相关推荐
千寻技术帮2 小时前
10370_基于Springboot的校园志愿者管理系统
java·spring boot·后端·毕业设计
聆风吟º2 小时前
【Spring Boot 报错已解决】彻底解决 “Main method not found in class com.xxx.Application” 报错
java·spring boot·后端
乐茵lin2 小时前
golang中 Context的四大用法
开发语言·后端·学习·golang·编程·大学生·context
步步为营DotNet2 小时前
深度探索ASP.NET Core中间件的错误处理机制:保障应用程序稳健运行
后端·中间件·asp.net
bybitq2 小时前
Go中的闭包函数Closure
开发语言·后端·golang
吴佳浩10 小时前
Python入门指南(六) - 搭建你的第一个YOLO检测API
人工智能·后端·python
踏浪无痕11 小时前
JobFlow已开源:面向业务中台的轻量级分布式调度引擎 — 支持动态分片与延时队列
后端·架构·开源
Pitayafruit11 小时前
Spring AI 进阶之路05:集成 MCP 协议实现工具调用
spring boot·后端·llm
ss27311 小时前
线程池:任务队列、工作线程与生命周期管理
java·后端