首页 > 其他分享 >Spring Boot - 异步任务

Spring Boot - 异步任务

时间:2023-06-26 12:14:54浏览次数:53  
标签:异步 Spring Boot 线程 executor Async public

Spring Boot - 异步任务

前言

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

@RestController
@RequestMapping("async")
public class AsyncController {

    @GetMapping("/")
    public String index() throws InterruptedException {
        // 模拟耗时操作
        Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        return "consuming time behavior done!";
    }
}

当我们在浏览器请求localhost:8080/async/页面时,可以看到浏览器一直处于转圈等待状态,这样体验十分不友好。

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

在 Java 中,开启异步任务最常用的方式就是开辟线程执行异步任务,如下所示:

@RestController
@RequestMapping("async")
public class AsyncController {

    @GetMapping("/")
    public String index() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 模拟耗时操作
                    Thread.sleep(TimeUnit.SECONDS.toMillis(5));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        return "consuming time behavior processing!";
    }
}

这时浏览器请求localhost:8080/async/,就可以很快得到响应,并且耗时任务会在后台得到执行。

一般来说,前端不会关注耗时任务结果,因此前端只需负责提交该任务给到后端即可。但是如果前端需要获取耗时任务结果,则可通过Future等方式将结果返回,详细内容请参考后文。

事实上,在 Spring Boot 中,我们不需要手动创建线程异步执行耗时任务,因为 Spring 框架已提供了相关异步任务执行解决方案,本文主要介绍下在 Spring Boot 中执行异步任务的相关内容。

执行异步任务

Spring 3.0 时提供了一个@Async注解,该注解用于标记要进行异步执行的方法,当在其他线程调用被@Async注解的方法时,就会开启一个线程执行该方法。

@Async注解通常用在方法上,但是也可以用作类型上,当类被@Async注解时,表示该类中所有的方法都是异步执行的。

在 Spring Boot 中,如果要执行一个异步任务,只需进行如下两步操作:

  1. 使用注解@EnableAsync开启异步任务支持,如下所示:

    @SpringBootApplication
    @ComponentScan("com.yn.async")
    @EnableAsync // 开启异步调用
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    @EnableAsync注解可以让 Spring 开启异步方法执行,它会让 Spring 扫描被其注解的包及其子包下被@Async注解的类或方法,所以这里我们在根包下配置@EnableAsync

  2. 使用@Async注解标记要进行异步执行的方法,如下所示:

    @Service // 假设当前类是一个 Service
    @Slf4j
    public class AsyncTaskService {
    
        @Async
        public void asyncTaskWithoutReturnType() throws InterruptedException {
            log.info("asyncTaskWithoutReturnType: AsyncTaskService Thread = {}",Thread.currentThread().getName());
            // 模拟耗时任务
            Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        }
    
        @Async
        public Future<String> asyncTaskWithReturnType() throws InterruptedException {
            log.info("asyncTaskWithReturnType: AsyncTaskService Thread = {}",Thread.currentThread().getName());
            // 模拟耗时任务
            Thread.sleep(TimeUnit.SECONDS.toMillis(5));
            return new AsyncResult<>("async tasks done!");
        }
    }
    

    上述代码使用@Async标记了两个异步执行方法,一个没有返回值的asyncTaskWithoutReturnType,另一个拥有返回值asyncTaskWithReturnType,这里需要注意的一点时,被@Async注解的方法可以接受任意类型参数,但只能返回voidFuture类型数据。所以当异步方法返回数据时,需要使用Future包装异步任务结果,上述代码使用AsyncResult包装异步任务结果,AsyncResult间接继承Future,是 Spring 提供的一个可用于追踪异步方法执行结果的包装类。其他常用的Future类型还有 Spring 4.2 提供的ListenableFuture,或者 JDK 8 提供的CompletableFuture,这些类型可提供更丰富的异步任务操作。

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

  3. 经过前面两个步骤后,其实就已经完成了异步任务配置。到此就可以调用这些异步任务方法,如下所示:

    @RestController
    @RequestMapping("async")
    @Slf4j
    public class AsyncController {
    
        @Autowired // 注入异步任务类
        private AsyncTaskService asyncTaskService;
    
        @GetMapping("/asyncTaskWithoutReturnType")
        public void asyncTaskWithoutReturnType() throws InterruptedException {
            log.info("asyncTaskWithoutReturnType: Controller Thread = {}",Thread.currentThread().getName());
            this.asyncTaskService.asyncTaskWithoutReturnType();
        }
    
        @GetMapping("/asyncTaskWithReturnType")
        public String asyncTaskWithReturnType() throws Exception {
            log.info("asyncTaskWithReturnType: Controller Thread = {}",Thread.currentThread().getName());
            Future<String> future = this.asyncTaskService.asyncTaskWithReturnType();
            return future.get();
        }
    }
    

    请求上述两个接口,如下所示:

    $ curl -X GET localhost:8080/async/asyncTaskWithoutReturnType
    
    $ curl -X GET localhost:8080/async/asyncTaskWithReturnType
    async tasks done!
    

    查看日志,如下图所示:

    可以看到,异步任务方法运行在于Controller不同的线程上。

