Spring Boot 實(shí)現(xiàn)方法異步調(diào)用的正確姿勢(shì)!
01、背景介紹
在實(shí)際的項(xiàng)目開發(fā)過程中,通常會(huì)碰到某個(gè)方法內(nèi)各個(gè)邏輯并非緊密相連的業(yè)務(wù)。比如查詢文章詳情后更新文章閱讀量,其實(shí)對(duì)于用戶來說,最關(guān)心的是能快速獲取文章,至于更新文章閱讀量,用戶可能并不關(guān)心。
因此,對(duì)于這類邏輯并非緊密相連的業(yè)務(wù),可以將邏輯進(jìn)行拆分,讓用戶無需等待更新文章閱讀量,查詢時(shí)直接返回文章信息,縮短同步請(qǐng)求的耗時(shí),進(jìn)一步提升了用戶體驗(yàn)。
要實(shí)現(xiàn)這種效果,很多同學(xué)可能立刻想到,采用異步線程來更新文章閱讀量。
是的,這個(gè)思路沒錯(cuò),在 Java 項(xiàng)目中,我們可以開啟一個(gè)線程來實(shí)現(xiàn)方法異步執(zhí)行。
如果是在 Spring Boot 工程中,該如何優(yōu)雅的實(shí)現(xiàn)方法異步調(diào)用呢?
今天帶著這個(gè)問題,我們一起來學(xué)習(xí)一下如何在 Spring Boot 中實(shí)現(xiàn)方法的異步調(diào)用。
02、方案實(shí)踐
實(shí)際上,從 Spring 3.0 之后,在 Spring Framework 的 Spring Task 模塊中,提供了@Async注解,將其添加在方法上,就可以自動(dòng)實(shí)現(xiàn)該方法的異步調(diào)用效果。
不過有一個(gè)前提,需要在啟動(dòng)類或配置類加上@EnableAsync注解,以便使異步調(diào)用@Async注解生效。
2.1、異步調(diào)用簡(jiǎn)單示例
以用戶查詢文章詳情后,異步更新文章閱讀量為例,我們來看一個(gè)簡(jiǎn)單的應(yīng)用示例。
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("用戶請(qǐng)求開始");
// 查詢文章
String result = articleService.queryArticle();
// 更新文章閱讀量
articleService.updateCount();
LOGGER.info("用戶請(qǐng)求結(jié)束");
return result;
}
}
2.1.3、啟動(dòng)類或配置類添加 EnableAsync 注解
@EnableAsync
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.1.4、服務(wù)測(cè)試
最后啟動(dòng)服務(wù),在瀏覽器中向query接口方法發(fā)起請(qǐng)求,輸出結(jié)果如下:
圖片
從日志上可以清晰的看到,當(dāng)發(fā)起查詢文章請(qǐng)求的時(shí)候,結(jié)果立刻響應(yīng)給了客戶端;其次,更新文章閱讀量的方法采用的是task-1線程來執(zhí)行,并沒有阻塞主線程的執(zhí)行,異步調(diào)用效果明顯。
2.2、自定義線程池執(zhí)行異步方法
被@Async注解標(biāo)注的方法,默認(rèn)采用SimpleAsyncTaskExecutor線程池來執(zhí)行。這個(gè)線程池有一個(gè)特點(diǎn)就是,每來一個(gè)請(qǐng)求任務(wù)就會(huì)創(chuàng)建一個(gè)線程去執(zhí)行,如果系統(tǒng)不斷的創(chuàng)建線程,最終可能導(dǎo)致 CPU 和內(nèi)存占用過高,引發(fā)OutOfMemoryError錯(cuò)誤。
實(shí)際上,SimpleAsyncTaskExecutor并不是嚴(yán)格意義上的線程池,因?yàn)樗_(dá)不到線程復(fù)用的效果。因此,在實(shí)際開發(fā)中,建議自定義線程池來執(zhí)行異步方法。
實(shí)現(xiàn)步驟也很簡(jiǎn)單,首先,注入自定義線程池對(duì)象到 Spring Bean 中;然后,在@Async注解中指定線程池,即可實(shí)現(xiàn)指定線程池來異步執(zhí)行任務(wù)。
2.2.1、配置自定義線程池類
@Configuration
public class AsyncConfig {
@Bean("customExecutor")
public ThreadPoolTaskExecutor asyncOperationExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 設(shè)置核心線程數(shù)
executor.setCorePoolSize(3);
// 設(shè)置最大線程數(shù)
executor.setMaxPoolSize(5);
// 設(shè)置隊(duì)列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 設(shè)置線程活躍時(shí)間(秒)
executor.setKeepAliveSeconds(30);
// 設(shè)置線程名前綴+分組名稱
executor.setThreadNamePrefix("customThread-");
executor.setThreadGroupName("customThreadGroup");
// 所有任務(wù)結(jié)束后關(guān)閉線程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 初始化
executor.initialize();
return executor;
}
}
2.2.2、在方法注解上指定線程池
比如,將更新文章閱讀量的方法,改成customExecutor線程池來執(zhí)行,在@Async注解上指定線程池即可。
@Async("customExecutor")
public void updateCount(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOGGER.info("更新文章閱讀量...");
}
2.2.3、服務(wù)測(cè)試
最后啟動(dòng)服務(wù),重新發(fā)起請(qǐng)求,輸出結(jié)果如下:
圖片
從日志上可以清晰的看到,更新方法采用了customThread-1線程來異步執(zhí)行任務(wù)。
2.3、配置全局默認(rèn)線程池
從上文中我們得知,被@Async注解標(biāo)注的方法,默認(rèn)采用SimpleAsyncTaskExecutor線程池來執(zhí)行。
某些場(chǎng)景下,如果希望系統(tǒng)統(tǒng)一采用自定義配置線程池來執(zhí)行任務(wù),但是又不想在被@Async注解的方法上一個(gè)一個(gè)的去指定線程池,如何處理呢?
此時(shí)可以重寫AsyncConfigurer接口的getAsyncExecutor()方法,配置默認(rèn)線程池。
實(shí)現(xiàn)也很簡(jiǎn)單,示例如下!
2.3.1、自定義默認(rèn)異步線程池
@Configuration
public class AsyncConfiguration implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 設(shè)置核心線程數(shù)
executor.setCorePoolSize(3);
// 設(shè)置最大線程數(shù)
executor.setMaxPoolSize(5);
// 設(shè)置隊(duì)列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 設(shè)置線程活躍時(shí)間(秒)
executor.setKeepAliveSeconds(30);
// 設(shè)置線程名前綴+分組名稱
executor.setThreadNamePrefix("asyncThread-");
executor.setThreadGroupName("asyncThreadGroup");
// 所有任務(wù)結(jié)束后關(guān)閉線程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 初始化
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, obj) ->{
System.out.println("異步調(diào)用,異常捕獲---------------------------------");
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("異步調(diào)用,異常捕獲---------------------------------");
};
}
}
2.3.2、服務(wù)測(cè)試
將@Async注解中指定的線程池,最后啟動(dòng)服務(wù),重新發(fā)起請(qǐng)求,輸出結(jié)果如下:
從日志上可以清晰的看到,更新方法采用了asyncThread-1線程來異步執(zhí)行任務(wù)。
03、遇到的一些坑
在使用@Async注解的時(shí)候,可能會(huì)失效,總結(jié)下來主要有以下幾個(gè)場(chǎng)景。
- 場(chǎng)景一:異步方法使用static修飾,此時(shí)不會(huì)生效
- 場(chǎng)景二:調(diào)用的異步方法,在同一個(gè)類中,此時(shí)不會(huì)生效。因?yàn)?Spring 在啟動(dòng)掃描時(shí)會(huì)為其創(chuàng)建一個(gè)代理類,而同類調(diào)用時(shí),還是調(diào)用本身的代理類的,所以還是同步調(diào)用
- 場(chǎng)景三:異步類沒有使用@Component、@Service等注解,導(dǎo)致 spring 無法掃描到異步類,此時(shí)不會(huì)生效
- 場(chǎng)景四:采用SpringBoot框架開發(fā)時(shí),沒有在啟動(dòng)類上添加@EnableAsync注解,此時(shí)不會(huì)生效
其次,關(guān)于事務(wù)機(jī)制的一些問題,直接在@Async方法上再標(biāo)注@Transactional是會(huì)失效的,此時(shí)可以在方法內(nèi)采用編程式事務(wù)方式來提交數(shù)據(jù)。但是,在@Async方法調(diào)用其它類的方法上標(biāo)注的@Transactional注解有效。
04、小結(jié)
最后總結(jié)一下,在 Spring Boot 工程中,如果想要實(shí)現(xiàn)方法異步執(zhí)行的效果,只需要兩步即可完成。
首先,在啟動(dòng)類或者配置類上添加@EnableAsync,表達(dá)開啟異步執(zhí)行功能;然后,在需要異步執(zhí)行的方法上,添加@Async注解,使方法實(shí)現(xiàn)異步調(diào)用的目標(biāo)。
如果希望采用自定義線程池來執(zhí)行,可以配置一個(gè)線程池對(duì)象并注入到 bean 工廠,最后在異步注解中指定即可;也可以全局配置默認(rèn)線程池。
示例代碼地址:
https://gitee.com/pzblogs/spring-boot-example-demo