Spring 配置#EnableAsync启动报错_读spring @Async的源码让我收获了什么?

d462a56018370d45f0f288093d758d57.png

对于从事后端开发的同学来说,为了提升系统性能异步是必须要使用的技术之一。通常我们可以通过:线程、线程池、定时任务 和 回调等方法来实现异步,其中用得最多的可能是线程和线程池。

但创建线程需要实现Runnable接口或继承Thread类,为了避免单继承问题,我们优先使用实现Runnable接口的方式创建线程,在run方法中执行我们自己的业务逻辑。此外,使用线程池,我们也需要一个类去实现Runnable或Callable接口,然后将该类的实例提交到线程池中,如果该类实现的是Runnable接口,则在run方法中执行我们自己的业务逻辑,并且没有返回值,也获取不到异常信息。如果该类实现的是Callable接口,则在call方法中执行我们自己的业务逻辑,并且能获取返回值,也能捕获异常信息。

大家有没有发现有部分代码有冗余?spring的开发者们考虑到异步是一种思想,不应该拘泥于实现Runnable接口或Callable接口,在run方法或call方法中实现业务逻辑,它将线程的创建细节封装起来,只需少许的注解,就可以实现异步的功能,让我们把更多时间花在业务方法上。让我们一起看看spring是怎么做的?

一、spring异步的使用

1.在springboot的启动类上面加上@EnableAsync注解

0bce3a4607dc9b35614b248e9e95d57a.png

2.在需要执行异步调用的业务方法加上@Async注解

6e13b53d6e4aea39170cc52d38cb5144.png

3.在controller方法中调用这个业务方法

d74fa5a6caae8ed6367886ab27e14c50.png

调用category/add接口后打印信息如下:

3f872ae1b369174e0957e5321fc65ca5.png

其中的add在end之后打印,说明确实是异步调用,spring的异步任务使用起来就是这么简单,不用怀疑,只需要在springboot的启动类加上@EnableAsync注解,然后在业务方法上加上@Async注解就可以搞定,so easy。

那么,它的底层是如何实现的呢?

二、源码分析

先从@EnableAsync注解开始,因为它是一切的开始

91f20773c8d67494255abfb3e07e3e77.png

该注解还是非常简单的,关键是ImportAsyncConfigurationSelector类。

知识点:其实EnableXXX开头的注解,在springboot中使用非常多,它更像一个开关,使用该注解就开启了相关功能,说白了,就是通过@Import注解引入相关的功能类。

真正核心的内容在Import的类中。

1221efbd3f4a5da6aa9268074e59d67e.png

AsyncConfigurationSelector类的代码也非常简单,它会根据不同的adviceMode通知模式引入不同的配置类。

知识点:看到这里明白了为什么上一步@EnableAsync注解ImportAsyncConfigurationSelector类,而不是直接引入配置类,因为根据不同的adviceMode通知模式引入不同的配置类,不能单独只引入一个Configuration配置类。selectImports方法是在BeanPostProcessor解析Configuration配置类的时候调用的,import的类有三种:Configuration配置类、实现了ImportSelector接口的类 和 实现了ImportBeanDefinitionRegistrar接口的类。后面两种都可以根据不同的条件返回不同的配置类,有什么区别呢?最大的区别是ImportBeanDefinitionRegistrar接口除了可以获取到注解元数据之后,还可以获取到ImportBeanDefinitionRegistrar类,这个类可以获取到所有注册的BeanDefinition实例。

好了,接下来,我们一起看看ProxyAsyncConfiguration配置类。

ff47a881fc3e8ba7b6a7b5c9c862c6cd.png

该类是一个配置类,里面创建了AsyncAnnotationBeanPostProcessor实例,并将@EnableAsync注解中的属性赋值到该实例对象上。

fc9a3a6510092f4b2e96928b17cf6101.png

AsyncAnnotationBeanPostProcessor实现了BeanPostProcessor和BeanFactoryAware接口,其实BeanPostProcessor是后置处理器,我们知道AOP的入口类就是后置处理器。而实现了BeanFactoryAware接口,就意味着要重写setBeanFacotory方法,该方法是核心代码:

663551a169a4e8ae2a102ad726a4884d.png

知识点:我们在项目中如果想根据bean的名称获取bean实例该怎么办呢?以前我们的做法是new一个ClassPathXmlApplicationContext对象applicationContext,使用这个对象根据bean的名称获取bean实例。现在可以通过定义一个类实现:BeanFactoryAware、ApplicationContextAware 和 ApplicationListener ,从重写的方法入参中可以获取到spring容器对象,用该容器对象就能根据bean的名称获取bean实例。

上面的setBeanFacotory方法创建了一个切面AsyncAnnotationAdvisor,切面有两个要素:通知切入点,我们一起看看它是怎么玩的?

14794961a03f9f136248ee12f443ce7b.png

先将@Async和javax.ejb.Asynchronous类添加到set集合中,然后使用buildAdvice方法创建通知,使用buildPointcut方法创建切入点。

6a0014615feffdeeb52879b2e3265a8f.png

buildAdvice方法里面只创建了一个拦截器AnnotationAsyncExecutionInterceptor实例,spring 异步任务的主要逻辑就在这个拦截器中实现的。

c52562a18a5077277721739a1bb69d9b.png

该方法的逻辑:

1.根据invocation对象先找到targetClass类

2.再根据invocation.getMethod()和targetClass类校验目标方法的访问权限,然后找到真正的目标方法。

3.根据目标方法找到任务执行器

4.创建一个Callable匿名类,在它的call方法中执行目标方法,如果是Future类型则返回数据。

5.将Callable匿名类实例提交到任务,返回这个方法的数据。

ac2f5a5e7ca21ab1ad294db44fe47db7.png

这个方法会先从缓存中根据method查询AsyncTaskExecutor,如果不为空,则直接返回。

0411bd83edcee4c10db99e4102fca897.png

知识点:缓存其实就是一个ConcurrentHashMap对象,这种用法在spring中随处可见,比如bean实例放在singletonObjects对象中,该对象也是一个ConcurrentHashMap,这样做的好处是为了提升性能,不用每次都new AsyncTaskExecutor对象的实例。

如果缓存中没有,则通过getExecutorQualifier方法找出method中定义的任务执行器的名字

39696e4a0c349781e3cff2f2274ad08f.png

该方法先从method上面找@Async注解,如果有则使用方法上定义的执行器名称,如果没有则用该方法所在类上定义的执行器名称,所以要特别注意一下,@Async注解既可以使用在方法上,又可以使用在类上面,如果方法和类上面都定义了,优先使用方法上定义的执行器名称。

再看看获取任务执行器的方法:

1b1bf44334e876269e89637d53373fd8.png

它最终会根据名称和类型从容器中获取相应的bean实例,即AsyncTaskExecutor对象实例。

那么问题来了:什么时候可以获取到AsyncTaskExecutor对象实例?

就是在创建了ThreadPoolTaskExecutor线程池的时候,但是不只这一个线程池,只要实现了AsyncTaskExecutor接口的线程池都可以。

回到上面代码,如果既没有从缓存中获取到syncTaskExecutor对象实例,又没有定义过线程池,则创建一个默认的任务执行器:SimpleAsyncTaskExecutor对象。

最后将创建的任务执行器放入缓存中,然后任务执行器。

有意思的是这段代码:

33a7cccaed2f1dd624ae229edf875d6f.png

使用了双重检查锁

b2b74ed4f6febf816c8da0ab364ec8c9.png

并且defaultExecutor对象被定义成了volatile的,为什么要这样定义?

是为了解决指令重排问题,比如:一个new Object();代码,看起来简单,其底层其实分了三步:分配内存,初始化,将引用指向分配的对象。这三步在synchronized同步代码块中是可能发生指令重排的,如果指令重排了可能会出现先分配内存和将引用指向分配的对象,还没来得及初始化,另外一个线程调用这个对象时就会报错。所以,这里使用volatile,防止指令重排。如果有些朋友想进一步了解volatile原理,可以看看《天天在用volatile,你知道它的底层原理吗?》。

那么,为什么说它有意思?

因为它跟一般的双重检查锁不一样,它使用了targetExecutor局部变量保存defaultExecutor对象的值。

为什么要这样设计?

普通的双重检查锁加volatile关键字,虽说可以解决指令重排问题,但是需要消耗一定的性能,因为volatile的底层是通过内存屏障命令来处理的,内存屏障会增加额外的开销。第一个为空的判断,完全没有必要使用内存屏障,第二个为空的判断才需要,即实例化任务执行器的时候,可以缩小内存屏障使用范围。

最后,看一下invoke方法中的doSubmit方法

d6ed69f047c82df46d6b8240f4d7a256.png

这个方法可以说是spring异步的核心,根据不同的返回值类型,使用不同的AsyncTaskExecutor任务执行器,执行不同的操作:

  1. CompletableFuture类型使用CompletableFuture异步执行,返回数据
  2. ListenableFuture类型使用AsyncListenableTaskExecutor提交任务,返回数据
  3. Future类型使用AsyncTaskExecutor提交任务,返回数据
  4. 其他的使用AsyncTaskExecutor提交任务,返回空

知识点:

AsyncListenableTaskExecutor和AsyncTaskExecutor有什么区别?

区别是AsyncListenableTaskExecutor返回ListenableFuture类型的数据,而AsyncTaskExecutor返回Future类型的数据,而ListenableFuture是对Future的增强,我们知道Future表示一个异步计算任务,当任务完成时可以得到计算结果。如果我们希望一旦计算完成就拿到结果展示给用户或者做另外的计算,就必须使用另一个线程不断的查询计算状态。这样做,代码复杂,而且效率低下。使用ListenableFuture Guava帮我们检测Future是否完成了,如果完成就自动调用回调函数,这样可以减少并发程序的复杂度。

苏三说技术 发起了一个读者讨论通过CompletableFuture获取返回数据,跟通过Callable的Future获取返回数据 有什么区别?

三、收获

  1. 使用EnableXXX开头的注解,配合@import注解一起提供一个开关的功能。
  2. @import注解中的类包含:Configuration配置类、实现了ImportSelector接口的类 和 实现了ImportBeanDefinitionRegistrar接口的类 三种。
  3. AOP的入口是BeanPostProcessor接口的实现类,我们可以在该类中定义切面来实现异步的功能,切面的两个要素:切入点 和 通知。
  4. 实现了BeanFactoryAware、ApplicationContextAware 和 ApplicationListener接口的类,可以获取到spring容器。
  5. 可以使用ConcurrentHashMap做缓存,bean实例放的singletonObjects对象就是ConcurrentHashMap类型
  6. 使用synchronized和volatile实现双重检查锁时,使用局部变量性能更好
  7. 使用ListenableFuture可以拿到计算完成的结果,而Future只能拿到整个任务完成的结果。

通过阅读spring异步的源码,收获还是蛮多的,关键是要多思考。

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,或者点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多大厂的前辈交流和学习。坚持原创不易,你的支持是我坚持的最大动力,谢谢啦。

http://weixin.qq.com/r/YyptdWTE8DmPrSAW939x (二维码自动识别)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值