异步任务相关限制

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

  • @Async注解的方法必须是public的,这样方法才可以被代理。

  • 不能在同一个类中调用@Async方法,因为同一个类中调用会绕过方法代理,调用的是实际的方法。

  • @Async注解的方法不能是static

  • `@Async`不能用于被`@Configuration`注解的类方法上。
    官方文档写的是不能在@Configuration类中使用,但本人实际测试发现,无论是将@Async注解到@Configuration类上,还是将@Async注解到方法上,都是可以异步执行方法的。

  • @Async注解不能与 Bean 对象的生命周期回调函数(比如@PostConstruct)一起注解到同一个方法中。解决方法可参考:Spring - The @Async annotation

  • 异步类必须注入到 Spring IOC 容器中(也即异步类必须被@Component/@Service等进行注解)。

  • 其他类中使用异步类对象必须通过@Autowired等方式进行注入,不能手动new对象。

自定义 Executor

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

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

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

  • 方法层级:即为单独一个或多个方法指定运行线程池( @Async("methodLevelExecutor1")),其他未指定的异步方法运行在默认线程池。如下所示:

    @SpringBootApplication
    @ComponentScan("com.yn.async")
    @EnableAsync
    public class Application {
        // ...
        @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。如下代码所示:

    @RestController
    @RequestMapping("async")
    @Slf4j
    public class AsyncController {
    
        @Autowired // 注入异步任务类
        private AsyncTaskService asyncTaskService;
    
        @GetMapping("/asyncTaskWithoutReturnType")
        public void asyncTaskWithoutReturnType() throws InterruptedException {
            log.info("asyncTaskWithoutReturnType: Controller Thread = {}",Thread.currentThread().getName());
            this.asyncTaskService.asyncTaskWithoutReturnType();
        }
    
        @GetMapping("/asyncTaskWithReturnType")
        public String asyncTaskWithReturnType() throws Exception {
            log.info("asyncTaskWithReturnType: Controller Thread = {}",Thread.currentThread().getName());
            Future<String> future = this.asyncTaskService.asyncTaskWithReturnType();
            return future.get();
        }
    }
    

    请求上述接口,如下所示:

    $ curl -X GET localhost:8080/async/asyncTaskWithoutReturnType
    
    $ curl -X GET localhost:8080/async/asyncTaskWithReturnType
    async tasks done!
    

    请求日志如下所示:

    2020-09-25 00:55:31,953 INFO  [http-nio-8080-exec-1] com.yn.async.AsyncController: asyncTaskWithoutReturnType: Controller Thread = http-nio-8080-exec-1
    2020-09-25 00:55:31,984 INFO  [Method-Level-Async1-1] com.yn.async.AsyncTaskService: asyncTaskWithoutReturnType: AsyncTaskService Thread = Method-Level-Async1-1
    2020-09-25 00:55:45,592 INFO  [http-nio-8080-exec-2] com.yn.async.AsyncController: asyncTaskWithReturnType: Controller Thread = http-nio-8080-exec-2
    2020-09-25 00:55:45,594 INFO  [http-nio-8080-exec-2] org.springframework.aop.interceptor.AsyncExecutionAspectSupport: More than one TaskExecutor bean found within the context, and none is named 'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly as an alias) in order to use it for async processing: [methodLevelExecutor1, methodLevelExecutor2]
    2020-09-25 00:55:45,595 INFO  [SimpleAsyncTaskExecutor-1] com.yn.async.AsyncTaskService: asyncTaskWithReturnType: AsyncTaskService Thread = SimpleAsyncTaskExecutor-1
    

    结果跟我们上述的分析一致。

  • 应用层级:即全局生效的 Executor。依据 Spring 默认搜索机制,其实就是配置一个全局唯一的TaskExecutor实例或者一个名称为taskExecutorExecutor实例即可,如下所示:

    @SpringBootApplication
    @ComponentScan("com.yn.async")
    @EnableAsync
    public class Application {
        // ...
        @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;
        }
    }
    

    上述代码定义了一个名称为taskExecutorExecutor,此时@Async方法默认就会运行在该Executor中。

    其实 Spring 还提供了另一个功能更加强大的接口AsyncConfigurer,该接口主要是用于自定义一个Executor配置类,提供了应用层级Executor接口,以及对于@Async方法异常捕获功能。如果 Spring 检测到该接口实例,会优先采用该接口自定义的Executor。如下所示:

    @Configuration
    @EnableAsync
    public class AsyncConfigure implements AsyncConfigurer {
        @Override
        public Executor getAsyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            // 设置核心线程数
            int cores = Runtime.getRuntime().availableProcessors();
            executor.setCorePoolSize(cores);
            // 设置最大线程数
            executor.setMaxPoolSize(20);
            // 等待所有任务结束后再关闭线程池
            executor.setWaitForTasksToCompleteOnShutdown(true);
            // 设置线程默认前缀名
            executor.setThreadNamePrefix("AsyncConfigure-");
            // 注意,此时需要调用 initialize
            executor.initialize();
            return executor;
        }
    }
    

    :使用自定义实现AsyncConfigurer接口的配置类的另一个好处就是无论@EnableAsync的包层级多深,默认都会对整个项目扫描@Async方法,这样我们就无需将@EnableAsync注解到根包类中。

