SpringBoot教程(二十) | SpringBoot整合异步任务

SpringBoot教程(二十) | SpringBoot整合异步任务

需求:某个方法里面存在多个处理逻辑的操作,其中有一个调有短信/邮箱的逻辑操作,如何不堵塞整个方法,实现异步操作

一、使用线程(Thread),实现"类似异步"操作

重点:线程本身不是异步的,但是由于线程之间是并发执行的,这确实会产生类似于异步行为的效果

以下是一个简单的Spring Boot案例,展示了如何在服务层中使用Thread来实现类似异步的操作。

首先,我们需要一个服务类,该类将包含一个方法,该方法将使用Thread来异步执行某些任务:

java 复制代码
import org.springframework.stereotype.Service;

@Service
public class AsyncService {

    // 使用Thread来模拟异步操作
    public void performAsyncTask(String taskName) {
        // 创建一个新的线程来执行异步任务
        new Thread(() -> {
            // 模拟耗时的异步操作
            try {
                System.out.println(Thread.currentThread().getName() + " 开始执行异步任务: " + taskName);
                Thread.sleep(2000); // 假设这个任务需要2秒钟来完成
                System.out.println(Thread.currentThread().getName() + " 异步任务完成: " + taskName);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("异步任务被中断: " + taskName);
            }
        }).start(); // 启动线程
    }
}

然后,我们需要一个控制器来触发这个异步服务:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AsyncController {

    @Autowired
    private AsyncService asyncService;

    @GetMapping("/startAsyncTask")
    public String startAsyncTask(@RequestParam String taskName) {
        // 调用服务层的方法来启动异步任务
        asyncService.performAsyncTask(taskName);
        // 注意:这里不会等待异步任务完成,而是立即返回
        return "异步任务已启动: " + taskName;
    }
}

在这个例子中,当客户端向/startAsyncTask端点发送请求时,AsyncController会调用AsyncService中的performAsyncTask方法。performAsyncTask方法会创建一个新的线程来执行耗时的异步操作,而不会阻塞主线程(即处理HTTP请求的线程)。因此,客户端会立即收到响应,而不需要等待异步任务完成。

然而,需要注意的是,直接使用Thread类来管理异步任务可能会导致一些问题,比如:

  1. 资源管理:如果不当心地管理线程,可能会导致资源耗尽(如创建过多的线程)。
  2. 异常处理 :在异步线程中抛出的异常可能不会被主线程捕获,除非你使用某种机制(如FutureCompletableFuture)来跟踪异步任务的结果。
  3. 结果同步 :如果你需要等待异步任务的结果,那么你需要自己实现同步机制(如使用CountDownLatchSemaphoreCyclicBarrier等)。

二、使用@Async+@EnableAsync注解(Spring的异步支持-更为常用)

@Async 用于标记一个方法应该异步执行。
@EnableAsync 用于启用Spring的异步方法执行能力,并触发Spring去查找和配置所有被@Async注解的方法。

这两个注解通常一起使用,以在Spring应用中实现异步编程

在 Spring Boot 应用中,@EnableAsync 注解确实可以加在配置类(通常是带有 @Configuration 注解的类)上,也可以加在启动类(通常是带有 @SpringBootApplication 注解的类)上。不过,通常情况下,你只需要在其中一个地方加上这个注解即可,因为 Spring Boot 会扫描并应用启动类和所有配置类上的注解。

下面我将分别提供 @EnableAsync 加在启动类和配置类上的完整案例,包括控制层(Controller)、服务层接口(Service Interface)和服务层实现(Service Implementation)。

1. @EnableAsync 加在启动类上

启动类(Application.java)

java 复制代码
@SpringBootApplication
@EnableAsync
public class AsyncApplication {

    public static void main(String[] args) {
        SpringApplication.run(AsyncApplication.class, args);
    }
}

服务层接口(AsyncService.java)

java 复制代码
public interface AsyncService {
    void executeAsyncTask(String taskName);
}

服务层实现(AsyncServiceImpl.java)

java 复制代码
@Service
public class AsyncServiceImpl implements AsyncService {

    @Async
    @Override
    public void executeAsyncTask(String taskName) {
        System.out.println("Executing " + taskName + " in " + Thread.currentThread().getName());
    }
}

控制层(AsyncController.java)

java 复制代码
@RestController
@RequestMapping("/async")
public class AsyncController {

    @Autowired
    private AsyncService asyncService;

    @GetMapping("/task")
    public ResponseEntity<String> startAsyncTask(@RequestParam String taskName) {
        asyncService.executeAsyncTask(taskName);
        return ResponseEntity.ok("Async task " + taskName + " started.");
    }
}

2. @EnableAsync 加在配置类上

配置类(AsyncConfig.java)

java 复制代码
@Configuration
@EnableAsync
public class AsyncConfig {
    // 这里可以配置自定义的 TaskExecutor,但在这个例子中我们保持简单
}

启动类(Application.java)服务层接口(AsyncService.java)服务层实现(AsyncServiceImpl.java)控制层(AsyncController.java) 与上面的例子完全相同。

如果都加会有影响吗?

如果你同时在启动类和配置类上加了 @EnableAsync,通常不会有负面影响。

Spring Boot 会智能地处理这种情况,确保 @EnableAsync 的效果只被应用一次。

然而,这种做法是不必要的,因为它没有提供额外的功能或灵活性,反而可能让其他开发者感到困惑。

因此,建议只在一个地方(通常是启动类)加上 @EnableAsync 注解,以保持代码的清晰和一致性。

