Async 异步任务注解类的用法及原理分析

背景

看项目源码发现有一个 @Async 注解,它是 Spring 的注解,作用是用线程池执行注解的方法体,底层是动态代理。

之前不知道这个知识点,小小测试了一下,发现项目中这个注解的用法是错误的,本文来理一理它的原理、正确用法及注意事项。

关键问题:

  1. Async 原理是什么?AOP 代理,调用链路比较长,知晓关键类 AsyncAnnotationPostBeanProcessor 即可。
  2. Async 基本用法及失效场景:主类添加 @EnableAsync、Async 方法必须和调用方位于不同类中,否则注解失效。
  3. Spring 为执行异步任务内置的线程池参数是什么?默认队列长度 Integer.MAX_VALUE 、核心线程数为 8。
  4. Spring 为执行异步任务创建的线程池是什么时候关闭的?Spring 容器注入的线程池,容器也会负责在应用程序退出时自动关闭。
  5. 如何修改异步任务默认线程池的配置?默认队列长度有 OOM 风险,使用需要权衡。异步任务的默认线程池配置类为TaskExecutionProperties,可以通过 spring.task.execution 进行调整。
  6. 注解失效的原理是什么?参考最后一部分。

基本用法

Async 注解的用法很简单,但是需要注意注解失效的情况,具体步骤:

  1. 在应用启动类上添加 @EnableAsync 注解,开启异步任务功能。
  2. 在需要异步方式执行的方法上添加 @Async 注解,且不能在当前类的其他方法中直接调用@Async 注解的方法 , 否则该注解无效。类似的还有事务注解 @Transactional 等,也是一样的原理。它的参数值是执行当前任务的线程池实例的 beanName,非空时用指定线程池,默认使用 Spring 内置的线程池,beanName=applicationTaskExecutor。

第一步,搭建一个简单的 SpringBoot Web 引用,应用启动类上加上启动注解。

第二步,写一个简单的Demo ,定义一个提供异步任务的服务类 AsyncService,内容如下:

java 复制代码
@Configuration
public class AsyncService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Bean(value = "myThreadPool")
    public Executor myExecutor() {
        return Executors.newSingleThreadExecutor();
    }

    @Async(value = "myThreadPool")
    public void testInsert() {
        logger.info("Thread name {}", Thread.currentThread().getName());
        logger.info("Test async annotation start.");

        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        logger.info("Test async annotation end.");
    }
}

第三步,创建 Controller 请求方法中调用异步方法:

java 复制代码
@RestController
public class IndexController {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource
    private AsyncService asyncService;

    @GetMapping("/get")
    public Object Index1(HttpServletRequest request){
        asyncService.testInsert();

        logger.info("request get url.");
        return request.getSession().getAttribute("userUid");
    }
}

访问 get 请求时,异步任务休眠20秒,该请求立即返回,这就达到了「耗时操作异步执行、页面请求立即响应」的目的。

启用过程

从入口注解 EnableAsync 开始跟踪,梳理实现异步任务的关键类。

1、 EnableAsync 引入了 AsyncConfigurationSelector : 2、AsyncConfigurationSelector 又导入异步支持配置类 ProxyAsyncConfiguration: 3、ProxyAsyncConfiguration ,这个我们就很熟悉了,它注入了一个 AsyncAnnotationBeanPostProcessor 后置处理器,也是实现注解方式执行异步任务的关键类:

核心类 AsyncAnnotationBeanPostProcessor

AsyncAnnotationBeanPostProcessor 是穿起 Spring 的 AOP 和异步任务实现的重要类,它有三种能力,按被容器触发的顺序依次是:

  1. BeanPostProcessor,注册到 Spring 的默认 Bean 工厂的一个后置处理器,所有实例化完成的 Bean 都会传入后置处理器列表走一遍加固。
  2. BeanFactoryAware,实现该接口的类在初始化过程中会被容器调用 setFactory 方法注入工厂本身。
  3. AbstractAdvisingBeanPostProcessor 的子类,具有 AOP 的织入能力,包含一个成员变量 Advisor,穿起 AOP 的一对 Advice 和 Pointcut 。

从它的初始化调用链入手,简单看看三种功能对应的核心代码。在 ProxyAsyncConfiguration 类的 AsyncAnnotationBeanPostProcessor asyncAdvisor() 方法中打断点,跟踪这个类初始化调用链。

