跳到正文
hello world

45. 为什么 @Async 同类调用会失效:Spring 代理机制

发布于阅读量 0

45. 为什么 @Async 同类调用会失效:Spring 代理机制

上一节使用了:

@Async("pdfTaskExecutor")

把 PDF 处理方法提交到指定线程池中执行。

但使用 @Async 时有一个非常重要的问题:

同一个类中的方法互相调用时,@Async 通常不会生效。

例如下面这段代码:

@Service
public class PdfTaskService {

    public void submitPdf(File file) {
        processPdfAsync(file);
    }

    @Async("pdfTaskExecutor")
    public void processPdfAsync(File file) {
        System.out.println(
                Thread.currentThread().getName()
        );

        processPdf(file);
    }

    private void processPdf(File file) {
        // 处理 PDF
    }
}

调用:

pdfTaskService.submitPdf(file);

很多人会认为:

submitPdf 调用 processPdfAsync;

processPdfAsync 上有 @Async;

所以 processPdfAsync 应该在线程池中执行。

但实际运行时,可能看到:

http-nio-8080-exec-1

而不是:

pdf-worker-1

这说明 processPdfAsync() 仍然在 HTTP 请求线程中同步执行。

原因不在于线程池配置错误,而在于:

这次方法调用没有经过 Spring 代理对象。

@Async 不是方法自己实现的能力

先看一个普通方法:

public void processPdfAsync(File file) {
    processPdf(file);
}

给它加上:

@Async("pdfTaskExecutor")

并不会改变这个 Java 方法本身的代码。

它的方法体仍然只是:

processPdf(file);

@Async 本身也不会自动创建线程。

真正实现异步的是 Spring 在方法外面增加的一层代理逻辑。

可以简单理解为,Spring 帮我生成了一个类似这样的对象:

public class PdfTaskServiceProxy
        extends PdfTaskService {

    private final Executor pdfTaskExecutor;

    @Override
    public void processPdfAsync(File file) {

        pdfTaskExecutor.execute(() -> {
            super.processPdfAsync(file);
        });
    }
}

这不是 Spring 真实生成代码的完整形式,只是为了帮助理解。

它表达的核心意思是:

调用代理对象的 processPdfAsync;

代理对象先把任务提交给线程池;

线程池再调用真正的业务方法。

所以异步能力不在原始方法里面,而在代理对象外面。


Spring 容器中保存的可能是代理对象

假设有这个 Service:

@Service
public class PdfAsyncService {

    @Async("pdfTaskExecutor")
    public void processPdfAsync(File file) {
        processPdf(file);
    }

    private void processPdf(File file) {
        // 处理 PDF
    }
}

Spring 启动时发现:

@Async

会为这个 Bean 创建代理对象。

其他类通过依赖注入拿到的:

private final PdfAsyncService pdfAsyncService;

表面类型是:

PdfAsyncService

但实际拿到的对象可能是 Spring 生成的代理对象。

所以外部调用:

pdfAsyncService.processPdfAsync(file);

大致经过:

调用方
  ↓
Spring 代理对象
  ↓
识别 @Async
  ↓
把方法任务提交到 pdfTaskExecutor
  ↓
工作线程执行真实业务方法

这时 @Async 可以正常生效。


外部调用为什么可以异步

例如有两个 Service。

异步执行类:

@Service
public class PdfAsyncService {

    @Async("pdfTaskExecutor")
    public void processPdfAsync(File file) {

        System.out.println(
                "异步线程:"
                        + Thread.currentThread().getName()
        );

        processPdf(file);
    }

    private void processPdf(File file) {
        // PDF 处理代码
    }
}

任务调度类:

@Service
public class PdfTaskService {

    private final PdfAsyncService pdfAsyncService;

    public PdfTaskService(
            PdfAsyncService pdfAsyncService
    ) {
        this.pdfAsyncService = pdfAsyncService;
    }

    public void submitPdf(File file) {

        System.out.println(
                "调用线程:"
                        + Thread.currentThread().getName()
        );

        pdfAsyncService.processPdfAsync(file);
    }
}

Controller 调用:

pdfTaskService.submitPdf(file);

执行流程是:

Controller
  ↓
PdfTaskService
  ↓
Spring 管理的 PdfAsyncService 代理对象
  ↓
@Async 拦截器
  ↓
pdfTaskExecutor
  ↓
真实的 processPdfAsync 方法

日志可能是:

调用线程:http-nio-8080-exec-1
异步线程:pdf-worker-1

