目录
前言
本篇会涉及到@Async的使用场景,工作中遇到的坑。以及底层源码的详细分析。
定义
@Async注解是spring的一个用于调用方法时异步的注解。也就是方法的异步调用,当一个业务方法被调用时,主线程不需要它马上返回结果,可以丢到一边让他自己执行的时候,@Async注解便派上了用场。
使用方式
相信很多小伙伴工作中都使用过@Async这个注解,或许没有使用过也见到过。这是一个非常常见的注解。它的使用方式也是特别简单,只需要在需要异步执行的方法上加上这个注解就可以了。
需要使用它,首先你需要在springboot启动类上加上@EnableAsync,这非常重要,因为你不加它不会生效(因为这个注解的作用是初始化Aysnc的一些配置,这个后面源码里讲)。
然后它大概就是这种姿势
@Async
public void simpleAsync(){
log.info("{}-线程执行", Thread.currentThread().getName());
}
调用这个方法
@Test
public void simpleAsyncTest(){
log.info("{}-线程执行", Thread.currentThread().getName()); asyncDemo.simpleAsync();
}
查看执行结果,果然是一个线程单独在跑,是异步的没有问题。
注意到这里异步方法执行的时候有一个SimpleAsyncTaskExecutor,一看这个名字就像是线程池一样的东西。其实这个东西不算是一个线程池,使用这个Executor只是会新创建一条新的线程来执行你的方法,有兴趣的可以点进去看看它的源码。
如果你不指定线程池,那么它就会默认使用这个伪线程池。但是在工作中肯定是使用自己创建的线程池呀,这样也可控一点。当然有默认就会给你使用自己线程池的方法。
使用自己定义的线程池也比较简单,只需要在@Async注解里面传入自己定义的线程池的beanName就可以了,就下面这样:
先定义一个线程池:
@Bean("specificThreadPool")
public ThreadPoolTaskExecutor createSpecificThreadPool() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(6);
threadPoolTaskExecutor.setMaxPoolSize(20);
threadPoolTaskExecutor.setKeepAliveSeconds(60);
threadPoolTaskExecutor.setQueueCapacity(Integer.MAX_VALUE);
threadPoolTaskExecutor.setThreadNamePrefix("specificThreadPool-");
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
然后再将这个线程池名称放在注解里面,这样这个异步方法使用线程池就是我们自己定义的线程池啦。
@Async("commonThreadPool")
public CompletableFuture<String> testAsync01(String str) {
log.info("{}-线程执行", Thread.currentThread().getName());
return CompletableFuture.completedFuture(str);
}
值得注意的事@Async注解不仅可以定义在方法上,还可以定义在类上,没错,在类上使用该注解,整个类里面的方法都会被代理,变成异步的方法。
以下是使用这个注解需要注意的坑点,在同一类里面,同步方法调用异步方法,那么异步方法是不会生效的。
为什么?这个放到源码解析的结尾说吧,这里先卖个关子(主要了解了源码后才会对各种情况了若指掌)。
调用方式 | 同一类 | 结论 |
同步->异步 | 是 | 不生效 |
异步->异步 | 是 | 生效,使用调用方法的线程池 |
同步->异步 | 否 | 生效 |
异步->异步 | 否 | 生效,使用方法各自的线程池 |
源码解析
好了在了解了@Async注解的基本使用方法后,终于到激动人心的源码部分了。
1.1 Async注解
那就先从这个注解本身入手去看一下。注解上面有一大段注释,我们挨个大概看一下。
上面第一段大概意思就是,这个注解可以使用在方法上,也可以使用在类上,它这里的type level应该就是类级别的意思,然后,它不能和@Configuration注解一起使用。
第二段的注解比较有意思,它这里说使用@Async注解的方法,只允许返回void或者Future类,然后这个Future类可以是ListenableFuture也可以是CompletableFuture,那么是不是这样呢?各位可以试一下,笔者反正试过,如果是直接返回一个String或者是Integer那么返回值会是空。这里先记下来,后面咱们深入源码的时候再找为什么。
然后再简单看一下注解里面value值上面的注解,他这里大概意思就是说,你可以指定线程池的名称,然后@Async注解会使用这些注解,如果你将注解定义到类上,那么这个类里面的所有方法都会使用这个线程池。
其实,这些注释就已经非常完整的告诉了我们,@Async注解到底应该怎样使用了。
还记得使用@Async注解需要先引入什么注解吗?对,就是@EnableAsync。一般加在启动类加载的注解,都会初始化一些东西,所以我们从这个注解开始入手。
这个注解引入了一个AsyncConfigurationSelector,点进去,这个类继承了AdviceModeImportSelector类,而类又实现了ImportSelector这个就是和@Import搭配使用的,不了解伙伴可以自行百度一下,这里做了一下判断,返回了一个ProxyAsyncConfiguration类。我们点进这个类去看一下。
发现这是一个配置类,这个配置类初始化了一个AsyncAnnotationBeanPostProcessor对象。继续进去研究一下这个对象。
1.2 抽象Bean处理器
我们先看一下这个AsyncAnnotationBeanPostProcessor类的继承和实现关系图如下所示,从这张图我们可以看到,它是继承了两个抽象类,并且,它的父亲实现了BeanFactoryAware,而他的爷爷实现了BeanPostProcessor。看到这两个接口我们可以大胆的猜想一下,BeanFactoryAware实现是可以拿到beanFactory然后做一些事情,而BeanPostProcessor更是被很多源码广泛应用,通过实现它可以在Bean初始化的前后对bean做处理,都知道@Async应该是基于Aop做的,而这里会不会是在Bean初始化的时候去做的增强呢?带着疑问,继续往下深究。
进入AsyncAnnotationBeanPostProcessor类,既然是实现了BeanFactoryAware那么肯定是会实现setBeanFactroy方法啦,首先找到这个方法看一下。
发现这里是去new了一个AsyncAnnotationAdvisor对象,进入这个类里瞧一眼先。
1.3 切面,构建advisor
老规矩先把类的关系理顺才有利于整个功能的剖析,发现这个类继承了一个抽象类AbstractPointcutAdvisor看这个类名称又是pointcut又是advisor的,肯定是aop相关的呀。而且这个抽象类是实现了advisor,这是一个容器,将pointcut与通知结合形成切面的容器。
理清楚关系之后,我们再进入AsyncAnnotationAdvisor类看一下,发现这个类有两个属性一个是advice一个是pointcut,这都是老朋友了,一个是通知一个则是切入点,然后就可以看构造方法了,这里我们可以看到,advice由buildAdvice创建,pointcut由buildPointcut创建。
我们就先从这个通知的创建入手,点击进入buildAdvice方法,这个方法是去创建了一个AnnotationAsyncExecutionInterceptor对象。
1.4 advice方法拦截器
这个对象的继承关系图如下所示,我们可以看到这个类的父亲是实现了一个方法的拦截器接口,MethodInterceptor。那好,就进入他的父类看看。
父类重写的invoke方法就是@Async实现最核心的方法了!经过千辛万苦一步一个脚印,终于是来到了这一步。
那么肯定需要仔细的阅读一下这部分代码了,首先它这里是先去拿了异步调用所需要的线程池通过determineAsyncExecutor方法
而这个方法里面是先去拿一个qualifier,这个方法其实就是去拿你写在@Async注解里面的value的值,可以点进去跟一下这个方法,发现果然是这样的。
好,回到determineAsyncExecutor方法,这里代码就比较简单易懂了,大概就是如果你指定了线程池的名称,他就通过beanFactory去拿对应名称的线程池的bean,如果你没有制定,他就去拿默认的线程池。
通过determineAsyncExecutor拿到线程池后,我们回到invoke方法,继续阅读他的方法,它这里是new了一个Callable,也就是多线程的常规操作,然后里面做了一个判断,如果返回值是Futrue类型,则将方法执行的结果返回,否则没有做任何处理,只是去执行方法。还记得我们之前阅读@Async注解上面的注释说的吗?他说他只支持Future类型以及void,这里就真的是说到做到了,哈哈。
创建了Callable后就调用doSubmit方法,这个方法里面就是多个if判断是不是future类,如果不是的话,就直接由线程池提交任务了,然后这里是直接返回一个null。这里也印证了,被@Async注解修饰的方法,只支持Future和void类型,如果强行使用其他类型的返回值,那就会返回null。
以上就是buildAdvice的创建过程啦,其实也就是创建了个方法拦截器,对方法进行增强,也就是通过线程池拿一条线程过来,然后执行@Async修饰的方法。
1.5 创建切入点
继续回到构造方法,我们现在已经知道了通知是怎么创建的了,那么现在来看一下buildPointcut,看看他是如何创建切入点的。这里是丢进去了一个Set集合,这个集合放入的是Async注解。
然后这个方法就是创建切入点的实现。
创建完成后,整个advisor就构建完成了,我们先得回到最初我们所说的AsyncAnnotationBeanPostProcessor这个bean处理器了。
1.6 回到抽象Bean处理器
深入看了一圈之后,有的小伙伴我相信已经彻底蒙圈,醒醒,该回来了!如果忘了也别急回头看下1.2的类构建关系,我们现在需要回到AsyncAnnotationBeanPostProcessor,这里将上面所说到的advisor给创建好之后就赋值给了当前类的advisor属性。
还记得我们之前做过猜想吗?1.2的时候,猜想是通过aop对bean处理进行增强,我们的猜想正在一步步印证。观察1.2章给出的类关系图,发现是AbstractAdvisingBeanPostProcessor类实现了BeanPostProcessor,到这里,算是彻底破案了。和我们的想法一模一样,这里postProcessAfterInitialization方法,在bean实例化之后,通过ProxyFactory创建出一个代理对象返回。而这个代理对象的Advisor也正是当前这个类的Advisor,也就是之前我们分析的通过AsyncAnnotationAdvisor类。
自此,整个代理过程全部结束,整个@Async的源码分析也到此结束啦,完结,撒花!
总结
其实整个@Async注解的源码不算特别难,是各种抽象类继承接口实现,让我们阅读起来比较麻烦,但是多看几遍,应该都是没有问题的。整个核心代码说实话也不难,要咱们写也能写出来,哈哈!不过整个方法实现的过程曲曲折折。不过我相信多看源码,多理解大佬们的思想,有朝一日,自己也能变得很强,加油,打工人!