Springboot非同步事件配置和使用

冰雪女娲發表於2024-10-23

Spring中提供了完整的事件處理機制,本身底層內建實現了一些事件和監聽,同時支援開發者擴充套件自己的事件和監聽實現。

一般這種基於事件的實現在專案實際開發中我們主要用來解耦,和做非同步處理(預設是同步),提供應用的響應速度。

核心架構

先簡要看一下,在Spring中要實現自定義事件監聽需要涉及哪些介面類,這裡忽略非同步的引用、註解的實現,後面會說到。

image

基本實現步驟

  1. 自定義事件:一般繼承自ApplicationEvent即可,注意裡面要去定義和實現自己的事件方法,也就是具體這個事件要做什麼事,一般就在事件類、或者基於事件類去實現即可。
  2. 事件釋出:業務程式碼中注入ApplicationEventPublisher類,然後再具體業務方法中呼叫publishEvent方法,傳入上面自定義的事件,以及自定義的必要引數等資訊
  3. 實現事件監聽:有了事件、也釋出了,那必須有對應的監聽來呼叫具體的事件,一般實現ApplicationListener泛型傳入自己的事件型別即可

注意事項

  1. 異常和事物:預設情況下事件的釋出、監聽處理都是和當前業務執行緒繫結到一起的,也就是在同一個執行緒中操作事件任務。因此無論是事件釋出時導致異常,或者是具體事件任務實現的方法異常,都會導致當前業務異常;相應的如果當前業務有事物,那麼異常了也會回滾。
  2. 事件型別:首先一定要自定義自己的事件,其次在監聽的時候也是監聽自己的事件,而不是監聽基類或者介面然後去判斷,這樣反而失去了基於事件監聽程式設計靈活性,同時也違法開閉原則,並不利於後期擴充套件。具體事件中可以定義其他一些額外的引數,這樣方便在具體方法中傳參使用
  3. 事件順序:一次可以釋出多個事件,無論是同一個還是不同的,執行順序預設也是按照發布順序。

場景應用

這裡以訂單完成和推送給平臺訂單相關資料為業務模型來舉例說明。Spring4.2之後提供了註解來實現事件監聽,非常的方便,這裡我們使用註解的方式實現監聽即可。

  • 縮略的業務類:包含事件的釋出
@Resource
private ApplicationEventPublisher publisher;

public void completeTrade(TradeOrder trade){
  tradeMapper.modifyStatus(trade);
  publisher.publishEvent(new TradeStatusEvent(this,new TradeStatusEvent.Params(trade,"完成訂單")));
}
  • 具體事件的定義:繼承自ApplicationEvent
public class TradeStatusEvent extends ApplicationEvent {
    private static final Logger logger = LoggerFactory.getLogger(TradeStatusEvent.class);

    private Params params;
    
    public Params getParams(){
      return this.params;
    }
    
    public TradeStatusEvent(Object source,Params param) {
        super(source);
        this.param = param;
    }
    
    public void send(){
      try{
        HttpUtils.send("xx.oo", PlatformBean.Builder().note(this.params.note)..build());
      } catch(Exception e){
        logger.error("TradeStatusEvent處理異常:",e);
      } 
    }
    
    
    public static class Params {
        private TradeOrder trade;
        private String note;
        //get、set 定義其他引數等
    }
}
  • 監聽實現:使用註解,注意這裡我使用了 事務監聽註解 ,按照具體業務場景可以選擇具體的註解,比如最常用的@EventListener。因為我這裡的訴求是當前事物提交完成之後再去推送訊息,而且實際情況是啟用了非同步監聽來實現,同時有的人在監聽的方法中可能還執行了回查,也就是去查詢業務中提交的資料,那如果這裡不標記為事物提交之後執行,在非同步情況下無法獲取到資料
@Component
public class TradeStatusEventListener {

    @TransactionalEventListener(phase= TransactionPhase.AFTER_COMMIT, fallbackExecution=true)
    void handlerAfterComplete(TradeStatusEvent event) {
        event.send();
    }
}

非同步實現

所謂非同步實現,一般是指非同步監聽,將主體業務邏輯和訊息監聽任務放到不同的執行緒去執行,提高業務的響應速度。

Springboot中我們有多個辦法來實現非同步監聽執行,最簡單、最直接的就和非同步方法實現一模一樣,只需在監聽方法上加上@Async註解(前提是啟用了非同步執行)

  • 第一種辦法:Configuration配置類中加上註解@EnableAsync,啟用Spring的非同步方法執行能力。然後在監聽方法上加上@Async註解,標明此方法是非同步執行。Over就這樣就行了【我們沒有配置非同步執行緒對不對?那是會直接new Thread()來執行非同步任務嗎,當然不是,而是Spring預設提供並初始化了一個專門用來執行非同步任務的執行緒池ThreadPoolTaskExecutor,會接管所有的非同步任務在同一個執行緒池中執行。也支援定製化處理,後續我們會說到】
@Configuration
@EnableAsync
public class AppConfig{}
//````
@Component
public class TradeStatusEventListener {

    @Async
    @TransactionalEventListener(phase= TransactionPhase.AFTER_COMMIT, fallbackExecution=true)
    void handlerAfterComplete(TradeStatusEvent event) {
        event.send();
    }
}
  • 第二種辦法:如果說不想全域性開啟非同步,只是想給事件監聽的程式碼實現非同步任務呢?那最簡單就是直接在監聽哪裡new Thread().start(),不受控、不優雅,但是業務場景簡單,訪問量小的情況下也不是不可以。那要規範一點呢,就是自己建立一個執行緒池,比如ExecutorService executorService = Executors.newCachedThreadPool();然後在event.send哪裡使用executorService.execute(..)執行即可。
  • 第三種辦法:優雅點實現,建立SimpleApplicationEventMulticaster的Bean,然後建立一個執行緒池給塞進去,注意需要把自定義實現注入到Spring容器中。其他程式碼不用做任何修改,就像同步邏輯一樣,在事件釋出的時候廣播會使用multicastEvent呼叫taskExecutor獲取一個執行緒去執行監聽任務
@Configuration
public class AppConfig{
  @Bean
  public SimpleApplicationEventMulticaster simpleApplicationEventMulticaster(){
        SimpleApplicationEventMulticaster mu = new SimpleApplicationEventMulticaster();
        //這裡我使用spring提供的任務構造器建立了一個立即執行的有界佇列任務執行緒池
        Executor taskExecutor = new TaskExecutorBuilder().corePoolSize(8).maxPoolSize(200).queueCapacity(20).threadNamePrefix("trade-send-").build();
        mu.setTaskExecutor(taskExecutor);
        //設定異常處理
        mu.setErrorHandler((t)->{
            //logger.error("==========呼叫平臺傳送訊息方法失敗,",t);
        });
    return mu;
  }
}

框架原理

  • 為什麼非同步監聽只需要@EnableAsync、以及在方法上加上@Async就可以了呢?
    • 當我們使用Springboot,引入starter時會自動引入spring-boot-autoconfigure,此包裡面實現了很多自動配置的功能(約定大於配置)名字都是xxxAutoConfiguration,比如我們這裡要說的就是TaskExecutionAutoConfiguration,容器啟動的時候就會載入和建立預設的任務執行緒池,可以透過spring.task.execution開頭屬性來配置。需要注意的是,無論是否加入@EnableAsync註解TaskExecutionAutoConfiguration都會初始化一個預設的執行緒池,因為這個是全域性的。
      image

    • @EnableAsync的作用是在容器啟動的時候,告訴Spring我可要支援非同步處理任務了,你看著辦。Spring所好的朋友,我給你準備了一個專門搞事的攔截器。
      image

    • 當我們加入了註解,Spring會將按照配置將準備工作全部做完,從而做到開箱即用,直接一步到位。

總結

  • Spring事件模型的四個核心:事件源也就是業務方、事件、廣播器、監聽器
  • 事件機制支援同步、非同步,按需調整和使用。使用非同步監聽時,推薦使用執行緒池管理執行緒,高效、穩定而且易於維護。
  • 使用Springboot時透過註解的方式監聽、啟用非同步盡享絲滑。實際原理核心就是觀察者模式。

相關文章