说明发生了线程切换。


同类内部调用为什么失效

再看同一个类中的调用:

@Service
public class PdfTaskService {

    public void submitPdf(File file) {
        processPdfAsync(file);
    }

    @Async("pdfTaskExecutor")
    public void processPdfAsync(File file) {
        processPdf(file);
    }

    private void processPdf(File file) {
        // PDF 处理代码
    }
}

当外部调用:

pdfTaskService.submitPdf(file);

第一步确实经过了 Spring 代理对象。

但代理看到的是:

submitPdf()

submitPdf() 上没有 @Async,所以正常进入真实对象。

进入真实对象以后执行:

processPdfAsync(file);

实际上相当于:

this.processPdfAsync(file);

这里的 this 是当前真实业务对象。

它没有重新绕回 Spring 代理对象。

因此执行流程变成:

外部调用
  ↓
Spring 代理对象
  ↓
真实对象的 submitPdf
  ↓
this.processPdfAsync
  ↓
直接执行真实方法

中间没有再次经过:

@Async 拦截器

所以不会提交到线程池。


可以把代理理解成公司前台

可以把 Spring 代理对象理解成公司前台。

外部人员来找员工办事时,需要先经过前台:

外部人员
  ↓
前台
  ↓
检查办理规则
  ↓
安排对应员工处理

@Async 就像前台看到某类业务后,会把任务交给后台工作组。

但员工进入办公室以后,自己直接叫旁边的同事做事:

员工 A
  ↓
直接叫员工 B

这个过程没有重新经过前台。

前台自然不知道这次调用,也就无法应用异步规则。

对应到代码就是:

Bean 外部调用:

经过代理,可以触发 @Async。

同类内部调用:

使用 this 直接调用,不经过代理。

怎样证明同类调用没有切换线程

可以写一个简单示例。

@Service
public class AsyncTestService {

    public void outerMethod() {

        System.out.println(
                "outerMethod 线程:"
                        + Thread.currentThread().getName()
        );

        innerAsyncMethod();
    }

    @Async("pdfTaskExecutor")
    public void innerAsyncMethod() {

        System.out.println(
                "innerAsyncMethod 线程:"
                        + Thread.currentThread().getName()
        );
    }
}

Controller 调用:

@RestController
@RequestMapping("/async-test")
public class AsyncTestController {

    private final AsyncTestService asyncTestService;

    public AsyncTestController(
            AsyncTestService asyncTestService
    ) {
        this.asyncTestService =
                asyncTestService;
    }

    @GetMapping
    public String test() {

        asyncTestService.outerMethod();

        return "success";
    }
}

请求接口后,可能输出:

outerMethod 线程:http-nio-8080-exec-1
innerAsyncMethod 线程:http-nio-8080-exec-1

两个方法线程名相同。

说明 innerAsyncMethod() 没有真正异步。


直接从 Controller 调用异步方法

如果 Controller 直接调用:

asyncTestService.innerAsyncMethod();

例如:

@GetMapping("/direct")
public String direct() {

    asyncTestService.innerAsyncMethod();

    return "success";
}

日志可能变成:

innerAsyncMethod 线程:pdf-worker-1

因为 Controller 注入的是 Spring 管理的代理对象。

这次调用经过了:

Spring 代理对象
  ↓
@Async 拦截器
  ↓
线程池

所以异步生效。

这也解释了为什么:

从 Controller 调用可以异步;

从本类另一个方法调用却不异步。

区别不在 Controller,而在调用是否经过代理。


推荐解决方案:拆成两个 Service

最推荐的做法是把异步方法拆到独立 Bean 中。

PdfTaskService

负责:

接收任务;

组织业务流程;

调用异步执行类。
@Service
public class PdfTaskService {

    private final PdfAsyncService pdfAsyncService;

    public PdfTaskService(
            PdfAsyncService pdfAsyncService
    ) {
        this.pdfAsyncService = pdfAsyncService;
    }

    public CompletableFuture<PdfTaskResult>
    submitPdf(File file) {

        return pdfAsyncService
                .processPdfAsync(file);
    }
}

PdfAsyncService

负责:

在线程池中执行 PDF 处理。
@Service
public class PdfAsyncService {

    @Async("pdfTaskExecutor")
    public CompletableFuture<PdfTaskResult>
    processPdfAsync(File file) {

        long start =
                System.currentTimeMillis();

        String targetPath =
                processPdf(file);

        long cost =
                System.currentTimeMillis()
                        - start;

        PdfTaskResult result =
                PdfTaskResult.success(
                        file.getName(),
                        targetPath,
                        cost
                );

        return CompletableFuture
                .completedFuture(result);
    }

    private String processPdf(File file) {

        System.out.println(
                Thread.currentThread().getName()
                        + " 开始处理:"
                        + file.getName()
        );

        return "output/"
                + file.getName()
                .replace(
                        ".pdf",
                        "-watermark.pdf"
                );
    }
}

调用关系是:

PdfTaskService
  ↓
PdfAsyncService 的 Spring 代理对象
  ↓
@Async
  ↓
pdfTaskExecutor

这样最容易理解,也最容易维护。


为什么拆分类更合理

拆成两个 Service 不只是为了绕过代理问题。

它还明确划分了职责。

PdfTaskService 负责业务调度:

创建任务;

检查参数;

调用异步处理;

记录任务状态;

组织批量任务。

PdfAsyncService 负责具体执行:

进入线程池;

处理单个 PDF;

返回处理结果。

以后如果不再使用 @Async,改成消息队列或者分布式任务系统,也可以只替换执行部分。


方案二:直接使用 CompletableFuture.supplyAsync

如果不想依赖 @Async 代理机制,也可以直接注入线程池,然后手动提交任务。

@Service
public class PdfTaskService {

    private final Executor pdfTaskExecutor;

    public PdfTaskService(
            @Qualifier("pdfTaskExecutor")
            Executor pdfTaskExecutor
    ) {
        this.pdfTaskExecutor =
                pdfTaskExecutor;
    }

    public CompletableFuture<PdfTaskResult>
    submitPdf(File file) {

        return CompletableFuture.supplyAsync(
                () -> processPdf(file),
                pdfTaskExecutor
        );
    }

    private PdfTaskResult processPdf(File file) {

        String targetPath =
                "output/"
                        + file.getName()
                        .replace(
                                ".pdf",
                                "-watermark.pdf"
                        );

        return PdfTaskResult.success(
                file.getName(),
                targetPath,
                0
        );
    }
}

这种方式没有同类调用失效的问题。

因为异步行为直接写在代码中:

CompletableFuture.supplyAsync(...)

它不依赖方法是否经过 Spring 代理。


@Async 和 supplyAsync 的本质区别

@Async

异步逻辑由 Spring 代理完成:

调用代理方法
  ↓
代理读取 @Async
  ↓
代理提交线程池

业务代码看起来比较简洁:

@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
}

但要遵守代理调用规则。


supplyAsync

异步提交由代码直接完成:

CompletableFuture.supplyAsync(
        () -> processPdf(file),
        pdfTaskExecutor
);

调用关系更加显式。

不需要依赖代理对象,但需要自己编排任务。


怎么选择

如果只是把一个完整业务方法异步执行,可以使用:

@Async

如果有复杂流程,例如:

处理 PDF;

上传文件;

查询用户;

合并结果;

保存数据库;

发送通知。

更适合使用:

CompletableFuture.supplyAsync();

thenCompose();

thenCombine();

thenApply();

因为任务之间的关系可以直接从代码中看出来。


能不能通过注入自己解决

有一种写法是让 Service 注入自己的代理对象。

例如:

@Service
public class PdfTaskService {

    private PdfTaskService self;

    @Autowired
    public void setSelf(
            @Lazy PdfTaskService self
    ) {
        this.self = self;
    }

    public void submitPdf(File file) {
        self.processPdfAsync(file);
    }

    @Async("pdfTaskExecutor")
    public void processPdfAsync(File file) {
        processPdf(file);
    }
}

这里调用:

self.processPdfAsync(file);

可能经过代理,从而让 @Async 生效。

但这种写法存在明显问题:

类依赖自己;

代码不容易理解;

可能产生循环依赖;

业务代码和代理实现高度耦合;

测试和维护比较麻烦。

所以不推荐作为常规方案。

更清楚的方式仍然是:

拆分独立的异步 Service。

能不能从 ApplicationContext 获取自己

也有人这样写:

@Service
public class PdfTaskService {

    private final ApplicationContext
            applicationContext;

    public PdfTaskService(
            ApplicationContext applicationContext
    ) {
        this.applicationContext =
                applicationContext;
    }

    public void submitPdf(File file) {

        PdfTaskService proxy =
                applicationContext.getBean(
                        PdfTaskService.class
                );

        proxy.processPdfAsync(file);
    }