第一部分,BeanPostProcessor 能力。 容器启动时,所有实现 BeanPostProcessor 接口的类都需要预先注册,由 PostProcessorRegistrationDelegate.registerBeanPostProcessors 方法完成。由于 ProxyAsyncConfiguration 通过 @Bean 的定义声明了 AsyncAnnotationBeanPostProcessor ,所以该类也被预先注册到容器中。 它拿到全部注册的 postProcessorNames 名称集合,然后逐个调用 beanFactory.getBean 创建对象并添加到工厂的处理器集合中。

第二部分,BeanFactoryAware 能力 。AsyncAnnotationBeanPostProcessor 初始化过程,由于它实现了 BeanFactoryAware 接口,由工厂类的 invokeAwareMethods 触发 setFactory: setFactory 方法中创建了 AsyncAnnotationAdvisor 对象,并设置给自己的成员变量。

第三部分,AbstractAdvisingBeanPostProcessor 能力

AOP 能力由两部分组成,一个是 AsyncAnnotationAdvisor ,它穿起 Advice 和 Pointcut,信息如下: 组装了一个 AnnotationAsyncExecutionInterceptor 增强 ,切点是 org.springframework.scheduling.annotation.Async 注解。增强的能力是,向工作线程提交一个执行原有方法的任务: 其次,AbstractAdvisingBeanPostProcessor 的后置处理方法,对于满足 isEligible() 条件的 bean ,先准备一个代理工厂,然后将 AsyncAnnotationAdvisor 设置为代理工厂的切点,并返回一个继承自该 bean 所属类的代理类:

异步方法所在的类创建过程

Spring 容器在 bean 初始化完成后,会执行 applyBeanPostProcessor 方法,遍历 Bean 工厂的后置处理器列表,依次调用它们的 postProcessAfterInitialization 方法。

而大部分的后置处理器都是空处理,直接返回目标 bean ,那些被特定后置处理器关心的 Bean 会被执行额外的操作。

前面的 Demo 中,AsyncService 类在初始化完成后, 遇到 AsyncAnnotationBeanPostProcessor 这个处理器,AsyncService 的类型与当前后置处理器的 advisor 匹配: 于是,这个 AsyncService 对象就被 AsyncAnnotationBeanPostProcessor 偷梁换柱,返回了一个代理类: 程序中调用 asyncService.testInsert() 的地方,会被代理类通过 AsyncExecutionInterceptor 进行增强,最终将调用逻辑提交到线程池中异步执行了:

注解失效原理

参考这篇《在同一个类中,一个方法调用另外一个有注解(比如@Async,@Transational)的方法,注解失效的原因和解决方法》 Spring 的代理是通过继承实现的,上图中,被委托类 A 添加了事务注解。Spring 容器实际注入了一个继承于 A 的代理类 proxy$A,它包含一个 A 的对象,对具有增强标记的方法,先执行增强逻辑,再调用 A 的对应方法。其他普通方法,则直接调用 A 的对应方法

A 类的 a() 方法在代理类 proxy$A 中的实现是直接委托调用,所以不具备增强功能,即注解失效。

总结

在请求中包含耗时较长的逻辑、又需要立即返回结果给页面的情况下,可以考虑用在单独的线程中执行目标操作。

而用 Spring 提供的 @Async 注解实现异步很当方便,由 Spring 来管理异步任务的提交比自己创建线程更可靠。

用法虽简单,但是需要注意失效的情况。底层调用链路比较长,知道这个AsyncAnnotationPostProcessor 是核心类就差不多了。

相关推荐
伊布拉西莫10 小时前
Spring 6.x HTTP interface 使用说明
spring·restclient
YDS82912 小时前
苍穹外卖 —— Spring Cache和购物车功能开发
java·spring boot·后端·spring·mybatis
Elieal12 小时前
Spring 框架核心技术全解析
java·spring·sqlserver
组合缺一12 小时前
(对标 Spring)OpenSolon v3.7.0, v3.6.4, v3.5.8, v3.4.8 发布(支持 LTS)
java·后端·spring·web·solon
♡喜欢做梦12 小时前
Spring IOC
java·后端·spring
葡萄城技术团队1 天前
迎接下一代 React 框架:Next.js 16 核心能力解读
javascript·spring·react.js
灰小猿1 天前
Spring前后端分离项目时间格式转换问题全局配置解决
java·前端·后端·spring·spring cloud
知其然亦知其所以然1 天前
这波AI太原生了!SpringAI让PostgreSQL秒变智能数据库!
后端·spring·postgresql
zhaomx19891 天前
Spring 事务管理 Transaction rolled back because it has been marked as rollback-only
数据库·spring
曹朋羽1 天前
Spring EL 表达式
java·spring·el表达式