Spring Boot - 异步任务

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot - 异步任务相关的知识,希望对你有一定的参考价值。

参考技术A

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Spring Boot入门系列如何实现异步执行任务

前面介绍了Spring Boot 如何整合定时任务,不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/category/1657780.html

今天主要讲解Spring Boot中的另外一个任务:异步任务。所谓异步任务,其实就是异步执行程序,有些时候遇到一些耗时的的任务,如果一直卡等待,肯定会影响其他程序的执行,所以就让这些程序需要以异步的方式去执行。那么下面就来介绍Spring Boot 如何实现异步任务。

 

一、使用注解@EnableAsync 开启异步调用方法

在application启动类中,加上@EnableAsync注解,Spring Boot 会自动扫描异步任务。

复制代码
package com.weiz;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
//扫描 mybatis mapper 包路径
@MapperScan(basePackages = "com.weiz.mapper")
//扫描 所有需要的包, 包含一些自用的工具类包 所在的路径
@ComponentScan(basePackages = {"com.weiz","org.n3r.idworker"})
//开启定时任务
@EnableScheduling
//开启异步调用方法
@EnableAsync
public class SpringBootStarterApplication {
public static void main(String[] args) { SpringApplication.run(SpringBootStarterApplication.class, args); } }
复制代码

 

二、创建异步执行类,定义@Component及@Async组件

创建com.weiz.tasks包,在tasks包里增加AsyncTask 异步任务类,加上@Component 注解,然后在需要异步执行的方法前面加上@Async注解,这样Spring Boot容器扫描到相关异步方法之后,调用时就会将这些方法异步执行。

package com.weiz.tasks;

import java.util.concurrent.Future;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;

@Component
public class AsyncTask {
    
    @Async
    public Future<Boolean> doTask11() throws Exception {
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println("任务1耗时:" + (end - start) + "毫秒");
        return new AsyncResult<>(true);
    }
    
    @Async
    public Future<Boolean> doTask22() throws Exception {
        long start = System.currentTimeMillis();
        Thread.sleep(700);
        long end = System.currentTimeMillis();
        System.out.println("任务2耗时:" + (end - start) + "毫秒");
        return new AsyncResult<>(true);
    }
    
    @Async
    public Future<Boolean> doTask33() throws Exception {
        long start = System.currentTimeMillis();
        Thread.sleep(600);
        long end = System.currentTimeMillis();
        System.out.println("任务3耗时:" + (end - start) + "毫秒");
        return new AsyncResult<>(true); 
    }
}

说明:@Async 加上这个注解,就表示该方法是异步执行方法。

 

三、调用

创建一个DoTask调用类,我们看看这几个方法,是怎么执行的:

package com.weiz.tasks;

import java.util.concurrent.Future;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("tasks")
public class DoTask {

    @Autowired
    private AsyncTask asyncTask;

    @RequestMapping("test1")
    public String test1() throws Exception {

        long start = System.currentTimeMillis();

        Future<Boolean> a = asyncTask.doTask11();
        Future<Boolean> b = asyncTask.doTask22();
        Future<Boolean> c = asyncTask.doTask33();

        while (!a.isDone() || !b.isDone() || !c.isDone()) {
            if (a.isDone() && b.isDone() && c.isDone()) {
                break;
            }
        }

        long end = System.currentTimeMillis();

        String times = "任务全部完成,总耗时:" + (end - start) + "毫秒";
        System.out.println(times);

        return times;
    }
}

 

四、测试

启动程序之后,在浏览器输入:http://localhost:8080/tasks/test1  。

 

从这个总耗时可以看出,三个方法确实是异步执行的。耗时接近时间最长的doTask11方法。

 

最后

以上,就把Spring Boot 创建异步任务的方法简单介绍完了,是不是特别简单。

这个系列课程的完整源码,也会提供给大家。大家关注我的微信公众号(架构师精进),回复:springboot源码。获取这个系列课程的完整源码。

 

 

以上是关于Spring Boot - 异步任务的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot入门系列如何实现异步执行任务

Spring Boot 整合定时任务和异步任务处理

spring boot 1.5.4 定时任务和异步调用

Spring Boot Async异步执行任务

SpringBoot系列:Spring Boot异步调用@Async

Spring Boot中有多个@Async异步任务时,记得做好线程池的隔离!