    @Async("pdfTaskExecutor")
    public void processPdfAsync(File file) {
        processPdf(file);
    }
}

这种方式能够重新从 Spring 容器中拿到代理对象。

但业务类直接依赖:

ApplicationContext

会带来新的问题:

业务代码依赖 Spring 容器;

依赖关系隐藏;

代码测试困难;

可读性降低。

所以也不推荐在普通业务代码中使用。


能不能使用 AopContext.currentProxy

还可以通过:

AopContext.currentProxy()

获取当前代理对象。

例如:

PdfTaskService proxy =
        (PdfTaskService)
                AopContext.currentProxy();

proxy.processPdfAsync(file);

但这种写法需要额外开启代理暴露,并且让业务代码直接知道 AOP 代理存在。

这会造成:

业务逻辑和 Spring AOP 强耦合;

类型转换容易出错;

代码理解成本增加;

只有在特定代理调用环境中才能工作。

因此同样不适合作为常规业务方案。


代理对象和真实对象是什么关系

假设原始类是:

PdfAsyncService

Spring 可能创建一个代理对象:

PdfAsyncService 的代理对象

外部注入时拿到的是代理对象。

代理对象内部保存或关联真实业务对象。

调用方法时,大致流程是:

代理对象接收调用;

执行增强逻辑;

调用真实业务方法;

返回结果。

不同注解可能对应不同增强逻辑:

@Async:

提交线程池。

@Transactional:

开启、提交或者回滚事务。

@Cacheable:

查询和写入缓存。

@Retryable:

失败后重试。

自定义 AOP:

记录日志、权限校验、性能统计。

这些注解之所以经常存在同类调用失效问题,是因为它们都可能依赖代理拦截。


JDK 动态代理和 CGLIB

Spring 常见代理方式有两种:

JDK 动态代理;

CGLIB 代理。

JDK 动态代理

JDK 动态代理主要基于接口。

例如:

public interface PdfService {

    void processPdf(File file);
}

实现类:

@Service
public class PdfServiceImpl
        implements PdfService {

    @Override
    @Async("pdfTaskExecutor")
    public void processPdf(File file) {
        // 处理 PDF
    }
}

代理对象实现相同接口:

PdfService 代理对象

调用方通常通过接口类型注入:

private final PdfService pdfService;

代理对象接收接口方法调用,再执行异步增强。


CGLIB 代理

CGLIB 代理主要通过创建目标类的子类实现。

可以简单理解成:

public class PdfAsyncServiceProxy
        extends PdfAsyncService {

    @Override
    public void processPdfAsync(File file) {
        // 先执行异步代理逻辑
        // 再调用父类方法
    }
}

真实生成的代理要复杂得多,但理解到这里已经够用。

无论使用 JDK 动态代理还是 CGLIB,默认代理模式都有一个共同点:

只有经过代理对象的方法调用,增强逻辑才能生效。

同类内部的:

this.processPdfAsync()

不会重新经过代理。


final 方法为什么可能影响代理

如果一个方法被声明成:

public final void processPdfAsync(File file) {
}

CGLIB 通过创建子类并重写方法实现代理。

final 方法不能被子类重写。

所以某些基于子类的代理增强无法应用到 final 方法。

同样,如果整个类被声明为:

public final class PdfAsyncService {
}

也不能通过普通子类方式创建 CGLIB 代理。

因此需要代理增强的方法,一般不要随意声明为:

final 类;

final 方法。

private 方法为什么不能正常被代理

例如:

@Async("pdfTaskExecutor")
private void processPdfAsync(File file) {
}

private 方法不能被子类重写,也不是供外部代理接口调用的方法。

而且它通常只能从本类内部调用:

this.processPdfAsync(file);

因此不会经过 Spring 代理对象。

所以需要使用 @Async 的方法通常应该是:

public

并且由其他 Spring Bean 调用。


@Async 和 @Transactional 都有同类调用问题

例如:

@Service
public class OrderService {

    public void createOrder() {
        saveOrder();
    }

    @Transactional
    public void saveOrder() {
        // 保存订单
    }
}

createOrder() 内部直接调用:

saveOrder();

也可能不会经过事务代理。

这和 @Async 同类调用失效的原因相同:

调用没有经过 Spring 代理。

所以学会理解代理以后,很多 Spring 注解问题就能一起理解。


@Async 和 @Transactional 同时使用

例如:

