SpringBoot | 第三十二章:事件的發布和監聽


前言

今天去官網查看spring boot資料時,在特性中看見了系統的事件及監聽章節。想想,spring的事件應該是在3.x版本就發布的功能了,並越來越完善,其為beanbean之間的消息通信提供了支持。比如,我們可以在用戶注冊成功后,發送一份注冊成功的郵件至用戶郵箱或者發送短信。使用事件其實最大作用,應該還是為了業務解耦,畢竟用戶注冊成功后,注冊服務的事情就做完了,只需要發布一個用戶注冊成功的事件,讓其他監聽了此事件的業務系統去做剩下的事件就好了。對於事件發布者而言,不需要關心誰監聽了該事件,以此來解耦業務。今天,我們就來講講spring boot中事件的使用和發布。當然了,也可以使用像guavaeventbus或者異步框架Reactor來處理此類業務需求的。本文僅僅談論ApplicationEvent以及Listener的使用。

一點知識

示例前,我們來了解下相關知識點。

Java的事件機制

java中的事件機制一般包括3個部分:EventObjectEventListenerSource

EventObject

java.util.EventObject是事件狀態對象的基類,它封裝了事件源對象以及和事件相關的信息。所有java的事件類都需要繼承該類。

EventListener

java.util.EventListener是一個標記接口,就是說該接口內是沒有任何方法的。所有事件監聽器都需要實現該接口。事件監聽器注冊在事件源上,當事件源的屬性或狀態改變的時候,調用相應監聽器內的回調方法。

Source

事件源不需要實現或繼承任何接口或類,它是事件最初發生的地方。因為事件源需要注冊事件監聽器,所以事件源內需要有相應的盛放事件監聽器的容器。

java的事件機制是一個觀察者模式。大家可以根據這個模式,自己實現一個。可以看看這篇博文:《java事件機制》一個很簡單的實例。

Spring的事件

ApplicationEvent以及ListenerSpring為我們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是為了系統業務邏輯之間的解耦,提高可擴展性以及可維護性。

  • ApplicationEvent就是Spring的事件接口
  • ApplicationListener就是Spring的事件監聽器接口,所有的監聽器都實現該接口
  • ApplicationEventPublisherSpring的事件發布接口,ApplicationContext實現了該接口
  • ApplicationEventMulticaster就是Spring事件機制中的事件廣播器,默認實現SimpleApplicationEventMulticaster

Spring中通常是ApplicationContext本身擔任監聽器注冊表的角色,在其子類AbstractApplicationContext中就聚合了事件廣播器ApplicationEventMulticaster和事件監聽器ApplicationListnener,並且提供注冊監聽器的addApplicationListnener方法。

其執行的流程大致為:

當一個事件源產生事件時,它通過事件發布器ApplicationEventPublisher發布事件,然后事件廣播器ApplicationEventMulticaster會去事件注冊表ApplicationContext中找到事件監聽器ApplicationListnener,並且逐個執行監聽器的onApplicationEvent方法,從而完成事件監聽器的邏輯。

Spring中,使用注冊監聽接口,除了繼承ApplicationListener接口外,還可以使用注解@EventListener來監聽一個事件,同時該注解還支持SpEL表達式,來觸發監聽的條件,比如只接受編碼為001的事件,從而實現一些個性化操作。下文示例中會簡單舉例下。

簡單來說,在Java中,通過java.util. EventObject來描述事件,通過java.util. EventListener來描述事件監聽器,在眾多的框架和組件中,建立一套事件機制通常是基於這兩個接口來進行擴展。

SpringBoot的默認啟動事件

SpringBoot1.5.x中,提供了幾種事件,供我們在開發過程中進行更加便捷的擴展及差異化操作。

  • ApplicationStartingEvent:springboot啟動開始的時候執行的事件

  • ApplicationEnvironmentPreparedEventspring boot對應Enviroment已經准備完畢,但此時上下文context還沒有創建。在該監聽中獲取到ConfigurableEnvironment后可以對配置信息做操作,例如:修改默認的配置信息,增加額外的配置信息等等。

  • ApplicationPreparedEventspring boot上下文context創建完成,但此時spring中的bean是沒有完全加載完成的。在獲取完上下文后,可以將上下文傳遞出去做一些額外的操作。值得注意的是:在該監聽器中是無法獲取自定義bean並進行操作的。

  • ApplicationReadyEventspringboot加載完成時候執行的事件。

  • ApplicationFailedEventspring boot啟動異常時執行事件。

1.5.15版本

