发布网友 发布时间:2024-09-26 15:28
共1个回答
热心网友 时间:2024-12-02 15:14
SpringBoot之@Async异步调用利用SpringInitializer创建一个gradle项目spring-boot-async-task,创建时添加相关依赖。
在SpringBoot入口类上配置@EnableAsync注解开启异步处理。
创建任务抽象类AbstractTask,并分别配置三个任务方法doTaskOne(),doTaskTwo(),doTaskThree()。
下面通过一个简单示例来直观的理解什么是同步调用:
定义Task类,继承AbstractTask,三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10秒内)。
在单元测试用例中,注入Task对象,并在测试用例中执行doTaskOne(),doTaskTwo(),doTaskThree()三个方法。
执行单元测试,可以看到类似如下输出:
任务一、任务二、任务三顺序的执行完了,换言之doTaskOne(),doTaskTwo(),doTaskThree()三个方法顺序的执行完成。
上述的可以看到执行时间比较长,若这三个任务本身之间不存在依赖关系,可以并发执行的话,同步调用在执行效率方面就比较差,可以考虑通过异步调用的方式来并发执行。
创建AsyncTask类,分别在方法上配置@Async注解,将原来的同步方法变为异步方法。
在单元测试用例中,注入AsyncTask对象,并在测试用例中执行doTaskOne(),doTaskTwo(),doTaskThree()三个方法。
执行单元测试,可以看到类似如下输出:
如果反复执行单元测试,可能会遇到各种不同的结果,比如:
原因是目前doTaskOne(),doTaskTwo(),doTaskThree()这三个方法已经异步执行了。主程序在异步调用之后,主程序并不会理会这三个函数是否执行完成了,由于没有其他需要执行的内容,所以程序就自动结束了,导致了不完整或是没有输出任务相关内容的情况。
根据业务需求,可以将暂时不需要立即获得处理的方法设置为@Async.
比如用户在前端点击完成了登录操作,这时候根据业务要求需要在登录成功之后进行埋点的处理.
其实埋点成功与否都不影响用户操作,这时候就可以将埋点方法设置为@Async.
个人认为此类任务通常有三个特征:
为了让doTaskOne(),doTaskTwo(),doTaskThree()能正常结束,假设我们需要统计一下三个任务并发执行共耗时多少,这就需要等到上述三个函数都完成动用之后记录时间,并计算结果。
那么我们如何判断上述三个异步调用是否已经执行完成呢?我们需要使用FutureT来返回异步调用的结果。
在单元测试用例中,注入AsyncCallBackTask对象,并在测试用例中执行doTaskOneCallback(),doTaskTwoCallback(),doTaskThreeCallback()三个方法。循环调用Future的isDone()方法等待三个并发任务执行完成,记录最终执行时间。
在测试用例一开始记录开始时间;在调用三个异步函数的时候,返回Future类型的结果对象;在调用完三个异步函数之后,开启一个循环,根据返回的Future对象来判断三个异步函数是否都结束了。若都结束,就结束循环;若没有都结束,就等1秒后再判断。跳出循环之后,根据结束时间-开始时间,计算出三个任务并发执行的总耗时。
执行一下上述的单元测试,可以看到如下结果:
可以看到,通过异步调用,让任务一、任务二、任务三并发执行,有效的减少了程序的运行总时间。
在上述操作中,创建一个线程池配置类TaskConfiguration,并配置一个任务线程池对象taskExecutor。
上面我们通过使用ThreadPoolTaskExecutor创建了一个线程池,同时设置了以下这些参数:
创建AsyncExecutorTask类,三个任务的配置和AsyncTask一样,不同的是@Async注解需要指定前面配置的线程池的名称taskExecutor。
在单元测试用例中,注入AsyncExecutorTask对象,并在测试用例中执行doTaskOne(),doTaskTwo(),doTaskThree()三个方法。
执行一下上述的单元测试,可以看到如下结果:
执行上面的单元测试,观察到任务线程池的线程池名的前缀被打印,说明线程池成功执行异步任务!
解决方案如下,重新设置线程池配置对象,新增线程池setWaitForTasksToCompleteOnShutdown()和setAwaitTerminationSeconds()配置:
SpringBoot-异步任务有时候,前端可能提交了一个耗时任务,如果后端接收到请求后,直接执行该耗时任务,那么前端需要等待很久一段时间才能接受到响应。如果该耗时任务是通过浏览器直接进行请求,那么浏览器页面会一直处于转圈等待状态。一个简单的例子如下所示:
当我们在浏览器请求localhost:8080/async/页面时,可以看到浏览器一直处于转圈等待状态,这样体验十分不友好。
事实上,当后端要处理一个耗时任务时,通常都会将耗时任务提交到一个异步任务中进行执行,此时前端提交耗时任务后,就可直接返回,进行其他操作。
在Java中,开启异步任务最常用的方式就是开辟线程执行异步任务,如下所示:
这时浏览器请求localhost:8080/async/,就可以很快得到响应,并且耗时任务会在后台得到执行。
一般来说,前端不会关注耗时任务结果,因此前端只需负责提交该任务给到后端即可。但是如果前端需要获取耗时任务结果,则可通过Future等方式将结果返回,详细内容请参考后文。
事实上,在SpringBoot中,我们不需要手动创建线程异步执行耗时任务,因为Spring框架已提供了相关异步任务执行解决方案,本文主要介绍下在SpringBoot中执行异步任务的相关内容。
Spring3.0时提供了一个@Async注解,该注解用于标记要进行异步执行的方法,当在其他线程调用被@Async注解的方法时,就会开启一个线程执行该方法。
注:@Async注解通常用在方法上,但是也可以用作类型上,当类被@Async注解时,表示该类中所有的方法都是异步执行的。
在SpringBoot中,如果要执行一个异步任务,只需进行如下两步操作:
被@Async注解的异步任务方法存在相关*:
默认情况下,Spring会自动搜索相关线程池定义:要么是一个唯一TaskExecutorBean实例,要么是一个名称为taskExecutor的ExecutorBean实例。如果这两个Bean实例都不存在,就会使用SimpleAsyncTaskExecutor来异步执行被@Async注解的方法。
综上,可以知道,默认情况下,Spring使用的Executor是SimpleAsyncTaskExecutor,SimpleAsyncTaskExecutor每次调用都会创建一个新的线程,不会重用之前的线程。很多时候,这种实现方式不符合我们的业务场景,因此通常我们都会自定义一个Executor来替换SimpleAsyncTaskExecutor。
对于自定义Executor(自定义线程池),可以分为如下两个层级:
前文介绍过,对于被@Async注解的异步方法,只能返回void或者Future类型。对于返回Future类型数据,如果异步任务方法抛出异常,则很容易进行处理,因为Future.get()会重新抛出该异常,我们只需对其进行捕获即可。但是对于返回void的异步任务方法,异常不会传播到被调用者线程,因此我们需要自定义一个额外的异步任务异常处理器,捕获异步任务方法抛出的异常。
自定义异步任务异常处理器的步骤如下所示:
SpringBoot异步任务--@EnableAsync详解@EnableAsync注解启用了Spring异步方法执行功能,在SpringFrameworkAPI中有详细介绍。
@EnableAsync默认启动流程:
1搜索关联的线程池定义:上下文中唯一的TaskExecutor实例,或一个名为taskExecutor的java.util.concurrent.Executor实例;
2如果以上都没找到,则会使用SimpleAsyncTaskExecutor处理异步方法调用。
注意:具有void返回类型的带注释方法不能将任何异常发送回调用者,默认情况下此类未捕获异常只会被记录日志。
定制@EnableAsync启动行为:
1实现AsyncConfigurer接口
2实现getAsyncExecutor()方法自定义java.util.concurrent.Executor
3实现getAsyncUncaughtExceptionHandler()方法自定义AsyncUncaughtExceptionHandler
示例:修改AsyncConfig配置类实现
SpringBoot微服务异步调用@EnableAsync@Async第一步:在Application启动类上面加上@EnableAsync注解
第二步:定义[线程池]
第三步:在异步方法上添加@Async
第四步:测试
输出结果:
时间testA:2
开始testB
开始testA
完成testA
完成testB
任务testA,当前线程:async-thread-pool-1
时间testB:3002
异步方法@Async注解失效情况:
(1)在@SpringBootApplication启动类没有添加注解@EnableAsync
(2)调用方法和异步方法写在同一个类,需要在不同的类才能有效。
(2)调用的是静态(static)方法
(3)调用(private)私有化方法
个别失效报错情况:
报错一:提示需要在@EnableAsync上设置proxyTargetClass=true来强制使用基于cglib的代理。注解上加上即可。
Springboot使用@Async开启异步调用
大家都知道,java是同步顺序执行。当需要异步执行时,需要新创建一个线程完成。
1.使用常规的方法显示异步调用
第一步新建ThreadTest.java实现Runnable接口
第二步新建测试执行
当然,除了这种显式newThread对象,我们通过线程池获取线程名称,这里不做演示。我们熟悉的spring在spring3中提供了@Async注解,来方便开发者优雅的使用异步调用。
2.使用springboot@Async注解,优雅的实现异步调用
第一步开启异步调用注解。
第二步定义线程池
第三步创建service测试类TestService.java
第四步新建Service实现类,TestServiceImpl.java
第五步测试执行,执行结果
SpringBoot使用@Async优雅的异步调用就暂时记录到这里,欢迎评论区一起讨论学习。
一图看懂SpringBoot异步框架在SpringBoot的日常开发中,一般都是同步调用的。但经常有特殊业务需要做异步来处理,例如:注册新用户,送100个积分,或下单成功,发送push消息等等。
就拿注册新用户为什么要异步处理?
在SpringBoot中使用异步调用是很简单的,只需要使用@Async注解即可实现方法的异步调用。
采用@EnableAsync来开启异步任务支持,另外需要加入@Configuration来把当前类加入springIOC容器中。
增加一个service类,用来做积分处理。
@Async添加在方法上,代表该方法为异步处理。
@Async注解,在默认情况下用的是SimpleAsyncTaskExecutor线程池,该线程池不是真正意义上的线程池,因为线程不重用,每次调用都会新建一条线程。
可以通过控制台日志输出查看,每次打印的线程名都是[task-1]、[task-2]、[task-3]、[task-4].....递增的。
@Async注解异步框架提供多种线程
SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
ThreadPoolTaskScheler:可以使用cron表达式。
ThreadPoolTaskExecutor:最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。