一文彻底讲透@Async注解的原理和使用方法
一.背景:spring提供了@Async异步注解,使得方法的调用可以异步的进行,下面代码提供简单的演示:
@Configuration @EnableAsync @ComponentScan("com.yang.xiao.hui.aop") public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(App.class); MyAsync service = ctx.getBean(MyAsync.class); System.out.println(service.getClass()); service.async1(); System.out.println("目标方法执行完没........."); } } @Component public class MyAsync { @Async public void async1() { try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("hello1:"+this.getClass()); } public void async2(){ System.out.println("hello2:"+this.getClass()); this.async1(); } }
上述代码提供了最简单的异步使用方式,如果是同步执行,那么控制台打印的顺序应该是:
System.out.println("hello1:"+this.getClass())-------》 System.out.println("目标方法执行完没.........");
然而控制台的打印刚好相反,证明异步的注解生效了:
二.原理分析
1.猜想:aync1()方法标注了@Async注解,该方法就异步执行了,那么该方法肯定是被拦截了,方法拦截肯定存在一个方法拦截器MethodInterceptor
方法拦截器是一个接口,对异步方法的拦截,肯定是该接口的一个实现类,如何找到它:
2.线索分析:我们的唯一条件是主启动类上贴了一个@EnableAsync注解
点击进去分析:
根据追踪,我们发现,最终会往容器中注入 AsyncAnnotationBeanPostProcessor
分析其继承体体系,发现其实现了BeanFactoryAware接口,实现该接口的类,spring容器在创建该bean时,会回调:void setBeanFactory(BeanFactory beanFactory) throws BeansException;
接着我们看看:
我们之后看看AsyncAnnotationAdvisor的创建过程:
接着看this.advice = buildAdvice(executor, exceptionHandler);
至此,我们终于找到方法拦截器了,为何是它,看看它的继承体系:
3.验证:既然找到了方法拦截器,那么我们就打断点在拦截方法里,执行之前的测试代码:拦截方法在它的父类中:AsyncExecutionInterceptor
原理非常简单:1.获取线程池 2.创建callable 3.执行线程
重点是线程池的获取逻辑:
由上所得:线程池会首先通过用户自己配置的为准:
所以,我们可以自定义线程池,然后注入spring中,将bean的名字放到@Async注解的value值即可
最后我们看看默认的线程池是怎么获取的:
targetExecutor = this.defaultExecutor.get();
可以知道,他是通过调用getDefaultExecutor(this.beanFactory)
由此可见,会先获取容器中TaskExecutor的线程池,获取不到,就获取指定beanName的线程池DEFAULT_TASK_EXECUTOR_BEAN_NAME = "taskExecutor";
我们看看默认的线程池
总结:@Async的异步线程池获取顺序:
三:学以致用
直接创建2个异步方法:
四:思考:如果一个类中,非异步方法调用了异步的方法,异步方法还会生效么:
@Component public class MyAsync { @Async public void async1() { try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("hello1:"+this.getClass()); } public void async2(){ System.out.println("hello2:"+this.getClass()); this.async1(); } }
@Configuration @EnableAsync @ComponentScan("com.yang.xiao.hui.aop") public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(App.class); MyAsync service = ctx.getBean(MyAsync.class); System.out.println(service.getClass()); service.async2(); System.out.println("目标方法执行完没........."); } }
如果异步生效,那么System.out.println("目标方法执行完没.........");会先于System.out.println("hello1:"+this.getClass());执行
然而结果:
由此看到并不是同步执行,原因如下:
service.async2(); 方法调用:service是代理类,上图打印的第一行可以知道,它是cglib代理,因此该方法本质调用的是代理类的aync2(),而该方法并没有@aync注解也就没有方法拦截器,因此
代理类执行async2()方法,本质最终是直接调用了被代理类的方法,所以上图打印出的第二行可以知道:
this.getClass()==com.yang.xiao.hui.aop.MyAsync
由此可以知道,只要是代理类调用async1()方法,就可以让异步调用生效了,如何获取被代理类,既然asyn2()是代理类调用的,肯定会被拦截,我们debug进入
当this.advised.exposeProxy=true时,代理类设置到AopContext中
原来是设置到ThreadLocal中,那成立的条件又是啥
该值其实就是@EnableAspectJAutoProxy(exposeProxy = true)的值
默认情况是不会将代理类放到ThreadLocal中的,我们要自己开启:
由此,改进我们的调用方法,通过ThreadLocal获取代理类,然后调用目标方法
启动测试:
居然报错了。。。。。,这么说,@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)并没有生效,那么我们看看该注解的配置原理是啥?
由此发现,它指对自动创建的的aop创建器生效,到底有哪些呢?
事务注解相关的就可以,但我们的是异步注解,根据之前源码分析知道,异步是通过AsyncAnnotationBeanPostProcessor来实现的;
由此,我们是否可以通过获取AsyncAnnotationBeanPostProcessor的beanDefiniton然后给它新增属性,类型下面的实现:
具体改造如下:
@Component public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { BeanDefinition beanDefinition = registry.getBeanDefinition(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME); if(null!=beanDefinition){ beanDefinition.getPropertyValues().add("exposeProxy", Boolean.TRUE); } } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } }
此时启动类的@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)可以不用了,再次测试:
上述就是同个类中调用异步方法不生效的解决方案