Spring 的事件監(jiān)聽機制是什么?你知道嗎?
在復(fù)雜的業(yè)務(wù)系統(tǒng)中,模塊間的過度耦合往往會導(dǎo)致代碼維護困難、擴展性受限。Spring 事件監(jiān)聽機制基于觀察者模式,提供了一種優(yōu)雅的解耦方案,使得組件間通過事件驅(qū)動實現(xiàn)松耦合通信。這種機制不僅被 Spring 框架內(nèi)部使用(如容器生命周期事件),對外其也提供了靈活的擴展能力,我們可以基于這些擴展點輕松構(gòu)建個性化的事件監(jiān)聽功能。
設(shè)計思想
- 分層解耦設(shè)計:事件發(fā)布者與監(jiān)聽者通過事件對象進行間接通信,避免了直接方法調(diào)用帶來的強耦合。設(shè)計契合了"開閉原則"——新增監(jiān)聽器無需修改發(fā)布者代碼。
- 精準的事件路由機制:通過泛型約束(ApplicationListener<E>)和事件類型匹配算法,確保事件只會被感興趣的監(jiān)聽器處理,避免無效的事件傳播。
- 可擴展的廣播策略:ApplicationEventMulticaster 接口抽象了事件分發(fā)邏輯,支持同步/異步分發(fā)、異常處理策略等擴展點,為復(fù)雜場景提供靈活支持。
核心概念及組件
- 事件(ApplicationEvent):這是 Spring 中所有應(yīng)用事件的基類,它是一個抽象類,定義了事件的基本屬性,如事件源(source)、事件發(fā)生的時間(timestamp)等。我們可以根據(jù)自己的需求繼承該類來創(chuàng)建自定義事件。
// 承載業(yè)務(wù)數(shù)據(jù)的載體,所有 Spring 應(yīng)用事件的基類,必須由具體事件類繼承
publicabstractclass ApplicationEvent extends EventObject {
privatestaticfinallong serialVersionUID = 7099057708183571937L;
// 記錄事件發(fā)生的時間戳(毫秒級精度)
privatefinallong timestamp;
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
}
public ApplicationEvent(Object source, Clock clock) {
super(source);
this.timestamp = clock.millis();
}
public final long getTimestamp() {
returnthis.timestamp;
}
}
Spring 的內(nèi)置事件
Spring 框架內(nèi)部定義了一些常用的內(nèi)置事件,這些事件在容器的生命周期中扮演著重要角色。以下是部分內(nèi)置事件及其觸發(fā)時機和典型應(yīng)用場景:
事件類型 | 觸發(fā)時機 | 典型應(yīng)用場景 |
| 容器初始化完成或刷新時 | 緩存預(yù)熱、配置加載 |
| 調(diào)用 | 啟動后臺任務(wù)線程 |
| 調(diào)用 | 釋放資源、暫停定時任務(wù) |
| 調(diào)用 | 數(shù)據(jù)庫連接池銷毀 |
| HTTP 請求處理完畢時(需 Spring MVC 環(huán)境) | 請求耗時統(tǒng)計、日志記錄 |
- 事件發(fā)布器(ApplicationEventPublisher):事件發(fā)布接口,它提供了發(fā)布事件的方法。在 Spring 容器中,通常我們會在需要發(fā)布事件的 Bean 中通過依賴注入的方式注入 ApplicationEventPublisher 接口的實現(xiàn),并調(diào)用其 publishEvent 方法來發(fā)布事件。
/**
* 封裝事件發(fā)布功能的核心接口,同時也是 ApplicationContext 的父接口
* @FunctionalInterface 標記函數(shù)式接口,支持 lambda 表達式實現(xiàn)
*/
@FunctionalInterface
public interface ApplicationEventPublisher {
/**
* 發(fā)布 ApplicationEvent 類型的事件(如 ContextRefreshedEvent)
* 底層會將事件對象強轉(zhuǎn)為 Object 類型,然后調(diào)用重載方法統(tǒng)一處理
*/
default void publishEvent(ApplicationEvent event) {
publishEvent((Object) event);
}
/**
* 通用事件發(fā)布方法,支持任意對象類型的事件
* 若參數(shù)非 ApplicationEvent 類型,Spring 會將其封裝為 PayloadApplicationEvent
* 事件最終通過 ApplicationEventMulticaster 廣播給匹配的監(jiān)聽器
* 官方設(shè)計提示:耗時操作建議監(jiān)聽器自行實現(xiàn)異步處理
*/
void publishEvent(Object event);
}
- 事件監(jiān)聽器(ApplicationListener):事件監(jiān)聽接口,定義了監(jiān)聽事件的方法。應(yīng)用程序中可實現(xiàn)該接口,監(jiān)聽感興趣的事件,并在 onApplicationEvent() 方法中編寫處理事件的邏輯。當事件發(fā)布者發(fā)布事件時,Spring 容器會自動將事件通知到所有已注冊的、對該事件感興趣的事件監(jiān)聽器上。
/**
* 訂閱并處理特定事件,基于觀察者模式,繼承標準 EventListener 接口
*/
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
// 當匹配類型的事件發(fā)布時觸發(fā)
void onApplicationEvent(E event);
}
- 事件廣播器(ApplicationEventMulticaster):廣播器接口,它負責(zé)管理事件監(jiān)聽器的注冊與注銷,并將事件分發(fā)給所有的監(jiān)聽器。在 Spring 容器中,常見如 SimpleApplicationEventMulticaster,它提供了基本的事件廣播功能。
/**
* 事件廣播器接口,用于管理監(jiān)聽器注冊表,實現(xiàn)事件路由。
* 通常由 Spring 上下文內(nèi)部實現(xiàn),作為事件發(fā)布委托組件。
*/
publicinterface ApplicationEventMulticaster {
/**
* 添加監(jiān)聽器實例(編程式注冊)
*/
void addApplicationListener(ApplicationListener<?> listener);
/**
* 通過 Bean 名稱添加監(jiān)聽器(適用于容器管理的 Bean)
*/
void addApplicationListenerBean(String listenerBeanName);
/**
* 移除指定監(jiān)聽器實例
*/
void removeApplicationListener(ApplicationListener<?> listener);
/**
* 通過Bean名稱移除監(jiān)聽器
*/
void removeApplicationListenerBean(String listenerBeanName);
/**
* 清空所有注冊的監(jiān)聽器實例和Bean名稱
*/
void removeAllListeners();
/**
* 廣播事件(自動推斷事件類型)
*/
void multicastEvent(ApplicationEvent event);
/**
* 廣播事件(顯式指定事件類型,支持泛型解析)
*/
void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType);
}
案例解析
為了更直觀地了解 Spring 事件監(jiān)聽機制的使用,下面通過一個用戶注冊事件的案例進行說明。
- 定義事件模型:
@Getter
public class UserRegisterEvent extends ApplicationEvent {
private final String username;
public UserRegisterEvent(Object source, String username) {
super(source);
this.username = username;
}
}
- 定義事件監(jiān)聽器:
// 方式1:實現(xiàn) ApplicationListener 接口
@Component
publicclass EmailListener implements ApplicationListener<UserRegisterEvent> {
@Override
public void onApplicationEvent(UserRegisterEvent event) {
sendEmail(event.getUsername());
}
}
// 方式2:使用 @EventListener 注解
@Component
publicclass LogsListener {
@EventListener
public void handleEvent(UserRegisterEvent event) {
addLogs(event.getUsername());
}
}
- 注冊監(jiān)聽器:注冊監(jiān)聽器的方式有很多種
a.一種如上述代碼所示,通過 @Component 自動進行掃描
b.以配置 Bean 的方式,SpringBoot 自動裝配等
c.我們還可以通過手動注冊,applicationContext.addApplicationListener(new EmailListener());
- 發(fā)布事件:
@Service
public class UserService {
@Resource
private ApplicationContext context;
public void register(String username) {
// 業(yè)務(wù)邏輯 ......
context.publishEvent(new UserRegisterEvent(this, username));
}
}
源碼分析
看完上述案例后,我們帶著以下幾個問題來分析下源碼,看看這些個組件之間到底是如何協(xié)作的。
- ApplicationConext 接口是如何發(fā)布事件的
- Spring 是何時、如何識別并加載事件監(jiān)聽器的
- Spring 加載的監(jiān)聽器是如何找到對應(yīng)事件的
在上述案例中,發(fā)布事件時,我們使用的是 context.publishEvent(new UserRegisterEvent(this, username)); 來進行事件發(fā)布的。ApplicationConext 接口具有事件發(fā)布能力是因為其繼承了事件發(fā)布器接口 ApplicationEventPublisher:
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {}
但在其方法內(nèi)部真正發(fā)布事件的其實是事件廣播器:
@Override
public void publishEvent(ApplicationEvent event) {
publishEvent(event, null);
}
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
......
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
} else {
// 獲取事件廣播器并將事件路由至對應(yīng)的監(jiān)聽器上
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
......
}
那么,事件廣播器又是何時被初始化的呢?它定義在 AbstractApplicationContext 中,是在容器刷新時被初始化出來的,也就是在執(zhí)行 refresh() 方法時,方法邏輯復(fù)雜,這里只貼出關(guān)鍵部分:
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
......
// 預(yù)備刷新 beanFactory
prepareBeanFactory(beanFactory);
try {
// 注冊 bean 后置處理器
registerBeanPostProcessors(beanFactory);
// Initialize event multicaster for this context.
// 初始化事件廣播器
initApplicationEventMulticaster();
// 注冊事件監(jiān)聽器
registerListeners();
// 實例化所有非懶加載的單例 bean
finishBeanFactoryInitialization(beanFactory);
// 發(fā)布相關(guān)事件(ContextRefreshedEvent)
finishRefresh();
} catch (BeansException ex) {
......
} finally {
......
}
}
}
我們可以看到,在 refresh() 方法中,通過 initApplicationEventMulticaster() 方法初始化了初始化事件廣播器,具體邏輯如下:
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
// 判斷容器中是否有名為 applicationEventMulticaster 的廣播器,有的話使用這個進行初始化
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
}
}
else {
// 沒有的話,初始化一個 SimpleApplicationEventMulticaster,并注冊到容器中
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
}
}
初始化事件廣播器的邏輯比較簡單,首先是從容器中獲取 beanName 為 applicationEventMulticaster 的廣播器,如果用戶自定義并向容器中注冊了該名稱的事件廣播器,那么就會執(zhí)行該流程。如果沒有,則默認創(chuàng)建一個 SimpleApplicationEventMulticaster。至此,完成了事件發(fā)布器的初始化。
上文分析核心組件時,我們知道事件廣播器的職責(zé)是管理事件監(jiān)聽器的注冊與注銷,并進行事件路由。那么,事件監(jiān)聽器的注冊時機是什么時候呢?答案還是在 refresh() 方法中,通過調(diào)用 registerListeners() 方法,完成了監(jiān)聽器的注冊流程。代碼如下:
protected void registerListeners() {
/**
* 這里 getApplicationListeners() 是獲取的成員變量 applicationListeners 的值
* 是指通過 context.addApplicationListener(new EmailListener())編程式手動添加的
* 或者是通過 spring.factories 自動裝配進來的 Listener
*/
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}
/**
* 獲取容器中所有實現(xiàn) ApplicationListener 接口的 Bean 的名稱
* 這里不對 bean 進行初始化,交由 bean 的后置處理器在初始化后實際注冊
* 此處數(shù)組中的 beanName 不包含上述 getApplicationListeners() 方法中獲取到的 ApplicationListener
*/
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}
// 處理早期應(yīng)用事件(在事件廣播器初始化前緩存的事件)
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
// 如果存在早期事件,通過多播器廣播這些事件
if (!CollectionUtils.isEmpty(earlyEventsToProcess)) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}
在上述 registerListeners() 階段,應(yīng)用程序中通過 @Component 等注解定義的監(jiān)聽器 Bean 可能尚未實例化,因此只能注冊其名稱。在后續(xù) Bean 初始化階段,Bean 的后置處理器 ApplicationListenerDetector 確保這些監(jiān)聽器被實際注冊到上下文中。ApplicationListenerDetector 后置處理器是在 refresh() 的 prepareBeanFactory() 方法中被添加到 Bean 工廠中的。
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof ApplicationListener) {
// registerListeners() 階段可能會檢測不到某些監(jiān)聽器(例如延遲初始化的 Bean 或動態(tài)代理生成的 Bean)
// 從緩存中獲取該 Bean 是否為單例的標記
Boolean flag = this.singletonNames.get(beanName);
if (Boolean.TRUE.equals(flag)) {
// 將 Bean 注冊為應(yīng)用監(jiān)聽器
this.applicationContext.addApplicationListener((ApplicationListener<?>) bean);
}
elseif (Boolean.FALSE.equals(flag)) {
// 如果是非單例 Bean,移除該 Bean 的標記,避免后續(xù)重復(fù)處理
this.singletonNames.remove(beanName);
}
}
return bean;
}
到這里,監(jiān)聽器的注冊也完成了。事件監(jiān)聽器可以來自自動裝配、來自編程式手動添加,也可以來自掃描 ApplicationListener 接口的實現(xiàn)。事件廣播器和事件監(jiān)聽器都有了,接下來就是事件是如何路由到對應(yīng)的監(jiān)聽器的呢?
事件的路由還是回歸到事件的發(fā)布,事件發(fā)布時,最終是通過事件廣播器路由事件的,也就是如下這個代碼:
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
......
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
} else {
// 獲取事件廣播器并將事件路由至對應(yīng)的監(jiān)聽器上
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
......
}
廣播器通過調(diào)用 multicastEvent() 方法,對事件進行廣播,最終找到事件對應(yīng)的監(jiān)聽器,觸發(fā)監(jiān)聽邏輯:
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
// 推斷事件類型
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
// 獲取配置的異步執(zhí)行器
Executor executor = getTaskExecutor();
// 獲取所有匹配的監(jiān)聽器
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
// 如果配置了異步執(zhí)行器,則異步執(zhí)行
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
// 同步執(zhí)行監(jiān)聽器
invokeListener(listener, event);
}
}
}
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
// 執(zhí)行監(jiān)聽器
doInvokeListener(listener, event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
doInvokeListener(listener, event);
}
}
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
// 調(diào)用監(jiān)聽器接口方法
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
......
}
}
到這里,我們定義監(jiān)聽器時重寫的 onApplicationEvent() 方法被調(diào)用了,事件的發(fā)布和處理流程就分析完了。
Spring 事件機制完美詮釋了"不要打電話給我們,我們會通知你(Don’t call us, we’ll call you)的“好萊塢原則”。這種通過職責(zé)分離和抽象分層的設(shè)計使得系統(tǒng)模塊既能保持獨立演進,又能通過事件總線實現(xiàn)高效協(xié)作。
最后,我們再來看幾個面試經(jīng)常問到的問題:
同一個事件可以有多個監(jiān)聽器嗎?
可以。Spring 事件監(jiān)聽機制天然支持多監(jiān)聽器對同一事件的訂閱與響應(yīng)。
- 機制原理:當一個事件被發(fā)布時,Spring 會通過 ApplicationEventMulticaster 遍歷所有注冊的監(jiān)聽器,篩選出匹配事件類型的監(jiān)聽器并觸發(fā)其邏輯。
- 應(yīng)用場景:例如用戶注冊事件可同時觸發(fā)發(fā)送郵件、記錄日志、初始化積分等操作,每個功能由獨立監(jiān)聽器處理。
- 順序控制:可通過 @Order 注解或?qū)崿F(xiàn) Ordered 接口指定監(jiān)聽器的執(zhí)行順序,數(shù)值越小優(yōu)先級越高。
事件監(jiān)聽器一定要實現(xiàn) ApplicationListener<E> 接口嗎?
不一定。Spring 提供了兩種定義監(jiān)聽器的方式:
- 接口實現(xiàn):通過實現(xiàn) ApplicationListener<E> 接口并指定泛型事件類型(如 UserRegisterEvent),適用于需要強類型約束的場景。
- 注解驅(qū)動:使用 @EventListener 注解標注方法(如 @EventListener(UserRegisterEvent.class)),支持更靈活的事件類型匹配(包括非 ApplicationEvent 子類)。
- 優(yōu)勢對比:注解方式允許一個類內(nèi)定義多個監(jiān)聽方法,且支持條件過濾(condition 屬性),代碼更加簡潔。
@Component
publicclass UserEventListener {
// 默認處理:所有注冊事件均觸發(fā)
@EventListener
public void handleDefault(UserRegisterEvent event) {
System.out.println("記錄日志: 用戶 " + event.getUsername() + " 注冊成功");
}
// 指定渠道處理:僅當 channel=web 時觸發(fā)
@EventListener(condition = "#event.channel == 'web'")
public void handleWebChannel(UserRegisterEvent event) {
System.out.println("推送站內(nèi)消息至用戶: " + event.getUsername());
}
}
事件監(jiān)聽操作是同步的還是異步的?
默認同步執(zhí)行,但可通過配置實現(xiàn)異步。
- 同步模式:事件發(fā)布后,所有監(jiān)聽器按順序串行執(zhí)行。
- 異步模式:
a.全局異步:通過 SimpleApplicationEventMulticaster 的 setTaskExecutor() 方法設(shè)置一個 TaskExecutor。
b.局部異步:在監(jiān)聽方法上添加 @Async 注解,并啟用 @EnableAsync 配置。
c.注意事項:異步模式下需處理線程上下文傳遞(如事務(wù)傳播、ThreadLocal 變量)及異?;貪L邏輯。
分布式系統(tǒng)中 Spring 事件監(jiān)聽機制的限制及替代方案?
- 單機局限:Spring 事件僅在單個 JVM 內(nèi)傳播,無法跨服務(wù)邊界。
- 可靠性問題:無內(nèi)置重試、持久化機制,若監(jiān)聽器執(zhí)行失敗可能導(dǎo)致事件丟失。
- 替代方案:分布式系統(tǒng)中需集成消息隊列,將事件轉(zhuǎn)換為消息,通過 RabbitMQ、Kafka 等中間件實現(xiàn)跨服務(wù)事件分發(fā)。