文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-11-29 19:48

关注

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

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

是的,这个思路没错,在 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);

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

    
    @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注解的时候,可能会失效,总结下来主要有以下几个场景。

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

04、小结

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

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

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

示例代码地址:

https://gitee.com/pzblogs/spring-boot-example-demo
来源:潘志的研发笔记内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