Spring Boot 实现方法异步调用的正确姿势!
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