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 处理连接起来。