SpringBoot(19)异步任务

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

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

1、Java线程处理

在 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等方式将结果返回,详细内容如下

public class MyReturnableTask implements Callable {
    @Override
    public String call() throws Exception {
        long startTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+"线程运行开始");
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName()+"线程运行结束");
        return "result";
    }
}
    @GetMapping("/task")
    public void task() throws ExecutionException, InterruptedException {
        MyReturnableTask myReturnableTask = new MyReturnableTask();
        FutureTask futureTask = new FutureTask(myReturnableTask);
        Thread thread = new Thread(futureTask, "returnableThread");
        thread.start();
        String s = futureTask.get();
        System.out.println(s);
    }

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



2、SpringBoot异步任务

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

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

2.2、使用@Async注解标记要进行异步执行的方法

@Service
public class AsyncService {
    @Async
    public void t1() throws InterruptedException {
        // 模拟耗时任务
        Thread.sleep(TimeUnit.SECONDS.toMillis(5));
    }
    @Async
    public Future t2() throws InterruptedException {
        // 模拟耗时任务
        Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        return new AsyncResult<>("async tasks done!");
    }
}

2.3、controller测试

    @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()方法。


3、异步任务相关限制

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

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

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

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

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

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

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

4、自定义 Executor

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

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

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

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

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

4.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;
    }
}

4.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;

/**
 * @author qlx
 */
@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 t2() throws InterruptedException {
        // 模拟耗时任务
        Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        return new AsyncResult<>("async tasks done!");
    }
}

5、异常处理

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

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


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

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

@Configuration
@EnableAsync
public class AsyncConfigure implements AsyncConfigurer {

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

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

5.3、测试

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

你可能感兴趣的:(spring,boot)