扩展(配置 自定义的 TaskExecutor)

当 需要精细控制异步任务的执行过程时,可以自定义 TaskExecutor

本处创建线程池使用的是ThreadPoolTaskExecutor(该方法比Executors 更好)

还有一个细节的点:ThreadPoolExecutor是Java原生的线程池类,而ThreadPoolTaskExecutor是Spring推出的线程池工具

(一)具体关于"线程池的创建"可以看以下这篇博客
【Thread】线程池的 7 种创建方式及自定义线程池

(二)ThreadPoolTaskExecutor 和 ThreadPoolExecutor 的区别

此处我已经加过@EnableAsync 加在启动类上了

java 复制代码
/**
 * 线程池参数配置,多个线程池实现线程池隔离,
 * @Async注解,默认使用系统自定义线程池,可在项目中设置多个线程池,
 * 在异步调用的时候,指明需要调用的线程池名称,比如:@Async("taskName")
 **/
import org.springframework.context.annotation.Configuration;  
import org.springframework.scheduling.annotation.EnableAsync;  
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;  
import org.springframework.context.annotation.Bean;  
  
@Configuration  
@EnableAsync  
public class AsyncConfig {  
  
    @Bean(name = "taskExecutor")  
    public Executor taskExecutor() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        //设置线程池的核心线程数
        executor.setCorePoolSize(5);  
        //设置线程池的最大线程数
        executor.setMaxPoolSize(10);  
        //线程池的工作队列容量
        executor.setQueueCapacity(25);  
        //线程池中线程的名称前缀
        executor.setThreadNamePrefix("Async-");  
        //设置自定义的拒绝策略
        executor.setRejectedExecutionHandler((r, e) -> {  
    try {  
        // 记录一个警告日志,说明当前保存评价的连接池已满,触发了拒绝策略。  
        log.warn("保存评价连接池任务已满,触发拒绝策略");  
          
        // 尝试将任务重新放入队列中,等待30秒。  
        // 如果在这30秒内队列有空闲空间,任务将被成功放入队列;否则,offer方法将返回false。  
        boolean offer = e.getQueue().offer(r, 30, TimeUnit.SECONDS);  
          
        // 记录日志,显示等待30秒后尝试重新放入队列的结果。  
        log.warn("保存评价连接池任务已满,拒绝接收任务,等待30s重新放入队列结果rs:{}", offer);  
    } catch (InterruptedException ex) {  
        // 如果在等待过程中线程被中断,捕获InterruptedException异常。  
        // 记录一个错误日志,说明在尝试重新放入队列时发生了异常。  
        log.error("【保存评价】连接池任务已满,拒绝接收任务了,再重新放入队列后出现异常", ex);  
          
        // 构建一条警告消息,其中包含线程池的各种信息(如池大小、活动线程数、核心线程数等)。  
        String msg = String.format("保存评价线程池拒绝接收任务! Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)"  
                , e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(),  
                e.getMaximumPoolSize(), e.getLargestPoolSize(), e.getTaskCount(),  
                e.getCompletedTaskCount());  
          
        // 记录包含线程池详细信息的警告日志。  
        log.warn(msg);  
    }  
});
        //初始化线程池,线程池就会处于可以接收任务的状态
        executor.initialize();  
        return executor;  
    }  
}

接下来,在需要异步执行的方法上使用@Async注解。

例如,在SmsService服务类中:

java 复制代码
import org.springframework.scheduling.annotation.Async;  
import org.springframework.stereotype.Service;  
  
@Service  
public class SmsService {  
  
    // 使用@Async注解来标识该方法为异步方法  
    @Async("taskExecutor") // 可以指定使用哪个TaskExecutor,这里使用上面定义的taskExecutor  
    public void sendSms(String message, String phoneNumber) {  
        // 模拟耗时操作  
        System.out.println("执行逻辑二进行中。。。");
        try {  
            Thread.sleep(5000); // 假设发送短信需要5秒  
             // 调用短信接口发送短信  
            System.out.println("发送成功 to " + phoneNumber + " with message: " + message); 
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
    }  
}

SmsController 控制类中:

java 复制代码
@RestController
@RequestMapping("/sms")
public class SmsController {

    @Autowired
    private SmsService smsService ;

    @GetMapping("/task")
    public void doWork(@RequestParam String taskName) {
        // 逻辑一
        System.out.println("执行逻辑一完毕!");
        // 调用异步方法发送短信,不会阻塞主线程  
        smsService.sendSms("Hello", "17027897398");  
        // 逻辑三 
        System.out.println("执行逻辑三完毕!");  
    }
}
相关推荐
东阳马生架构6 分钟前
订单初版—5.售后退货链路中的技术问题说明文档
java
小小寂寞的城11 分钟前
JAVA策略模式demo【设计模式系列】
java·设计模式·策略模式
志辉AI编程26 分钟前
别人还在入门,你已经精通!Claude Code进阶必备14招
后端·ai编程
JAVA学习通28 分钟前
图书管理系统(完结版)
java·开发语言
代码老y33 分钟前
Spring Boot项目中大文件上传的高级实践与性能优化
spring boot·后端·性能优化
abigalexy35 分钟前
深入Java锁机制
java
paishishaba35 分钟前
处理Web请求路径参数
java·开发语言·后端
神仙别闹37 分钟前
基于Java+MySQL实现(Web)可扩展的程序在线评测系统
java·前端·mysql
程序无bug39 分钟前
Java中的8中基本数据类型转换
java·后端
雨落倾城夏未凉43 分钟前
8.Qt文件操作
c++·后端·qt