原文地址:http://blog.jboost.cn/2019/07/22/springboot-async.html
在業務開發中,有時候會遇到一些非核心的附加功能,比如簡訊或微信模板訊息通知,或者一些耗時比較久,但主流程不需要立即獲得其結果反饋的操作,比如儲存圖片、同步資料到其它合作方等等。如果將這些操作都置於主流程中同步處理,勢必會對核心流程的效能造成影響,甚至由於第三方服務的問題導致自身服務不可用。這時候就應該將這些操作非同步化,以提高主流程的效能,並與第三方解耦,提高主流程的可用性。
在Spring Boot中,或者說在Spring中,我們實現非同步處理一般有以下幾種方式:
1. 通過 @EnableAsync 與 @Asyc 註解結合實現
2. 通過非同步事件實現
3. 通過訊息佇列實現
1. 基於註解實現
我們以前在Spring中提供非同步支援一般是在配置檔案 applicationContext.xml 中新增類似如下配置
<task:annotation-driven executor="executor" /> <task:executor id="executor" pool-size="10-200" queue-capacity="2000"/>
Spring的 @EnableAsync 註解的功能與<task:annotation-driven/>
類似,將其新增於一個 @Configuration 配置類上,可對Spring應用的上下文開啟非同步方法支援。 @Async 註解可以標註在方法或類上,表示某個方法或某個類裡的所有方法需要通過非同步方式來呼叫。
我們以一個demo來示例具體用法,demo地址:https://github.com/ronwxy/springboot-demos/tree/master/springboot-async
1. 新增 @EnableAsync 註解
在一個 @Configuration 配置類上新增 @EnableAysnc 註解,我們一般可以新增到啟動類上,如
@SpringBootApplication @EnableAsync public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
2. 配置相關的非同步執行執行緒池
@Configuration public class AsyncConfig implements AsyncConfigurer { @Value("${async.corePoolSize:10}") private int corePoolSize; @Value("${async.maxPoolSize:200}") private int maxPoolSize; @Value("${async.queueCapacity:2000}") private int queueCapacity; @Value("${async.keepAlive:5}") private int keepAlive; public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setKeepAliveSeconds(keepAlive); executor.setThreadNamePrefix("async-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setDaemon(false); //以使用者執行緒模式執行 executor.initialize(); return executor; } public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new MyAsyncUncaughtExceptionHandler(); } public static class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { public void handleUncaughtException(Throwable throwable, Method method, Object... objects) { System.out.println("catch exception when invoke " + method.getName()); throwable.printStackTrace(); } } }
可通過配置類的方式對非同步執行緒池進行配置,並提供非同步執行時出現異常的處理方法,如
這裡我們通過實現 AsyncConfigurer 介面提供了一個非同步執行執行緒池物件,各引數的說明可以參考【執行緒池的基本原理,看完就懂了】,裡面有很詳細的介紹。且通過實現 AsyncUncaughtExceptionHandler 介面提供了一個非同步執行過程中未捕獲異常的處理類。
3. 定義非同步方法
非同步方法的定義只需要在類(類上註解表示該類的所有方法都非同步執行)或方法上新增 @Async 註解即可,如
@Service public class AsyncService { @Async public void asyncMethod(){ System.out.println("2. running in thread: " + Thread.currentThread().getName()); } @Async public void asyncMethodWithException() { throw new RuntimeException("exception in async method"); } }
4. 測試
我們可以通過如下測試類來對非同步方法進行測試
@RunWith(SpringRunner.class) @SpringBootTest public class AnnotationBasedAsyncTest { @Autowired private AsyncService asyncService; @Test public void testAsync() throws InterruptedException { System.out.println("1. running in thread: " + Thread.currentThread().getName()); asyncService.asyncMethod(); Thread.sleep(3); } @Test public void testAysncWithException() throws InterruptedException { System.out.println("1. running in thread: " + Thread.currentThread().getName()); asyncService.asyncMethodWithException(); Thread.sleep(3); } }
因為非同步方法在一個新的執行緒中執行,可能在主執行緒執行完後還沒來得及處理,所以通過sleep來等待它執行完成。具體執行結果讀者可自行嘗試執行,這裡就不貼圖了。
2. 基於事件實現
第二種方式是通過Spring框架的事件監聽機制實現,但Spring的事件監聽預設是同步執行的,所以實際上還是需要藉助 @EnableAsync 與 @Async 來實現非同步。
1. 新增 @EnableAsync 註解
與上同,可新增到啟動類上。
2. 自定義事件類
通過繼承 ApplicationEvent 來自定義一個事件
public class MyEvent extends ApplicationEvent { private String arg; public MyEvent(Object source, String arg) { super(source); this.arg = arg; } //getter/setter }
3. 定義事件處理類
支援兩種形式,一是通過實現 ApplicationListener 介面,如下
@Component @Async public class MyEventHandler implements ApplicationListener<MyEvent> { public void onApplicationEvent(MyEvent event) { System.out.println("2. running in thread: " + Thread.currentThread().getName()); System.out.println("2. arg value: " + event.getArg()); } }
二是通過 @EventListener 註解,如下
@Component public class MyEventHandler2 { @EventListener @Async public void handle(MyEvent event){ System.out.println("3. running in thread: " + Thread.currentThread().getName()); System.out.println("3. arg value: " + event.getArg()); } }
注意兩者都需要新增 @Async 註解,否則預設是同步方式執行。
4. 定義事件傳送類
可以通過實現 ApplicationEventPublisherAware 介面來使用 ApplicationEventPublisher 的 publishEvent()方法傳送事件,
@Component public class MyEventPublisher implements ApplicationEventPublisherAware { protected ApplicationEventPublisher applicationEventPublisher; public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } public void publishEvent(ApplicationEvent event){ this.applicationEventPublisher.publishEvent(event); } }
5. 測試
可以通過如下測試類來進行測試,
@RunWith(SpringRunner.class) @SpringBootTest public class EventBasedAsyncTest { @Autowired private MyEventPublisher myEventPublisher; @Test public void testAsync() throws InterruptedException { System.out.println("1. running in thread: " + Thread.currentThread().getName()); myEventPublisher.publishEvent(new MyEvent(this,"testing event based async")); Thread.sleep(3); } }
執行後發現兩個事件處理類都執行了,因為兩者都監聽了同一個事件 MyEvent 。
3. 基於訊息佇列實現
以上兩種方式都是基於伺服器本機執行,如果服務程式出現異常退出,可能導致非同步執行中斷。如果需要保證任務執行的可靠性,可以藉助訊息佇列的持久化與重試機制。阿里雲上的訊息佇列服務提供了幾種型別的訊息支援,如順序訊息、定時/延時訊息、事務訊息等(詳情可參考:https://help.aliyun.com/document_detail/29532.html?spm=5176.234368.1278132.btn4.6f43db25Rn8oey ),如果專案是基於阿里雲部署的,可以考慮使用其中一類訊息服務來實現業務需求。
4. 總結
本文對spring boot下非同步處理的幾種方法進行了介紹,如果對任務執行的可靠性要求不高,則推薦使用第一種方式,如果可靠性要求較高,則推薦使用自建訊息佇列或雲訊息佇列服務的方式。
本文demo原始碼地址:https://github.com/ronwxy/springboot-demos/tree/master/springboot-async/src/main/java/cn/jboost/async
我的個人部落格地址:http://blog.jboost.cn
我的微信公眾號:jboost-ksxy (一個不只有技術乾貨的公眾號,歡迎關注,及時獲取更新內容)
———————————————————————————————————————————————————————————————