@Async("pdfTaskExecutor")
@Transactional
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {

    saveTaskStatus();

    String targetPath =
            processPdf(file);

    updateTaskResult(targetPath);

    return CompletableFuture.completedFuture(
            PdfTaskResult.success(
                    file.getName(),
                    targetPath,
                    0
            )
    );
}

当外部通过代理调用这个方法时,可能同时经过:

异步拦截器;

事务拦截器。

最终业务方法在线程池线程中执行,并在线程池线程中建立事务。

但原调用线程中的事务不会自动传播到异步线程。

需要记住:

事务通常绑定在线程上;

@Async 会切换线程;

原事务上下文不会自然跨线程传递。

所以异步方法中的事务通常是一个新的事务边界。


调用异步方法后,外层事务可能还没提交

例如:

@Transactional
public void createTask() {

    PdfTask task =
            saveTask();

    pdfAsyncService.processPdfAsync(
            task.getId()
    );
}

这里可能发生:

外层事务保存任务;

事务还没有提交;

异步线程已经开始执行;

异步线程根据 taskId 查询数据库;

查询不到尚未提交的数据。

因此,不能简单认为调用了:

saveTask();

异步线程就一定能马上查到这条数据。

比较稳妥的思路是:

先完成并提交事务;

事务提交成功后;

再启动异步任务。

后面可以通过事务事件来实现:

@TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT
)

如何检查拿到的是不是代理对象

可以临时打印:

System.out.println(
        pdfAsyncService.getClass()
);

可能看到类似:

class com.succos.service.PdfAsyncService$$SpringCGLIB$$0

类名中出现:

SpringCGLIB

通常说明这是 Spring 创建的 CGLIB 代理对象。

也可以使用:

AopUtils.isAopProxy(
        pdfAsyncService
);

例如:

import org.springframework.aop.support.AopUtils;

boolean proxy =
        AopUtils.isAopProxy(
                pdfAsyncService
        );

System.out.println(
        "是否为代理对象:"
                + proxy
);

如果输出:

是否为代理对象:true

说明注入的对象经过了 Spring AOP 代理。

这些代码适合调试和理解,不需要长期放在业务逻辑中。


一个完整的代理验证示例

AsyncProxyService

package com.succos.service;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncProxyService {

    public void outerMethod() {

        System.out.println(
                "outerMethod 线程:"
                        + Thread.currentThread().getName()
        );

        innerAsyncMethod();
    }

    @Async("pdfTaskExecutor")
    public void innerAsyncMethod() {

        System.out.println(
                "innerAsyncMethod 线程:"
                        + Thread.currentThread().getName()
        );
    }
}

AsyncProxyController

package com.succos.controller;

import com.succos.service.AsyncProxyService;
import org.springframework.aop.support.AopUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/proxy-test")
public class AsyncProxyController {

    private final AsyncProxyService
            asyncProxyService;

    public AsyncProxyController(
            AsyncProxyService asyncProxyService
    ) {
        this.asyncProxyService =
                asyncProxyService;
    }

    @GetMapping("/class")
    public String printProxyClass() {

        System.out.println(
                "对象类型:"
                        + asyncProxyService
                        .getClass()
                        .getName()
        );

        System.out.println(
                "是否为代理对象:"
                        + AopUtils.isAopProxy(
                        asyncProxyService
                )
        );

        return "success";
    }

    @GetMapping("/internal")
    public String internalCall() {

        asyncProxyService.outerMethod();

        return "success";
    }

    @GetMapping("/external")
    public String externalCall() {

        asyncProxyService
                .innerAsyncMethod();

        return "success";
    }
}

访问:

GET /proxy-test/internal

可能输出:

outerMethod 线程:http-nio-8080-exec-1
innerAsyncMethod 线程:http-nio-8080-exec-1

说明内部调用没有异步。

访问:

GET /proxy-test/external

可能输出:

innerAsyncMethod 线程:pdf-worker-1

说明外部通过代理调用,异步生效。


为什么不能只看方法上有没有注解

刚开始排查时,容易只检查:

@Async

有没有写。

但真正应该检查的是:

1. 是否添加了 @EnableAsync;

2. 当前类是否由 Spring 管理;

3. 调用的是不是 Spring 注入的对象;

4. 方法调用是否经过代理;

5. 是否发生了同类内部调用;

6. @Async 指定的线程池 Bean 是否存在;

7. 方法是否适合被代理;

8. 日志中的线程名是否真的变化。

所以判断 @Async 是否生效,不能只看代码表面。

最直接的验证方式是打印:

Thread.currentThread().getName()

@Async 失效排查清单