异常处理

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

自定义异步任务异常处理器的步骤如下所示:

  1. 首先自定义一个异常处理器类实现接口AsyncUncaughtExceptionHandler,如下所示:

    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);
            }
        }
    }
    
  2. 然后,创建一个自定义Executor异步配置类,将我们的自定义异常处理器设置到其接口上。如下所示:

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

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

参考

参考:

https://www.jianshu.com/p/20a4e37314fc

标签:异步,Spring,Boot,线程,executor,Async,public
From: https://www.cnblogs.com/firsthelloworld/p/17505284.html

相关文章

  • spring boot 编译打包时将自定义引入的.jar包依赖,全部打包进去
    发现自己引入的.jar包,在打包时,.jar包并不会打进去,导致报错。打包时打入自定义.jar包方法:<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</a......
  • 通过网关添加过滤器【SpringCloud】
    spring:application:name:gateway#服务名称cloud:nacos:server-addr:localhost:8848#nacos地址gateway:routes:#网关路由配置-id:itemservice#路由id,自定义,只要唯一即可#uri:http://127.0.0.1:8081#路由......
  • Springboot graceful shutdown
    很多情况下,在应用程序启动后需要关闭时候,直接shutdown会导致正在处理的请求中断,而采用gracefulshutdown可以实现不再接受新的请求,并将已接收到的请求处理完毕再关闭程序释放资源 Springbootgracefulshutdown应用场景Springboot中提供了优雅停机方案,在关闭阶段会给应用程序......
  • SpringCloud Alibaba入门5之Hystrix的使用
    我们继续在前一章的基础上进行学习。https://blog.51cto.com/u_13312531/6546544使用目的:上一章我们已经使用OpenFeign完成了服务间的调用,如果现在存在大量的服务,每个服务有若干个节点,其中一个节点发生故障,加入的请求一直阻塞,大量堆积的请求会把服务打崩,可能导致级联式的失败,甚至......
  • 一天吃透Spring面试八股文
    原出处:topjavaer.cnSpring是什么?Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。最全面的Java面试网站:最全面的Java面试网站Spring的优点通过控制反转和依赖注入实现松耦合。支持面向切面的编程,并且把应用业务逻辑和系统服务分开。通过切面和模板减少样板......
  • spring boot graalvm native 试用
    核心是体验下新版本支持情况同时体验下企业特性(g1gc)参考示例就是基于springstart提供的web,添加了graalvmnative构建支持,graalvmoracle发行版直接可以官方下载pom.xml核心信息<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apach......
  • Microsoft Message Queuing(MSMQ)是由微软开发的一种消息队列服务,用于在分布式应用程序
    MicrosoftMessageQueuing(MSMQ)是由微软开发的一种消息队列服务,用于在分布式应用程序之间进行异步通信。它提供了一种可靠的方式来在不同的应用程序之间发送消息,并确保消息的可靠传递。MSMQ基于消息队列的原理,应用程序可以将消息发送到队列中,然后其他应用程序可以从队列中接收这......
  • 【SpringCloud】Hystrix
    目录1.前言1.1什么是服务雪崩?1.2怎么解决服务雪崩2.Hystrix快速入门2.1Hystrix+OpenFeign2.1.1服务提供者2.1.2服务消费者2.1.3测试2.2Hystrix+Ribbon3.Hystrix的常用配置......
  • spring里的@ImportResource注解介绍
    @ImportResource注解是Spring框架中的一个注解,它用于导入外部的XML配置文件。通过@ImportResource注解,可以将外部的XML配置文件加载到Spring的应用上下文中,从而使得这些配置文件中定义的Bean能够被Spring容器管理。使用@ImportResource注解的步骤如下:在需要使......
  • springboot+vue基于Web的社区医院管理服务系统,附源码+数据库+论文+PPT,适合课程设计、
    1、项目介绍在Internet高速发展的今天,我们生活的各个领域都涉及到计算机的应用,其中包括社区医院管理服务系统的网络应用,在外国线上管理系统已经是很普遍的方式,不过国内的管理系统可能还处于起步阶段。社区医院管理服务系统具有社区医院信息管理功能的选择。社区医院管理服务系统......