查看原文
其他

Spring Boot 实现方法异步调用的正确姿势!

志哥聊技术 潘志的研发笔记
2024-08-30

01、背景介绍

在实际的项目开发过程中,通常会碰到某个方法内各个逻辑并非紧密相连的业务。比如查询文章详情后更新文章阅读量,其实对于用户来说,最关心的是能快速获取文章,至于更新文章阅读量,用户可能并不关心。

因此,对于这类逻辑并非紧密相连的业务,可以将逻辑进行拆分,让用户无需等待更新文章阅读量,查询时直接返回文章信息,缩短同步请求的耗时,进一步提升了用户体验。

要实现这种效果,很多同学可能立刻想到,采用异步线程来更新文章阅读量。

是的,这个思路没错,在 Java 项目中,我们可以开启一个线程来实现方法异步执行。

如果是在 Spring Boot 工程中,该如何优雅的实现方法异步调用呢?

今天带着这个问题,我们一起来学习一下如何在 Spring Boot 中实现方法的异步调用。

02、方案实践

实际上,从 Spring 3.0 之后,在 Spring Framework 的 Spring Task 模块中,提供了@Async注解,将其添加在方法上,就可以自动实现该方法的异步调用效果。

不过有一个前提,需要在启动类或配置类加上@EnableAsync注解,以便使异步调用@Async注解生效。

2.1、异步调用简单示例

以用户查询文章详情后,异步更新文章阅读量为例,我们来看一个简单的应用示例。

2.1.1、service 层代码
@Component
public class ArticleService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ArticleService.class);

    /**
     * 查询文章信息
     * @return
     */

    public String queryArticle(){
        LOGGER.info("查询文章信息...");
        return "hello world";
    }

    /**
     * 更新文章阅读量
     * @return
     */

    @Async
    public void updateCount(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LOGGER.info("更新文章阅读量...");
    }
}
2.1.2、controller 层代码
@RestController
public class UserController {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private ArticleService articleService;

    @RequestMapping("/query")
    public String query(){
        LOGGER.info("用户请求开始");
        // 查询文章
        String result = articleService.queryArticle();
        // 更新文章阅读量
        articleService.updateCount();
        LOGGER.info("用户请求结束");
        return result;
    }
}
2.1.3、启动类或配置类添加 EnableAsync 注解
@EnableAsync
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
2.1.4、服务测试

最后启动服务,在浏览器中向query接口方法发起请求,输出结果如下:

从日志上可以清晰的看到,当发起查询文章请求的时候,结果立刻响应给了客户端;其次,更新文章阅读量的方法采用的是task-1线程来执行,并没有阻塞主线程的执行,异步调用效果明显。

2.2、自定义线程池执行异步方法

@Async注解标注的方法,默认采用SimpleAsyncTaskExecutor线程池来执行。这个线程池有一个特点就是,每来一个请求任务就会创建一个线程去执行,如果系统不断的创建线程,最终可能导致 CPU 和内存占用过高,引发OutOfMemoryError错误。

实际上,SimpleAsyncTaskExecutor并不是严格意义上的线程池,因为它达不到线程复用的效果。因此,在实际开发中,建议自定义线程池来执行异步方法。

实现步骤也很简单,首先,注入自定义线程池对象到 Spring Bean 中;然后,在@Async注解中指定线程池,即可实现指定线程池来异步执行任务。

2.2.1、配置自定义线程池类
@Configuration
public class AsyncConfig {

    @Bean("customExecutor")
    public ThreadPoolTaskExecutor asyncOperationExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(3);
        // 设置最大线程数
        executor.setMaxPoolSize(5);
        // 设置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(30);
        // 设置线程名前缀+分组名称
        executor.setThreadNamePrefix("customThread-");
        executor.setThreadGroupName("customThreadGroup");
        // 所有任务结束后关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 初始化
        executor.initialize();
        return executor;
    }
}
2.2.2、在方法注解上指定线程池

比如,将更新文章阅读量的方法,改成customExecutor线程池来执行,在@Async注解上指定线程池即可。

@Async("customExecutor")
public void updateCount(){
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    LOGGER.info("更新文章阅读量...");
}
2.2.3、服务测试

最后启动服务,重新发起请求,输出结果如下:

从日志上可以清晰的看到,更新方法采用了customThread-1线程来异步执行任务。

2.3、配置全局默认线程池

从上文中我们得知,被@Async注解标注的方法,默认采用SimpleAsyncTaskExecutor线程池来执行。

某些场景下,如果希望系统统一采用自定义配置线程池来执行任务,但是又不想在被@Async注解的方法上一个一个的去指定线程池,如何处理呢?

此时可以重写AsyncConfigurer接口的getAsyncExecutor()方法,配置默认线程池。

实现也很简单,示例如下!

2.3.1、自定义默认异步线程池
@Configuration
public class AsyncConfiguration implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(3);
        // 设置最大线程数
        executor.setMaxPoolSize(5);
        // 设置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(30);
        // 设置线程名前缀+分组名称
        executor.setThreadNamePrefix("asyncThread-");
        executor.setThreadGroupName("asyncThreadGroup");
        // 所有任务结束后关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 初始化
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, obj) ->{
            System.out.println("异步调用,异常捕获---------------------------------");
            System.out.println("Exception message - " + throwable.getMessage());
            System.out.println("Method name - " + method.getName());
            for (Object param : obj) {
                System.out.println("Parameter value - " + param);
            }
            System.out.println("异步调用,异常捕获---------------------------------");
        };
    }
}
2.3.2、服务测试

@Async注解中指定的线程池,最后启动服务,重新发起请求,输出结果如下:

从日志上可以清晰的看到,更新方法采用了asyncThread-1线程来异步执行任务。

03、遇到的一些坑

在使用@Async注解的时候,可能会失效,总结下来主要有以下几个场景。

  • 场景一:异步方法使用static修饰,此时不会生效
  • 场景二:调用的异步方法,在同一个类中,此时不会生效。因为 Spring 在启动扫描时会为其创建一个代理类,而同类调用时,还是调用本身的代理类的,所以还是同步调用
  • 场景三:异步类没有使用@Component@Service等注解,导致 spring 无法扫描到异步类,此时不会生效
  • 场景四:采用SpringBoot框架开发时,没有在启动类上添加@EnableAsync注解,此时不会生效

其次,关于事务机制的一些问题,直接在@Async方法上再标注@Transactional是会失效的,此时可以在方法内采用编程式事务方式来提交数据。但是,在@Async方法调用其它类的方法上标注的@Transactional注解有效。

04、小结

最后总结一下,在 Spring Boot 工程中,如果想要实现方法异步执行的效果,只需要两步即可完成。

首先,在启动类或者配置类上添加@EnableAsync,表达开启异步执行功能;然后,在需要异步执行的方法上,添加@Async注解,使方法实现异步调用的目标。

如果希望采用自定义线程池来执行,可以配置一个线程池对象并注入到 bean 工厂,最后在异步注解中指定即可;也可以全局配置默认线程池。

示例代码地址:

https://gitee.com/pzblogs/spring-boot-example-demo

写到最后

最后感谢各位的阅读,原创不易,如果觉得文章写的不错,欢迎大家转发,点击【在看】让更多的人看到,谢谢大家的支持!

推荐阅读


继续滑动看下一个
潘志的研发笔记
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存