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("执行逻辑三完毕!");  
    }
}
相关推荐
向前看-2 小时前
验证码机制
前端·后端
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石4 小时前
12/21java基础
java
李小白664 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp4 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea