Spring Boot從入門到實戰(十):非同步處理

【空山新雨】發表於2019-07-23

原文地址: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 (一個不只有技術乾貨的公眾號,歡迎關注,及時獲取更新內容)
———————————————————————————————————————————————————————————————
微信公眾號

相關文章