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