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 是核心类就差不多了。

相关推荐
成富43 分钟前
文本转SQL(Text-to-SQL),场景介绍与 Spring AI 实现
数据库·人工智能·sql·spring·oracle
鹿屿二向箔2 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架的汽车租赁共享平台系统
spring·mvc·mybatis
豪宇刘2 小时前
SpringBoot+Shiro权限管理
java·spring boot·spring
一只爱打拳的程序猿3 小时前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
ajsbxi6 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
鹿屿二向箔7 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架的咖啡馆管理系统
spring·mvc·mybatis
NoneCoder7 小时前
Java企业级开发系列(1)
java·开发语言·spring·团队开发·开发
paopaokaka_luck14 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
Yaml416 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
aloha_78916 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot