第十五章. common-core 核心配置:@Async 异步调用详解

第十五章. common-core 核心配置:@Async 异步调用详解

摘要:本章我们将深入 Spring 的 @Async 异步调用体系。我们将分析 RVP 5.x 如何 摒弃 旧的 AsyncConfig,转而使用 Spring Boot 3+ 的 自动化配置spring.task.execution)来管理 @Async 线程池。最后,我们将实战“@Async 内部调用失效”的经典陷阱及其解决方案。

在上一章(第十四章)中,我们深入剖析了 RVP 的 ThreadPoolConfig,从七大参数到 RVP 5.x 源码中 ScheduledExecutorService 的配置细节。我们最终掌握了 executesubmitschedule 这三种 手动 调用线程池的方式。

手动调用固然灵活,但在日常开发中,我们更希望有一种“更优雅”的方式。

@Async 注解就是 Spring 提供的答案。它允许我们 再手动注入 Executor 并调用 execute(),而是通过一个 简单的注解,就能让方法“自动”地在后台线程池中异步执行。

本章,我们将 严格基于 RVP 5.x 的最新源码,重新梳理 @Async 的配置体系,并深入探究 @Async 最著名 的“内部调用失效”陷阱及其解决方案。

本章学习路径

@Async 异步调用详解


15.1. RVP 5.x 异步架构的演进

要理解 RVP 5.x 的配置,我们必须先知道它“抛弃”了什么。

15.1.1. 【RVP 4.x 回顾】AsyncConfigurer 的手动绑定时代

在 Spring Boot 3 之前,@Async 注解默认是 不会 自动关联到我们 ThreadPoolConfig 里创建的那个线程池的。

为了让 @Async 使用我们自定义的线程池,RVP 4.x 必须:

  1. ThreadPoolConfig.java:手动创建一个 ThreadPoolTaskExecutor 类型的 Bean,并命名(例如 threadPoolTaskExecutor)。
  2. AsyncConfig.java:创建一个新类,实现 AsyncConfigurer 接口,并重写 getAsyncExecutor() 方法,手动@Async 注解的默认执行器指向 threadPoolTaskExecutor 这个 Bean。

这个过程需要两个类,配置繁琐且分散。

15.1.2. 【RVP 5.x 演进】Spring Boot 3+ 的“自动化”时代

RVP 5.x 采用了 Spring Boot 3+ 的 自动化配置,彻底抛弃了 AsyncConfig.java

这是一个巨大的简化。现在,Spring Boot 允许我们直接在 application.yml 中配置 @Async 的默认线程池。AsyncConfigurer 接口及其“手动绑定”的时代已经过去了。


15.2. RVP 5.x @Async 自动配置解析

我们来看 RVP 5.x 中让 @Async 跑起来的 两个 关键文件。

15.2.1. application.ymlspring.task.execution 属性详解

第一个文件是 ruoyi-admin/src/main/resources/application.yml

1
2
3
4
5
6
7
8
spring:
# ...
task:
execution:
# Spring Boot 自动配置的 @Async 线程池
thread-name-prefix: async-
# 由spring自己初始化线程池
mode: force

spring.task.execution 是 Spring Boot 3+ 原生 的异步线程池配置命名空间。

  • thread-name-prefix: async-
    • 作用:设置 @Async 默认线程池的 线程名前缀
    • 分析:这非常重要。当我们在日志中看到 [async-1] 这样的线程名时,我们就立刻知道这个任务是由 @Async 自动调度的,而不是由我们上一章配置的 [schedule-pool-1] 调度的。
  • mode: force
    • 作用:强制 Spring Boot 总是 初始化它自己的 TaskExecutor Bean。
    • 分析:这确保了 @Async 有一个 专属 的、与 RVP ScheduledExecutorService(用于定时任务)隔离 的线程池。

15.2.2. @EnableAsync 配置解析

第二个文件是 ruoyi-common-core 中的配置类:

文件路径ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java

1
2
3
4
5
6
@AutoConfiguration
@EnableAspectJAutoProxy
@EnableAsync(proxyTargetClass = true)
public class ApplicationConfig {

}
  • @EnableAsync:这是 开启异步功能 的“总开关”。没有这个注解,Spring Boot 会完全 忽略 @Async 注解。
  • proxyTargetClass = true:这是 强制 Spring AOP 使用 CGLIB 代理
    • 为什么必须是 CGLIB? Spring AOP 有两种代理方式:
      1. JDK 动态代理:要求目标类 必须 实现一个接口。
      2. CGLIB 代理:通过 继承 目标类来创建代理,不需要 接口。
    • @Async 的原理是 AOP 代理。如果我们不强制 proxyTargetClass = true,并且我们的 SysLogininforServiceImpl 没有 实现 ISysLogininforService 接口,那么 Spring 将 无法 为它创建 JDK 代理,导致 @Async 完全失效
    • true 确保了 @Async 能够应用于 RVP 中的 所有 Bean,无论它们是否实现了接口。

15.2.3. @Async 的 AOP 代理原理

当我们配置了 @EnableAsync 后,Spring 的 AOP 机制就开始工作了:

  1. 扫描:Spring 启动时,AsyncAnnotationBeanPostProcessor(一个后置处理器)会扫描所有 Bean,查找 @Async 注解。
  2. 创建代理:当它在 AsyncService 上找到 @Async 方法时,它 不会 直接将 AsyncService 实例放入容器。
  3. 包装:它会创建一个 AsyncServiceCGLIB 代理对象AsyncService$$Proxy),这个代理对象 包装 了真实的 AsyncService 实例。
  4. 注入:当 AsyncController 注入 @Autowired private AsyncService asyncService 时,它获取到的是这个 代理对象
  5. 拦截:当 Controller 调用 asyncService.method1() 时:
    • 调用被 代理对象AsyncService$$Proxy)的 AsyncExecutionInterceptor(异步执行拦截器)拦截
    • 拦截器 不会 立即执行 method1
    • 它将“调用 method1”这个动作打包成一个 CallableRunnable 任务。
    • 它将这个任务提交给 spring.task.executionasync- 线程池)去执行。
    • 拦截器 立即Controller 返回 null(如果方法是 void)或一个 Future(如果方法有返回值)。
  6. 结果Controller (主线程) 认为方法已“执行”完毕并立即返回,而 method1 的真正逻辑此时才在 async-(子线程)中开始执行。

15.3. 【RVP 真实场景】@Async 在 RVP 中的应用

@Async 在 RVP 框架中有大量应用,最典型的就是“异步写日志”。

15.3.1. 源码追踪:SysLogininforServiceImpl.recordLogininfor()

我们可以在 system 模块中找到这个真实案例。

文件路径ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class SysLogininforServiceImpl implements ISysLogininforService {

@Autowired
private SysLogininforMapper baseMapper;

/**
* 新增系统登录日志
* ...
*/
@Async // 【核心】
@Override
public void recordLogininfor(String username, String status, ...) {
// ... (省略构建 SysLogininfor 对象的代码) ...

// 核心:数据库 INSERT 操作
baseMapper.insert(logininfor);
}
}

15.3.2. 痛点分析:为何“记录登录日志”必须异步?

这个场景完美诠释了 @Async 的价值:

  1. 主流程SysLoginService.login() 是用户登录的核心流程,它必须 尽快 返回 Token,让用户登录。
  2. 非核心流程recordLogininfor() 只是一个“审计”功能(记录日志),它涉及一次数据库 INSERT 操作。
  3. 痛点INSERT 操作会涉及数据库连接、网络 IO、磁盘写入,可能耗时 20ms 到 100ms 不等。如果让 login() 主流程 同步 等待这 100ms,用户的登录体验会明显变差。
  4. @Async 解决方案login() 方法调用 recordLogininfor() 时,AOP 代理会 立即 返回,login() 方法可以继续执行并返回 Token。而 baseMapper.insert() 的数据库操作,会“稍后”在 async- 线程池中执行,完全不阻塞 用户的登录流程。

15.4. 【二开核心】@Async 内部调用失效(The “AOP Trap”)

这个陷阱是 Spring AOP 中最常遇到的问题,@Transactional 事务注解也会有同样的问题

15.4.1. 测试准备:创建 AsyncControllerAsyncService

为了完整复现这个陷阱,我们不能只使用 main 方法,必须在 Spring 容器中进行。我们将在 ruoyi-demo 模块中创建 AsyncControllerAsyncService