從官網文檔中,我們可以知道,由於一些事件實在上下文為加載完觸發的,所以無法使用注冊bean的方式來聲明,文檔中可以看出,可以通過SpringApplication.addListeners(…​)或者SpringApplicationBuilder.listeners(…​)來添加,或者添加META-INF/spring.factories文件z中添加監聽類也是可以的,這樣會自動加載。

org.springframework.context.ApplicationListener=com.example.project.MyListener

啟動類中添加:

@SpringBootApplication
public class Application {

    public static void main(String[] args){
        SpringApplication app =new SpringApplication(Application.class);
        app.addListeners(new MyApplicationStartingEventListener());//加入自定義的監聽類
        app.run(args);
    }
}

所以在需要的時候,可以通過適當的監聽以上事件,來完成一些業務操作。

自定義事件發布和監聽

通過以上的介紹,我們來定義一個自定義事件的發布和監聽。

0.加入POM依賴,這里為了演示加入了web依賴。事件相關類都在spring-context包下。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

1.自定義事件源和實體。

MessageEntity.java

/**
 * 消息實體類
 * @author oKong
 *
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageEntity {

    String message;
    
    String code;
}

CustomEvent.java

/**
 * 編寫事件源
 * @author oKong
 *
 */
@SuppressWarnings("serial")
public class CustomEvent extends ApplicationEvent{

    private MessageEntity messageEntity;
    
    public CustomEvent(Object source, MessageEntity messageEntity) {
        super(source);
        this.messageEntity = messageEntity;
    }
    
    public MessageEntity getMessageEntity() {
        return this.messageEntity;
    }
}

2.編寫監聽類

使用@EventListener方式。

/**
 * 監聽配置類
 * 
 * @author oKong
 *
 */
@Configuration
@Slf4j
public class EventListenerConfig {

    @EventListener
    public void handleEvent(Object event) {
        //監聽所有事件 可以看看 系統各類時間 發布了哪些事件
        //可根據 instanceof 監聽想要監聽的事件
//        if(event instanceof CustomEvent) {
//            
//        }
        log.info("事件:{}", event);
    }
    
    @EventListener
    public void handleCustomEvent(CustomEvent customEvent) {
        //監聽 CustomEvent事件
        log.info("監聽到CustomEvent事件,消息為:{}, 發布時間:{}", customEvent.getMessageEntity(), customEvent.getTimestamp());
    }
    
    /**
     * 監聽 code為oKong的事件
     */
    @EventListener(condition="#customEvent.messageEntity.code == 'oKong'")
    public void handleCustomEventByCondition(CustomEvent customEvent) {
        //監聽 CustomEvent事件
        log.info("監聽到code為'oKong'的CustomEvent事件,消息為:{}, 發布時間:{}", customEvent.getMessageEntity(), customEvent.getTimestamp());
    }
    
    @EventListener 
    public void handleObjectEvent(MessageEntity messageEntity) {
        //這個和eventbus post方法一樣了
        log.info("監聽到對象事件,消息為:{}", messageEntity);
        
    }
}

**
注意:Spring中,事件源不強迫繼承ApplicationEvent接口的,也就是可以直接發布任意一個對象類。但內部其實是使用PayloadApplicationEvent類進行包裝了一層。這點和guavaeventBus類似。**

而且,使用@EventListenercondition可以實現更加精細的事件監聽,condition支持SpEL表達式,可根據事件源的參數來判斷是否監聽。

使用ApplicationListener方式。

@Component
@Slf4j
public class EventListener implements ApplicationListener<CustomEvent>{

    @Override
    public void onApplicationEvent(CustomEvent event) {
        //這里也可以監聽所有事件 使用  ApplicationEvent 類即可
        //這里僅僅監聽自定義事件 CustomEvent
        log.info("ApplicationListener方式監聽事件:{}", event);
    }
}

3.編寫控制類,示例發布事件。

/**
 * 模擬觸發事件
 * @author oKong
 *
 */
@RestController
@RequestMapping("/push")
@Slf4j
public class DemoController {

    /**
     * 注入 事件發布類
     */
    @Autowired
    ApplicationEventPublisher eventPublisher;
    
    @GetMapping
    public String push(String code,String message) {
        log.info("發布applicationEvent事件:{},{}", code, message);
        eventPublisher.publishEvent(new CustomEvent(this, MessageEntity.builder().code(code).message(message).build()));
        return "事件發布成功!";
    }
    
    @GetMapping("/obj")
    public String pushObject(String code,String message) {
        log.info("發布對象事件:{},{}", code, message);
        eventPublisher.publishEvent(MessageEntity.builder().code(code).message(message).build());
        return "對象事件發布成功!";
    }
}

4.編寫啟動類。

