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

科技事要畅享 2024-08-13 15:45:15
01、背景介绍

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

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

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

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

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

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

02、方案实践

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

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

2.1、异步调用简单示例

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

2.1.1、service 层代码

复制

@Componentpublic 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("更新文章阅读量..."); }}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.2.1.2、controller 层代码

复制

@RestControllerpublic 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; }}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.2.1.3、启动类或配置类添加 EnableAsync 注解

复制

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

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

图片

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

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

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

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

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

2.2.1、配置自定义线程池类

复制

@Configurationpublic 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; }}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.2.2.2、在方法注解上指定线程池

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

复制

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

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

图片

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

2.3、配置全局默认线程池

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

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

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

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

2.3.1、自定义默认异步线程池

复制

@Configurationpublic 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("异步调用,异常捕获---------------------------------"); }; }}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.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

更多资讯,点击

0 阅读:18

科技事要畅享

简介:感谢大家的关注