springboot(10)异步任务

在SpringBoot应用程序中,有时需要执行一些长时间运行的操作,如发送电子邮件或从外部API获取数据。 这些操作可能需要几秒钟或几分钟才能完成。 如果您在主线程上执行此类操作,则应用程序停止响应,可能会导致用户体验不佳。

为了避免这种情况,并使应用程序在执行此类操作时继续响应,我们可以使用SpringBoot的异步任务功能。 异步任务是指可以在后台线程上执行的任务,因此不会阻塞主线程。

有时候,前端可能提交了一个耗时任务,如果后端接收到请求后,直接执行该耗时任务,那么前端需要等待很久一段时间才能接受到响应。如果该耗时任务是通过浏览器直接进行请求,那么浏览器页面会一直处于转圈等待状态。

事实上,当后端要处理一个耗时任务时,通常都会将耗时任务提交到一个异步任务中进行执行,此时前端提交耗时任务后,就可直接返回,进行其他操作。

1、SpringBoot异步任务

SpringBoot开启异步任务的步骤如下:

  1. 在SpringBootApplication类上添加@EnableAsync注解,启用SpringBoot异步任务支持。
  2. 在异步方法所在的类上添加@Async注解,标记该类中需要异步执行的方法。
  3. 在异步方法中使用CompletableFuture等类处理异步操作。

使用SpringBoot异步任务功能可以轻松地将长时间运行的操作转换为异步任务,提高应用程序的响应性能和用户体验。

1.1使用注解EnableAsync开启异步任务支持

@SpringBootApplication
@EnableAsync
public class ApplicationStarter {
    
    
  public static void main(String[] args) {
    
    
      SpringApplication.run(ApplicationStarter.class,args);
  }
}

1.2使用@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!");
    }
}

1.3controller测试

    @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注解的方法可以接受任意类型参数,但只能返回voidFuture类型数据

所以当异步方法返回数据时,需要使用Future包装异步任务结果,上述代码使用AsyncResult包装异步任务结果,AsyncResult间接继承Future,是 Spring 提供的一个可用于追踪异步方法执行结果的包装类。其他常用的Future类型还有 Spring 4.2 提供的ListenableFuture,或者 JDK 8 提供的CompletableFuture,这些类型可提供更丰富的异步任务操作。

如果前端需要获取耗时任务结果,则异步任务方法应当返回一个Future类型数据,此时Controller相关接口需要调用该Futureget()方法获取异步任务结果,get()方法是一个阻塞方法,因此该操作相当于将异步任务转换为同步任务,浏览器同样会面临我们前面所讲的转圈等待过程,但是异步执行还是有他的好处的,因为我们可以控制get()方法的调用时序,因此可以先执行其他一些操作后,最后再调用get()方法。

2.异步任务相关限制

@Async注解的异步任务方法存在相关限制:

  • @Async注解的方法必须是public的,这样方法才可以被代理。
  • 不能在同一个类中调用@Async方法,因为同一个类中调用会绕过方法代理,调用的是实际的方法。
  • @Async注解的方法不能是static
  • @Async注解不能与 Bean 对象的生命周期回调函数(比如@PostConstruct)一起注解到同一个方法中。
  • 异步类必须注入到 Spring IOC 容器中(也即异步类必须被@Component/@Service等进行注解)。
  • 其他类中使用异步类对象必须通过@Autowired等方式进行注入,不能手动new对象。

SpringBoot异步任务功能虽然可以提高应用程序的响应性能,但还是有一些限制需要注意:

  1. 异步方法不能与同步方法在同一个类中定义。必须将异步方法定义在单独的类中,或者使用CGLIB代理来将异步方法包装在另一个类中。
  2. 异步方法不能是private或final的。
  3. 异步方法不能返回void类型。必须返回一个Future或CompletableFuture对象,以便在异步任务完成后处理结果。
  4. 如果异步方法在同一类中调用另一个异步方法,则调用将不会异步执行,而是在同一线程中同步执行。

总之,虽然SpringBoot的异步任务功能可以提高应用程序的响应性能,但在使用时需要注意这些限制,以确保异步方法能够正确地执行并保持应用程序的稳定性。

3.1自定义 Executor

默认情况下,Spring 会自动搜索相关线程池定义:要么是一个唯一TaskExecutor Bean 实例,要么是一个名称为taskExecutorExecutor Bean 实例。如果这两个 Bean 实例都不存在,就会使用SimpleAsyncTaskExecutor来异步执行被@Async注解的方法。

综上,可以知道,默认情况下,Spring 使用的 Executor 是SimpleAsyncTaskExecutorSimpleAsyncTaskExecutor每次调用都会创建一个新的线程,不会重用之前的线程。很多时候,这种实现方式不符合我们的业务场景,因此通常我们都会自定义一个 Executor 来替换SimpleAsyncTaskExecutor

对于自定义 Executor(自定义线程池),可以分为如下两个层级:

  • 应用层级:即全局生效的 Executor。依据 Spring 默认搜索机制,其实就是配置一个全局唯一的TaskExecutor实例或者一个名称为taskExecutorExecutor实例即可,如下所示:
  • 方法层级:即为单独一个或多个方法指定运行线程池,其他未指定的异步方法运行在默认线程池。如下所示:

3.1.1应用层级:

下面代码定义了一个名称为taskExecutorExecutor,此时@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;
    }
}

3.1.2方法层级:

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作为所有@AsyncExecutor,而设置了多个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!");
    }
}

3.2自定义 Executor (第二种方式)

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类中定义的自定义任务执行器的名称。

总之,通过自定义任务执行器,我们可以更好地控制异步任务的执行,从而提高应用程序的性能和可靠性。

4.1异常处理

前文介绍过,对于被@Async注解的异步方法,只能返回void或者Future类型。对于返回Future类型数据,如果异步任务方法抛出异常,则很容易进行处理,因为Future.get()会重新抛出该异常,我们只需对其进行捕获即可。但是对于返回void的异步任务方法,异常不会传播到被调用者线程,因此我们需要自定义一个额外的异步任务异常处理器,捕获异步任务方法抛出的异常。

4.1.1自定义一个异常处理器类实现接口如下所示:

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);
        }
    }
}

4.1.2创建一个自定义Executor异步配置类,将我们的自定义异常处理器设置到其接口上

@Configuration
@EnableAsync
public class AsyncConfigure implements AsyncConfigurer {
    
    
 
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    
    
        return new CustomAsyncExceptionHandler();
    }
}

此时异步方法如果抛出异常,就可以被我们的自定义异步异常处理器捕获得到。

4.1.3测试

@Service
public class AsyncService {
    
    
    @Async("methodLevelExecutor1")
    public void t1() throws InterruptedException {
    
    
        // 模拟耗时任务
        Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        throw new NullPointerException();
    }

4.2异常处理(第二种方式)

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()方法。这些方法可以帮助我们更好地管理和处理异步任务的异常,提高应用程序的可靠性和稳定性。

猜你喜欢

转载自blog.csdn.net/Bilal_0/article/details/129953635