文件路径ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/AsyncService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.dromara.demo.service;

import cn.hutool.core.thread.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
* 异步调用实战 Service
*/
@Slf4j
@Service
public class AsyncService {

/**
* 这是一个 @Async 方法,它将在 'async-' 线程池中执行
*/
@Async
public void methodB() {
// 打印当前线程名,验证是否为子线程
log.info("methodB 开始执行... 线程: {}", Thread.currentThread().getName());
ThreadUtil.sleep(3, TimeUnit.SECONDS); // 模拟耗时 3 秒
log.info("methodB 执行完毕.");
}

}

文件路径ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/AsyncController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.dromara.demo.controller;

import cn.dev33.satoken.annotation.SaIgnore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.demo.service.AsyncService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @Async 注解实战
*/
@SaIgnore
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/async")
public class AsyncController {

private final AsyncService asyncService;

// ... 我们将在这里添加测试接口 ...
}

15.4.2. 【正确演示】Controller -> Service.@Async (AOP 代理生效)

首先,我们演示 正确 的调用方式:Controller (外部类) 调用 Service (内部类) 的 @Async 方法。

AsyncController.java 中添加 test1 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 位于 AsyncController.java

/**
* 场景一:【正确】外部调用 @Async 方法
*/
@GetMapping("/test1")
public R<String> test1() {
log.info("test1 (Controller) 开始执行... 线程: {}", Thread.currentThread().getName());

// 调用 asyncService 的 methodB (它有 @Async)
asyncService.methodB();

log.info("test1 (Controller) 执行完毕.");
return R.ok("操作成功 (立即返回)");
}

验证

  1. 启动 DromaraApplication
  2. 调用 GET http://localhost:8080/async/test1

控制台日志输出

1
2
3
4
5
6
7
8
... [main] INFO ... test1 (Controller) 开始执行... 线程: main
... [main] INFO ... test1 (Controller) 执行完毕.
// 立即返回 "操作成功"
...
// (过了 1 毫秒后)
... [async-1] INFO ... methodB 开始执行... 线程: async-1
// (又过了 3 秒后)
... [async-1] INFO ... methodB 执行完毕.

分析:完美!test1 (主线程 main) 立即 返回了。methodB 被 AOP 拦截器“抛”给了 async-1(子线程)执行,实现了异步。

15.4.3. 【错误演示】Service.methodA() -> this.methodB() (AOP 代理失效)

现在,我们来复现这个“陷阱”。

场景:我们在 AsyncService 内部,从一个 没有 @Async 注解的 methodA,去调用 @Async 注解的 methodB

步骤 1:修改 AsyncService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 位于 AsyncService.java
@Slf4j
@Service
public class AsyncService {

/**
* 【新增】一个没有 @Async 的普通方法
*/
public void methodA() {
log.info("methodA 开始执行... 线程: {}", Thread.currentThread().getName());

// 【关键错误】:使用 this 在【类内部】调用 @Async 方法
log.info("methodA 准备调用 this.methodB()...");
this.methodB();

log.info("methodA 执行完毕.");
}

@Async
public void methodB() {
// (代码同上)
log.info("methodB 开始执行... 线程: {}", Thread.currentThread().getName());
ThreadUtil.sleep(3, TimeUnit.SECONDS);
log.info("methodB 执行完毕.");
}
}

步骤 2:修改 AsyncController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 位于 AsyncController.java
/**
* 场景二:【错误】内部调用 @Async 方法
*/
@GetMapping("/test2")
public R<String> test2() {
log.info("test2 (Controller) 开始执行... 线程: {}", Thread.currentThread().getName());

// 调用 asyncService 的 methodA (它没有 @Async)
asyncService.methodA();

log.info("test2 (Controller) 执行完毕.");
return R.ok("操作成功 (3 秒后返回)");
}

验证

  1. 重启 DromaraApplication
  2. 调用 GET http://localhost:8080/demo/async/test2

结果:浏览器会“转圈” 3 秒钟才返回!

控制台日志输出

1
2
3
4
5
6
7
8
... [main] INFO ... test2 (Controller) 开始执行... 线程: main
... [main] INFO ... methodA 开始执行... 线程: main
... [main] INFO ... methodA 准备调用 this.methodB()...
... [main] INFO ... methodB 开始执行... 线程: main
// (主线程在这里同步等待了 3 秒)
... [main] INFO ... methodB 执行完毕.
... [main] INFO ... methodA 执行完毕.
... [main] INFO ... test2 (Controller) 执行完毕.

分析
@Async 完全失效了! methodB 并不是在 async-1 子线程执行的,而是在 main 主线程 同步 执行的。

15.4.4. 痛点分析:this 引用的是“原始对象”,不是“代理对象”

为什么会这样?我们必须回顾 15.2.3 节的 AOP 代理原理:

  1. Spring 启动时,为 AsyncService 创建了一个 代理对象 AsyncService$$Proxy,并把它注入给了 AsyncController
  2. Controller 调用 asyncService.methodA() 时,它调用的是 代理对象
  3. 代理对象 发现 methodA 没有 @Async 注解,于是 AOP 拦截器 放行,调用 原始对象methodA()
  4. 【关键】:程序进入 AsyncService 类的 原始对象 内部。当执行到 this.methodB() 时:
    • this 关键字,永远指向“当前这个类的原始实例”(AsyncService)。
    • 它调用的 不是 AsyncService$$Proxy.methodB()
    • 它调用的是 原始 AsyncService.methodB()

结论this 调用 绕过 了 AOP 代理,@Async 拦截器根本没有机会触发,导致 methodB 沦为了一个普通的同步方法。


15.5. 解决 @Async 内部调用失效

我们已经知道了失败的根源:this 引用的是“原始对象”。那么解决方案就呼之欲出了:我们必须在 methodA 内部,想办法拿到“代理对象”,然后用 代理对象去调用 methodB

15.5.1. 方案一 (RVP 推荐): SpringUtils.getAopProxy(this)

我们在 第四章 学习的 SpringUtils,它的核心功能之一 getAopProxy() 就是为了解决这个 AOP“内部调用失效”的经典难题而生的。

步骤 1:修改 AsyncService.java
我们添加一个 methodC 来演示正确用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 位于 AsyncService.java
// ...
import org.dromara.common.core.utils.SpringUtils;
// ...

@Service
public class AsyncService {

// ... methodA() 和 methodB() ...

/**
* 【正确演示】: 通过 SpringUtils 获取 AOP 代理
*/
public void methodC() {
log.info("methodC 开始执行... 线程: {}", Thread.currentThread().getName());

log.info("methodC 准备通过【AOP 代理】调用 methodB()...");

// 1. 【核心】使用 SpringUtils 获取当前对象的 AOP 代理
// 告诉 SpringUtils:“请把 Spring 容器中那个代理我的对象给我”
AsyncService proxy = SpringUtils.getAopProxy(this);

// 2. 使用代理对象去调用
proxy.methodB();

log.info("methodC 执行完毕.");
}
}

步骤 2:修改 AsyncController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 位于 AsyncController.java
/**
* 场景三:【正确】解决内部调用失效
*/
@GetMapping("/test3")
public R<String> test3() {
log.info("test3 (Controller) 开始执行... 线程: {}", Thread.currentThread().getName());

// 调用 asyncService 的 methodC (它会间接调用 @Async 的 methodB)
asyncService.methodC();

log.info("test3 (Controller) 执行完毕.");
return R.ok("操作成功 (立即返回)");
}

验证

  1. 重启 DromaraApplication
  2. 调用 GET http://localhost:8080/async/test3

控制台日志输出

1
2
3
4
5
6
7
8
9
10
11
... [main] INFO ... test3 (Controller) 开始执行... 线程: main
... [main] INFO ... methodC 开始执行... 线程: main
... [main] INFO ... methodC 准备通过【AOP 代理】调用 methodB()...
... [main] INFO ... methodC 执行完毕.
... [main] INFO ... test3 (Controller) 执行完毕.
// 立即返回 "操作成功"
...
// (过了 1 毫秒后)
... [async-1] INFO ... methodB 开始执行... 线程: async-1
// (又过了 3 秒后)
... [async-1] INFO ... methodB 执行完毕.

结论methodCmain 线程)立即返回了!methodB 成功地在 async-1 子线程中执行。AOP 代理生效!

15.5.2. 方案二 (Spring 常用): 注入自己

另一种在 Spring 中常见的(但不那么优雅的)解决方案是“自己注入自己”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 位于 AsyncService.java
import org.springframework.beans.factory.annotation.Autowired;
// ...

@Service
public class AsyncService {

// 【核心】自己注入自己
// Spring 在注入时,会注入 AOP 代理对象
@Autowired
private AsyncService selfProxy;

// ...

public void methodD() {
log.info("methodD 开始执行...");

// 使用注入的代理对象 selfProxy,而不是 this
selfProxy.methodB();

log.info("methodD 执行完毕.");
}
}

分析:这种方式同样可行,因为 Spring 注入的 selfProxyAsyncService$$Proxy 代理对象,调用 selfProxy.methodB() 同样可以触发 AOP 拦截。

RVP 框架更推荐使用 SpringUtils.getAopProxy(this),因为它不需要为 Service 额外增加一个 @Autowired 字段


15.6. @Async 进阶:指定线程池与异常处理

到目前为止,我们使用的 @Async(不带参数)都会被 Spring Boot 自动调度到它所配置的 async- 线程池中。

15.6.1. 场景:async- (默认池) vs scheduledExecutorService (RVP 定时池)

在我们的 RVP 框架中,现在至少存在 两个 线程池:

  1. async- 线程池:由 application.ymlspring.task.execution)自动配置,专门用于 @Async
  2. scheduledExecutorService:由 ThreadPoolConfig.java 手动配置,Bean 名称为 "scheduledExecutorService",用于 RVP 的定时和延迟任务。

如果我们的业务场景中,有一个 @Async 任务 希望和 其他 @Async 任务(例如 recordLogininfor)竞争 async- 线程池的资源,而是希望使用 RVP 提供的 scheduledExecutorService 线程池(schedule-pool- 前缀),该怎么办?

15.6.2. 实战:@Async("bean") (指定线程池)

@Async 注解允许我们传入一个 字符串 参数,这个字符串就是 目标线程池的 Bean 名称

步骤 1:修改 AsyncService.java
我们添加一个 methodE,并显式指定它使用 "scheduledExecutorService"

1
2
3
4
5
6
7
8
9
10
11
12
// 位于 AsyncService.java

/**
* 【进阶】: @Async 指定线程池 Bean 名称
* 这个任务将由 RVP 的 "schedule-pool-" 线程池执行
*/
@Async("scheduledExecutorService")
public void methodE() {
log.info("methodE 开始执行... 线程: {}", Thread.currentThread().getName());
ThreadUtil.sleep(1, TimeUnit.SECONDS);
log.info("methodE 执行完毕.");
}

步骤 2:修改 AsyncController.java
我们添加一个 test4 接口来调用 methodB (默认池) 和 methodE (指定池)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 位于 AsyncController.java

/**
* 场景四:【进阶】演示 @Async 指定不同线程池
*/
@GetMapping("/test4")
public R<String> test4() {
log.info("test4 (Controller) 开始执行... 线程: {}", Thread.currentThread().getName());

// 1. 调用 methodB,它将使用 Spring 自动配置的 [async-] 池
asyncService.methodB();

// 2. 调用 methodE,它将使用 RVP 手动配置的 [schedule-pool-] 池
asyncService.methodE();

return R.ok("操作成功 (立即返回)");
}

验证

  1. 重启 DromaraApplication
  2. 调用 GET http://localhost:8080/demo/async/test4

控制台日志输出

1
2
3
4
5
6
7
8
... [main] INFO ... test4 (Controller) 开始执行... 线程: main
// (过了 1 毫秒后)
... [async-1] INFO ... methodB 开始执行... 线程: async-1
... [schedule-pool-1] INFO ... methodE 开始执行... 线程: schedule-pool-1
// (又过了 1 秒后)
... [schedule-pool-1] INFO ... methodE 执行完毕.
// (又过了 2 秒后)
... [async-1] INFO ... methodB 执行完毕.

分析
@Async("beanName") 的方式成功地实现了 线程池隔离methodBmethodE两个完全不同 的线程池中并发执行。这在“二开”中对于隔离高、低优先级的异步任务非常有用。

15.6.3. 异步异常处理

最后一个关键问题:如果 @Async 标记的方法(void 返回值)内部抛出了异常,主线程会知道吗?

不会。

@Async 的 AOP 拦截器会 捕获 子线程的异常,主线程(Controller)会 毫不知情 并正常返回 200 OK

实战:演示“静默”异常

步骤 1:修改 AsyncService.java
我们添加一个 methodF,它会抛出 RuntimeException

1
2
3
4
5
6
7
8
9
10
11
// 位于 AsyncService.java

/**
* 【进阶】: @Async 方法抛出异常
*/
@Async
public void methodF() {
log.info("methodF 开始执行... 线程: {}", Thread.currentThread().getName());
// 模拟异常
throw new RuntimeException("这是一个故意的异步异常");
}

步骤 2:修改 AsyncController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 位于 AsyncController.java

/**
* 场景五:【进阶】测试 @Async 异常处理
*/
@GetMapping("/test5")
public R<String> test5() {
log.info("test5 (Controller) 开始执行...");

asyncService.methodF();

log.info("test5 (Controller) 执行完毕.");
return R.ok("操作成功 (主线程并不知道异常)");
}

验证

  1. 重启 DromaraApplication
  2. 调用 GET http://localhost:8080/demo/async/test5

结果

  • 浏览器立即 收到 200 OK 和“操作成功”的响应。
  • 控制台日志
    1
    2
    3
    4
    5
    6
    7
    8
    9
    ... [main] INFO ... test5 (Controller) 开始执行...
    ... [main] INFO ... test5 (Controller) 执行完毕.
    // (过了 1 毫秒后)
    ... [async-2] INFO ... methodF 开始执行... 线程: async-2
    // (Spring Boot 的默认处理器捕获并打印了错误)
    ... [async-2] ERROR ... Unexpected exception occurred invoking async method 'methodF'
    java.lang.RuntimeException: 这是一个故意的异步异常
    at org.dromara.demo.service.AsyncService.methodF(AsyncService.java:...)
    ...

分析:主线程 毫不知情,API 正常返回了 200。异常只在后台 日志 中打印(RVP 4.x 的 AsyncConfig 会捕获此异常并抛出 ServiceException,但 RVP 5.x 依赖 Spring Boot 3+ 的默认行为,即只记录日志)。

【二开守则】如何正确处理异步异常?
如果 methodF 的成功与否 非常重要,你 不应该 将其声明为 void。你应该将其声明为 Future<?>

1
2
3
4
5
6
7
8
@Async
public Future<Void> methodG() {
log.info("methodG 执行...");
if (true) {
throw new RuntimeException("异步异常");
}
return new AsyncResult<>(null); // Spring 提供的 Future 实现
}

调用方(例如 ControllerService)在调用 methodG() 后,可以通过 Future.get() 来捕获 ExecutionException,从而 感知 到异步任务的失败。


15.7. 本章总结

在本章中,我们深入了 RVP 5.x 框架下的 @Async 异步调用体系。

  1. 架构演进:我们明确了 RVP 5.x 摒弃 了 4.x 时代的 AsyncConfig(手动绑定),转而依赖 Spring Boot 3+ 原生spring.task.executionapplication.yml)来 自动配置 @Async 的默认线程池(async- 前缀)。
  2. RVP 配置:RVP 的 ApplicationConfig 负责 @EnableAsync(开启 AOP 代理),ThreadPoolConfig 负责 ScheduledExecutorService(定时任务池)。
  3. AOP 原理@Async 是通过 AOP 代理拦截器 实现的。拦截器将方法调用“抛”入线程池,主线程立即返回。
  4. 真实场景:RVP 在 SysLogininforServiceImpl 中使用 @Async 异步写入登录日志,避免了 INSERT 操作阻塞用户登录主流程。
  5. 核心陷阱:我们实战了“内部调用失效”——this.methodB()绕过 AOP 代理,导致 @Async 失效。
  6. 核心解决方案:在 RVP 中,我们应使用 SpringUtils.getAopProxy(this).methodB() 来获取 代理对象,从而正确触发异步。
  7. 进阶用法
    • @Async("scheduledExecutorService"):通过指定 Bean 名称,将任务调度到 RVP 的 定时任务池schedule-pool-)。
    • @Async void 方法的异常会被 静默 处理(只打印日志)。必须返回 Future 并调用 get(),才能在主线程中捕获 ExecutionException