如果发现异步方法没有进入线程池,可以依次检查。

是否开启异步支持

@EnableAsync

当前类是否为 Spring Bean

需要有:

@Service

或者:

@Component

不能自己创建:

new PdfAsyncService()

是否由其他 Bean 调用

推荐:

pdfAsyncService.processPdfAsync(file);

避免同类内部:

this.processPdfAsync(file);

方法是否为 public

推荐:

public CompletableFuture<PdfTaskResult>
processPdfAsync(File file)

避免把异步方法写成 private


执行器名称是否正确

@Async("pdfTaskExecutor")

必须对应:

@Bean(name = "pdfTaskExecutor")

线程名称是否发生变化

调用线程:

http-nio-8080-exec-1

异步线程:

pdf-worker-1

如果线程名完全相同,需要继续检查代理是否生效。


有没有在异步方法调用后立即阻塞

例如:

pdfAsyncService
        .processPdfAsync(file)
        .join();

异步方法本身可能已经生效,但当前线程又立即调用 join() 等待结果。

这不叫异步失效,而是调用方主动阻塞等待。

需要区分:

方法有没有在线程池执行;

调用线程有没有等待异步结果。

这是两个问题。


最推荐的项目结构

对于当前 PDF 项目,可以使用下面的结构:

PdfTaskController
        ↓
PdfTaskService
        ↓
PdfAsyncService 代理对象
        ↓
pdfTaskExecutor
        ↓
真正的 PDF 处理方法

PdfTaskController

负责接收请求。

@RestController
@RequestMapping("/pdf")
public class PdfTaskController {

    private final PdfTaskService
            pdfTaskService;

    public PdfTaskController(
            PdfTaskService pdfTaskService
    ) {
        this.pdfTaskService =
                pdfTaskService;
    }

    @PostMapping("/watermark")
    public CompletableFuture<PdfTaskResult>
    watermark(
            @RequestParam String fileName
    ) {
        return pdfTaskService
                .submitPdf(fileName);
    }
}

PdfTaskService

负责组织任务。

@Service
public class PdfTaskService {

    private final PdfAsyncService
            pdfAsyncService;

    public PdfTaskService(
            PdfAsyncService pdfAsyncService
    ) {
        this.pdfAsyncService =
                pdfAsyncService;
    }

    public CompletableFuture<PdfTaskResult>
    submitPdf(String fileName) {

        File file =
                new File(
                        "input",
                        fileName
                );

        return pdfAsyncService
                .processPdfAsync(file);
    }
}

PdfAsyncService

负责异步执行。

@Service
public class PdfAsyncService {

    @Async("pdfTaskExecutor")
    public CompletableFuture<PdfTaskResult>
    processPdfAsync(File file) {

        PdfTaskResult result =
                processPdf(file);

        return CompletableFuture
                .completedFuture(result);
    }

    private PdfTaskResult processPdf(
            File file
    ) {
        // 真正的 PDF 水印处理逻辑

        return PdfTaskResult.success(
                file.getName(),
                "output/test-watermark.pdf",
                0
        );
    }
}

这个结构既避免了同类调用问题,也让职责比较清楚。


这一节小结

这一节我主要记住几点:

1. @Async 的异步能力由 Spring 代理对象提供,不是方法自己提供;
2. 外部 Bean 调用异步方法时,会先经过 Spring 代理;
3. 同类内部调用相当于 this.method(),不会重新经过代理;
4. 没有经过代理,@Async 拦截器就无法把任务提交到线程池;
5. 最推荐的解决方式是把异步方法拆到独立的 Service;
6. 也可以直接使用 CompletableFuture.supplyAsync,避免依赖代理;
7. 不推荐通过自注入、ApplicationContext 或 AopContext 绕过代理问题;
8. private、final 方法可能影响基于代理的增强;
9. @Async、@Transactional 等注解都可能遇到同类调用失效;
10. 判断异步是否生效,最直接的方法是观察调用线程和执行线程名称。

用一句话总结:

@Async 不是看到注解就一定异步,只有方法调用真正经过 Spring 代理对象,异步拦截逻辑才会生效。

下一节继续学习:

@Async 方法为什么不能直接传 MultipartFile;

请求结束后临时文件可能发生什么;

怎样先把上传文件保存到磁盘,再安全提交后台任务。

这会把 Spring Web 文件上传和异步 PDF 处理连接起来。

45. 为什么 @Async 同类调用会失效:Spring 代理机制 - Java并发之从Thread到CompletableFuture - 上下文网