Spring学习笔记:@Async Spring异步任务的深入学习与使用
Spring的异步任务机制非常的有用,特别是记录是那些记录日志,发送短信,发送邮件等等非核心任务业务上面,系统的内部任务可以优化结构,加快响应速度,提升用户体验。
1 异步任务的概念
Spring的异步任务机制,可以让调用者无需等待任务(完成),可以避免方法阻塞,提升响应效率。通常用于日志记录,发送邮件,短信等非核心业务中。只需简单的配置即可使用Spring异步任务机制。
1.1 开启异步任务支持
跟IoC和AOP一样,Spring 的异步任务机制也可以用XML和注解来开启使用。
(1)对于注解的方式,一般将@EnableAsync注解添加到@Congifuration配置类上面,表示开启异步任务支持。
(2)对于XML的方式,< task:annotation-driven/>标签来开启异步任务支持。
1.2 任务执行器
Spring提供了TaskExcutor任务执行器抽象接口,等同于JDK5.0中的java.util.concurrent.Executor执行器,简单的任务执行器用于执行各种任务。
不同的TaskExcutor有不同的执行策略,最常见的是线程执行器,有其他类型的执行器,比如单线程执行器,同步执行器等,因此不能把任务执行器和线程池划等号(Excutor线程池)。
Spring提供各种版本的TaskExecutor实现,很多都可以配置,通常无需自定义的TaskExecutor实现,要用的时候,只需要这些bean定义注册到容器中即可。
Spring 异步任务最关键多线程的使用,和上面的任务执行器(TaskExecutor)关联起来,通过配置@Bean注解配置执行器或者基于XML自定义执行器Bean。通常自定义的执行器都是采用ThreadPoolTaskExecutor类型,这里的执行器基于Executor接口执行异步任务,也是在JDK得线程池。
基于XML的配置还可以使用,< task:executor/>标签来快速定义一个ThreadPoolTaskExecutor类型的执行器,对应的线程前缀就是“执行器id-”。
1.3 Async异步任务
你开启异步任务的方式无论是注解和XML的形式,但最后都是@Async注解来描述来完成,这个是Spring 3.0添加的注解。
1 @Async是通过代理来实现的,可以是JDK或者cglib来完成,通过@EnableAsync注解proxyTargetClass属性或者< task:annotation-driven/>标签的proxy-target-class属性来控制,默认为false,表示优先使用JDK代理,否则再尝试CGLIB代理,如果改为true,表示强制CGLIB代理。
2 被@Async注解标注的方法,称为异步方法,被@Async标注的类的方法也都是异步方法。方法的注解的优先级则直接使用该注解,没有的话就在对应的类上找@Async注解。
采用JDK代理,如果是JDK代理是只能代理接口的方法,如果采用CGLIB代理则不能代理final,static,private方法,当然这个类不能是final。
由于代理的限制,如果同一个类的方法互相调用,如果@Async方法不是在调用链首位,那么被调用的@Async方法不会被调用。
实际上,基于AOP的其他配置都不会生效,比如事物,因为里面的方法实际上是通过目标对象本身调用的,并且如果配置了@Async方法,那么这些种情况不能通过普遍的方式解决,比较有效的解决办法是可以在注入的属性加上@Lazy注解,或者将方法写到不同的类里面。如果@Async方法不能满足上面的要求,则还是通过调用线程去执行该方法,可能不会抛出异常,因此难以察觉;
3 @Async注解标注的异步方法通常没有返回值,但是可以有返回值,需要使用一个Future类型的对象来接受。这这个返回的Future类型的对象,返回的泛型类型可以是实际返回值类型,具体怎么获得,是可以Future.get()来获取真实返回值,当然也可以返回异常。实际上还有使用ListenableFuture、CompletableFuture来接收,这两个类作为异步获取结果类,更加适合与异步方法交互。
(CompleteableFuture和ListenableFuture来接收返回的Future任务,CompleteableFuture 是原生支持,ListenableFuture是Guava提供的,
CompleteableFuture适用场景:
复杂异步流水线,多阶段任务串联(A-》B-〉-》C或并行并行结果)
CompletableFuture.supplyAsync(() -> fetchData()).thenApply(data -> transform(data)).thenAccept(result -> save(result));
异常恢复,链式处理失败逻辑(如降级返回值)
CompletableFuture.supplyAsync(() -> riskyOp()).exceptionally(ex -> fallbackValue);
JDK8以上都可以支持使用。
ListenableFuture适用场景
简单回调任务,任务完成触发日志,通知等
ListenableFuture<String> future = executor.submit(task);
Futures.addCallback(future, new FutureCallback<>() {public void onSuccess(String result) { log(result); }public void onFailure(Throwable t) { logError(t); }
});
JDK7以及JDK7以下的旧项目不支持升级JDK版本
)
4 @Async如果与生命周期回调方法结合使用,比如@PostConstruct方法,在生命执行周期回调,不会异步执行,它通过目标对象执行。
5 @Async的value可以指定一个我们自定义的执行器的名字,导致对于该方法和该类的@Async方法
(1)选择通过Java配置AsyncConfigurer的getAsyncExcutor方法返回执行器(这些执行器不受Spring管理,默认返回null)或者是通过< task:annotation-driven/>标签的executor属性指向的执行器(默认没有设置)。
(2)上面的方法没有获取到执行器,继续判断在容器中查找如果有一个TaskExecutor类型的执行器,那么该执行器作为默认执行器;如果有多个或者没有任何一个,那么将查找beanName或者别名为“taskExecutor”类型为Executor的执行器作为默认执行器,如果还是找不到,那么将创建一个SimpleAsyncTaskExecutor类型的执行器作为默认执行器(该执行器不受到Spring管理)。
6 对于具有Future返回值异步方法,方便管理执行时的异常,产生的异常会封装到Future,通过get方法抛出。
7 Spring不能为@Async注解标注的类解决setter方法和反射字段注解字段的循环依赖注入。
主要的问题就是A-》B-〉A ,这种循环依赖注入的问题。
循环依赖会抛出:“……This means that said other beans do not use the final version of the bean……”异常,根本原因这个AOP代理对象不是使用通用的AbstractAutoProxyCreator的方法创建的,而是使用AsyncAnnotationBeanPostProcessor后处理器来创建的,Spring目前没有解决这个问题。
解决办法是在引入的依赖项上加一个@Lazy注解,原理就是再给它加一层AOP代理……。而其他的,Spring可以解决比如由于事物或者通知方法创建的AOP代理的循环依赖。
2 异步任务案例
2.1基于XML配置
maven依赖
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version> 5.2.8.RELEASE</version>
</dependency>
2.2 基于注解配置
异步@Async注释的方法
@Component
public class AsyncMethod {@Asyncpublic void log() {System.out.println("-----log:"+Thread.currentThread().getName());}@Asyncpublic void log2() {System.out.println("-----log2:"+Thread.currentThread().getName());log3();}@Asyncpublic void log3() {System.out.println("-----log3:"+Thread.currentThread().getName());}
}
添加Spring启动配置类
@Configuration
@EnableAsync
@ComponentScan
public class ConfigurationStart {private final LongAdder longAdder = new LongAdder();/*** 这里仅仅是配置了一个JDK的ThreadPoolExecutor*/@Beanpublic Executor taskExecutor() {return new ThreadPoolExecutor(3, 5, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), r -> {longAdder.increment();//线程命名return new Thread(r, "JDK线程-" + longAdder.longValue());});}/*** 这里仅仅是配置了一个Spring的ThreadPoolTaskExecutor*/@Beanpublic ThreadPoolTaskExecutor threadPoolTaskExecutor1() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();//配置核心线程数executor.setCorePoolSize(5);//配置最大线程数executor.setMaxPoolSize(10);//配置队列大小executor.setQueueCapacity(800);//配置线程池中的线程的名称前缀executor.setThreadNamePrefix("threadPoolTaskExecutor1-");// rejection-policy:拒绝策略,由调用者所在的线程来执行executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());return executor;}/*** 这里仅仅是配置了一个Spring的ThreadPoolTaskExecutor*/@Beanpublic ThreadPoolTaskExecutor threadPoolTaskExecutor2() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();//配置核心线程数executor.setCorePoolSize(5);//配置最大线程数executor.setMaxPoolSize(10);//配置队列大小executor.setQueueCapacity(800);//配置线程池中的线程的名称前缀executor.setThreadNamePrefix("threadPoolTaskExecutor2-");// rejection-policy:拒绝策略,由调用者所在的线程来执行executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());return executor;}
}
使用的方法
public class AnnTaskExecutorTest {public static void main(String[] args) {AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ConfigurationStart.class);AsyncMethod asyncMethod = ac.getBean(AsyncMethod.class);System.out.println(asyncMethod.getClass());System.out.println("--------" + Thread.currentThread().getName() + "--------");asyncMethod.log();asyncMethod.log2();}}
2.3 返回值和异常返回
异步方法可以有返回值,需要将异步方法的返回值封装到Future接口中。Future是多线程应用程序的一种常见的设计模式。
Future是一种多线程设计模式:
通过契约机制和分离任务来完成,避免主线程被阻塞,提高并发能力。
(1)异步调用:
调用耗时任务,不阻塞主线程,立即返回一个结果Future,代表未来可获取正确的真实数据。
做一个类比,就是你下单给你一个订单号,但是具体的订单对应的商家的备货出库,以及后面的物流都是对应的后续东西,你根据这个订单号来获取。这个订单号就是一个Future。
(2)等待时间复用
主任务在等待返回结果的时候可以处理别的任务,充分利用时间。
主线程提交任务获取Future → 异步线程执行任务并设置结果到Future → 主线程调用future.get()阻塞等待结。
如果任务未完成,直接调用get方法,会导致这个线程会被阻塞,知道异步任务完成返回结果。
Future核心思想:提交任务立即返回一个Future类型的结果,提交的任务会被一个新的线程异步执行任务。这样就不会阻塞主任务,调用的线程执行后续的逻辑,而后续我们也可以通过一系列方法获取Future中任务执行完毕之后真实的返回值,一些Future实现还能添加回调方法。
Future实现的很多
异步方法支持三种返回类型(排除void),这个源码在AsyncExecutionAspectSupport的doSubmit方法中:
CompleteableFuture:首先判断CompleteableFuture以及其子类,最终会返回S pring返回一个CompleteableFuture对象。
ListenableFuture:次判断如果返回值类型是ListenableFuture及其子类,那么最终会默认返回一个Spring为我们创建的ListenableFutureTask对象;
Future:随后判断如果异步方法返回值类型是Future及其子类,那么最终会默认返回一个Spring为我们创建的FutureTask对象;
最后,如果以上判断都不满足,即如果异步方法指定了返回其它类型,那么最终将返回一个null。正常返回时,返回的结果对象和我们在方法中返回的对象也不是同一个。
具有返回值的异步方法执行过程中产生的异常会被封装到Future中,因此很方法方便处理,它的get()方法就能抛出在执行过程中捕获的异常,而对于高级的Future,比如CompletableFuture和ListenableFuture则可以注册异常处理函数。
3 异步任务总结
对应Spring Boot应用,容器不存在任何定义Executor执行器,那么将在TaskExecutionAutoConfiguration这个自动配置类中默认创建一个ThreadPoolTaskExecutor类型的执行器,名为applicationTaskExecutor ,别名为taskExecutor,线程名为“task-xx”,它将作为默认的执行器,如果有至少一个执行器,那么将不会创建。
(这个在项目中可以看到日志)
Spring异步任务机制非常的有用,特别是在那些记录日志、发端短信、发送邮件等等非核心的业务上面,可以提升响应速度,提升用户体验。另外异步任务还常常与其他功能和并使用,比如异步定时任务等!