在SpringBoot应用程序中,有时需要执行一些长时间运行的操作,如发送电子邮件或从外部API获取数据。 这些操作可能需要几秒钟或几分钟才能完成。 如果您在主线程上执行此类操作,则应用程序停止响应,可能会导致用户体验不佳。
为了避免这种情况,并使应用程序在执行此类操作时继续响应,我们可以使用SpringBoot的异步任务功能。 异步任务是指可以在后台线程上执行的任务,因此不会阻塞主线程。
有时候,前端可能提交了一个耗时任务,如果后端接收到请求后,直接执行该耗时任务,那么前端需要等待很久一段时间才能接受到响应。如果该耗时任务是通过浏览器直接进行请求,那么浏览器页面会一直处于转圈等待状态。
事实上,当后端要处理一个耗时任务时,通常都会将耗时任务提交到一个异步任务中进行执行,此时前端提交耗时任务后,就可直接返回,进行其他操作。
SpringBoot开启异步任务的步骤如下:
使用SpringBoot异步任务功能可以轻松地将长时间运行的操作转换为异步任务,提高应用程序的响应性能和用户体验。
@SpringBootApplication
@EnableAsync
public class ApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ApplicationStarter.class,args);
}
}
@Async
注解标记要进行异步执行的方法@Service
public class AsyncService {
@Async
public void t1() throws InterruptedException {
// 模拟耗时任务
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
}
@Async
public Future<String> t2() throws InterruptedException {
// 模拟耗时任务
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
return new AsyncResult<>("async tasks done!");
}
}
@Autowired
private AsyncService asyncService;
@GetMapping("/task1")
public String asyncTaskWithoutReturnType() throws InterruptedException {
asyncService.t1();
return "rrrr";
}
@GetMapping("/task2")
public String asyncTaskWithReturnType() throws InterruptedException, ExecutionException {
asyncService.t2();
return "aaaaaaa";
}
被@Async
注解的方法可以接受任意类型参数,但只能返回void
或Future
类型数据
所以当异步方法返回数据时,需要使用Future
包装异步任务结果,上述代码使用AsyncResult包装异步任务结果,AsyncResult
间接继承Future
,是 Spring 提供的一个可用于追踪异步方法执行结果的包装类。其他常用的Future
类型还有 Spring 4.2 提供的ListenableFuture,或者 JDK 8 提供的CompletableFuture,这些类型可提供更丰富的异步任务操作。
如果前端需要获取耗时任务结果,则异步任务方法应当返回一个Future
类型数据,此时Controller
相关接口需要调用该Future
的get()
方法获取异步任务结果,get()
方法是一个阻塞方法,因此该操作相当于将异步任务转换为同步任务,浏览器同样会面临我们前面所讲的转圈等待过程,但是异步执行还是有他的好处的,因为我们可以控制get()
方法的调用时序,因此可以先执行其他一些操作后,最后再调用get()
方法。
被@Async
注解的异步任务方法存在相关限制:
@Async
注解的方法必须是public
的,这样方法才可以被代理。@Async
方法,因为同一个类中调用会绕过方法代理,调用的是实际的方法。@Async
注解的方法不能是static
。@Async
注解不能与 Bean 对象的生命周期回调函数(比如@PostConstruct
)一起注解到同一个方法中。@Component
/@Service
等进行注解)。@Autowired
等方式进行注入,不能手动new
对象。SpringBoot异步任务功能虽然可以提高应用程序的响应性能,但还是有一些限制需要注意:
总之,虽然SpringBoot的异步任务功能可以提高应用程序的响应性能,但在使用时需要注意这些限制,以确保异步方法能够正确地执行并保持应用程序的稳定性。
默认情况下,Spring 会自动搜索相关线程池定义:要么是一个唯一TaskExecutor Bean 实例,要么是一个名称为taskExecutor
的Executor Bean 实例。如果这两个 Bean 实例都不存在,就会使用SimpleAsyncTaskExecutor来异步执行被@Async
注解的方法。
综上,可以知道,默认情况下,Spring 使用的 Executor 是SimpleAsyncTaskExecutor
,SimpleAsyncTaskExecutor
每次调用都会创建一个新的线程,不会重用之前的线程。很多时候,这种实现方式不符合我们的业务场景,因此通常我们都会自定义一个 Executor 来替换SimpleAsyncTaskExecutor
。
对于自定义 Executor(自定义线程池),可以分为如下两个层级:
TaskExecutor
实例或者一个名称为taskExecutor
的Executor
实例即可,如下所示:下面代码定义了一个名称为taskExecutor
的Executor
,此时@Async
方法默认就会运行在该Executor
中。
@Configuration
public class ExcuterConfig {
@Bean("taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
int cores = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(cores);
// 设置最大线程数
executor.setMaxPoolSize(20);
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 设置线程默认前缀名
executor.setThreadNamePrefix("Application-Level-Async-");
return executor;
}
}
package com.buba.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class ExcuterConfig {
@Bean("methodLevelExecutor1")
public TaskExecutor getAsyncExecutor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(4);
// 设置最大线程数
executor.setMaxPoolSize(20);
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 设置线程默认前缀名
executor.setThreadNamePrefix("Method-Level-Async1-");
return executor;
}
@Bean("methodLevelExecutor2")
public TaskExecutor getAsyncExecutor2() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(8);
// 设置最大线程数
executor.setMaxPoolSize(20);
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 设置线程默认前缀名
executor.setThreadNamePrefix("Method-Level-Async2-");
return executor;
}
}
上述特意设置了多个TaskExecutor
,因为如果只设置一个TaskExecutor
,那么 Spring 就会默认采用该TaskExecutor
作为所有@Async
的Executor
,而设置了多个TaskExecutor
,Spring 检测到全局存在多个Executor
,就会降级使用默认的SimpleAsyncTaskExecutor
,此时我们就可以为@Async
方法配置执行线程池,其他未配置的@Async
就会默认运行在SimpleAsyncTaskExecutor
中,这就是方法层级的自定义 Executor。如下代码所示:
@Service
public class AsyncService {
@Async("methodLevelExecutor1")
public void t1() throws InterruptedException {
// 模拟耗时任务
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
}
@Async("methodLevelExecutor2")
public Future<String> t2() throws InterruptedException {
// 模拟耗时任务
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
return new AsyncResult<>("async tasks done!");
}
}
Spring Boot提供了默认的任务执行器,但是有时候我们需要自定义任务执行器以更好地控制任务执行的线程池。在Spring Boot中,我们可以通过实现AsyncConfigurer接口来自定义任务执行器。下面是一个使用自定义任务执行器的示例:
@Configuration
@EnableAsync
public class AppConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("MyExecutor-");
executor.initialize();
return executor;
}
}
在上面的示例中,我们实现了AsyncConfigurer接口,并覆盖了getAsyncExecutor()方法。在该方法中,我们创建了一个ThreadPoolTaskExecutor对象,并设置了线程池的核心线程数、最大线程数、队列容量和线程名称前缀。最后,我们返回这个线程池对象。
通过实现AsyncConfigurer接口并覆盖getAsyncExecutor()方法,我们可以轻松地自定义任务执行器。在应用程序中使用自定义任务执行器时,只需将其添加到异步方法所在的类上即可。例如:
@Service
public class MyService {
@Async("myExecutor")
public CompletableFuture<String> longRunningMethod() {
// long running code here
}
}
在上面的示例中,我们将@Async注解的value属性设置为"myExecutor",这是我们在AppConfig类中定义的自定义任务执行器的名称。
总之,通过自定义任务执行器,我们可以更好地控制异步任务的执行,从而提高应用程序的性能和可靠性。
前文介绍过,对于被@Async
注解的异步方法,只能返回void
或者Future
类型。对于返回Future
类型数据,如果异步任务方法抛出异常,则很容易进行处理,因为Future.get()
会重新抛出该异常,我们只需对其进行捕获即可。但是对于返回void
的异步任务方法,异常不会传播到被调用者线程,因此我们需要自定义一个额外的异步任务异常处理器,捕获异步任务方法抛出的异常。
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
System.out.println("Exception message - " + throwable.getMessage());
System.out.println("Method name - " + method.getName());
for (Object param : objects) {
System.out.println("Parameter value - " + param);
}
}
}
Executor
异步配置类,将我们的自定义异常处理器设置到其接口上@Configuration
@EnableAsync
public class AsyncConfigure implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncExceptionHandler();
}
}
此时异步方法如果抛出异常,就可以被我们的自定义异步异常处理器捕获得到。
@Service
public class AsyncService {
@Async("methodLevelExecutor1")
public void t1() throws InterruptedException {
// 模拟耗时任务
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
throw new NullPointerException();
}
SpringBoot异步任务的异常可以通过@Async注解的exceptional属性捕获。将exceptional属性设置为要捕获的异常类型即可。例如,以下示例演示如何捕获RuntimeException:
@Async(exceptional = RuntimeException.class)
public CompletableFuture<String> sendEmail() {
// send email code here that may throw a RuntimeException
}
在上面的示例中,如果sendEmail()方法抛出RuntimeException,则该异常将被捕获并包装在CompletableFuture对象中返回。
另一种捕获异步任务异常的方法是使用@Async注解的异步方法的返回类型。如果异步方法的返回类型是CompletableFuture,则可以使用CompletableFuture的exceptionally()方法捕获异步任务的异常。例如,以下示例演示如何使用CompletableFuture的exceptionally()方法捕获异步任务的异常:
@Async
public CompletableFuture<String> sendEmail() {
// send email code here that may throw a RuntimeException
}
CompletableFuture<String> future = sendEmail();
future.exceptionally(ex -> {
// handle the exception here
return "Error sending email: " + ex.getMessage();
});
在上面的示例中,如果sendEmail()方法抛出RuntimeException,则可以在future.exceptionally()方法中捕获该异常并处理它。注意,exceptionally()方法返回一个新的CompletableFuture对象,该对象包含捕获到的异常或原始异步操作的结果。
总之,SpringBoot提供了多种捕获异步任务异常的方法,包括使用@Async注解的exceptional属性和CompletableFuture对象的exceptionally()方法。这些方法可以帮助我们更好地管理和处理异步任务的异常,提高应用程序的可靠性和稳定性。