/**
 * 事件監聽
 * 
 * @author oKong
 *
 */
@SpringBootApplication
@Slf4j
public class EventAndListenerApplication {
    public static void main(String[] args) throws Exception {

        SpringApplication app =new SpringApplication(EventAndListenerApplication.class);
        app.addListeners(new MyApplicationStartingEventListener());//加入自定義的監聽類
        app.run(args);
        log.info("spring-boot-event-listener-chapter32啟動!");
    }
}

這里,創建了個ApplicationStartingEvent事件監聽類。

/**
 * 示例-啟動事件
 * @author oKong
 *
 */
public class MyApplicationStartingEventListener implements ApplicationListener<ApplicationStartingEvent>{

    @Override
    public void onApplicationEvent(ApplicationStartingEvent event) {
        // TODO Auto-generated method stub
        //由於 log相關還未加載 使用了也輸出不了的
//        log.info("ApplicationStartingEvent事件發布:{}", event);
        System.out.println("ApplicationStartingEvent事件發布:" + event.getTimestamp());
    }

}

5.啟動應用,控制台可以看出,在啟動時,我們監聽到了ApplicationStartingEvent事件

ApplicationStartingEvent

首先訪問下:http://127.0.0.1:8080/push?code=lqdev&message=趔趄的猿,可以看見事件已經被監聽到了,而監聽了codeoKong的監聽未觸發。

然后訪問下:http://127.0.0.1:8080/push?code=oKong&message=趔趄的猿,可以看見此時三個監聽事件都接收到了事件了

此時,由於寫了一個監聽所有事件的方法,可以看見請求結束后,會發布一個事件ServletRequestHandledEvent,里面記錄了請求的時間、請求url、請求方式等等信息。

事件:ServletRequestHandledEvent: url=[/push]; client=[127.0.0.1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]

異步監聽處理

默認情況下,監聽事件都是同步執行的。在需要異步處理時,可以在方法上加上@Async進行異步化操作。此時,可以定義一個線程池,同時開啟異步功能,加入@EnableAsync

對於異步處理,可以查看之前發布的文章:《第二十一章:異步開發之異步調用》。里面有詳細的介紹異步調用,這里就不闡述了。

異步簡單示例:

    /**
     * 監聽 code為oKong的事件
     */
    @Async
    @EventListener(condition="#customEvent.messageEntity.code == 'oKong'")
    public void handleCustomEventByCondition(CustomEvent customEvent) {
        //監聽 CustomEvent事件
        log.info("監聽到code為'oKong'的CustomEvent事件,消息為:{}, 發布時間:{}", customEvent.getMessageEntity(), customEvent.getTimestamp());
    }

關於事務綁定事件

當一些場景下,比如在用戶注冊成功后,即數據庫事務提交了,之后再異步發送郵件等,不然會發生數據庫插入失敗,但事件卻發布了,也就是郵件發送成功了的情況。此時,我們可以使用@TransactionalEventListener注解或者TransactionSynchronizationManager類來解決此類問題,也就是:事務成功提交后,再發布事件。當然也可以利用返回上層(事務提交后)再發布事件的方式了,只是不夠優雅而已罷了,其實能起作用就好了,是吧~

本例中未使用到數據庫,就不示例了,都在Spring-tx包下。

具體可查看文章:Spring Event 事件中的事務控制

spring4.2之前

spring4.2之后

參考資料

  1. https://docs.spring.io/spring-boot/docs/1.5.15.RELEASE/reference/htmlsingle/#boot-features-application-events-and-listeners

  2. https://blog.csdn.net/eos2009/article/details/77773551

  3. https://www.cnblogs.com/senlinyang/p/8496099.html

總結

本章節主要簡單介紹了spring的事件機制。感興趣的同學,可以編寫一個監聽所有事件的方法,然后看看系統運行各類請求或者相關操作時,系統會發布哪些事件,了解后可以在之后碰見一些特殊業務需求時,可以適當的監聽相關的事件來完成特定的業務公共。同時對這種觀察者模式,大家還可以看看eventbusreactor了。后者沒用過,有時間倒是可以看看。最近買了本RxJava2書籍,確實要好好補課下了。

最后

目前互聯網上很多大佬都有SpringBoot系列教程,如有雷同,請多多包涵了。原創不易,碼字不易,還希望大家多多支持。若文中有所錯誤之處,還望提出,謝謝。

老生常談

  • 個人QQ:499452441
  • 微信公眾號:lqdevOps

公眾號

個人博客:http://blog.lqdev.cn

完整示例:https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-32

原文地址:https://blog.lqdev.cn/2018/11/06/springboot/chapter-thirty-two/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM