ContextRefreshedEvent事件使用不當(dāng)引發(fā)了線上問題

ContextRefreshEvent是Spring容器加載完發(fā)送的一個(gè)事件,在工作中有很多實(shí)現(xiàn)邏輯使用了該機(jī)制。

常見的使用姿勢如下:

@Component
public class ExampleListener {
    @EventListener
    public void handleEvent(ContextRefreshedEvent event) {
    業(yè)務(wù)實(shí)現(xiàn)的邏輯
    }
}

當(dāng)調(diào)用 bstractApplicationContext.refresh 法進(jìn)行加載或者刷新容器后,會在最后一步調(diào)用 finishRefresh 方法,發(fā)布一些相關(guān)的事件。

protected void finishRefresh() {
        // Clear context-level resource caches (such as ASM metadata from scanning).
        clearResourceCaches();
        // Initialize lifecycle processor for this context.
        initLifecycleProcessor();
        // Propagate refresh to lifecycle processor first.
        getLifecycleProcessor().onRefresh();
        // Publish the final event.
        publishEvent(new ContextRefreshedEvent(this));
        // Participate in LiveBeansView MBean, if active.
        LiveBeansView.registerApplicationContext(this);
}

但是在工作有一次使用了類似上述姿勢的代碼,卻導(dǎo)致了bug,經(jīng)過日志排查發(fā)現(xiàn) handleEvent 方法調(diào)用了多次,即發(fā)布了多次 ContextRefreshedEvent 事件,我以前誤認(rèn)為只會發(fā)送一次。

在Spring的文檔注釋中提示到:

Event raised when an {@code ApplicationContext} gets initialized or refreshed.

即當(dāng) ApplicationContext 進(jìn)行初始化或者刷新時(shí)都會發(fā)送該事件。

只有 finishRefresh() 方法里面會發(fā)送該事件,而該方法又只有 refresh() 方法調(diào)用,

在 refresh() 方法中有這些注釋:

Load or refresh the persistent representation of the configuration, which ight be from Java-based configuration, an XML file, a properties file, a elational database schema, or some other format. As this is a startup method, it should destroy already created singletons if it fails, to avoid dangling resources. In other words, after invocation of this method, either all or no singletons at all should be instantiated.

簡單翻譯總結(jié)下:

加載或者刷新配置文件,由于這是一個(gè)啟動(dòng)方法,如果失敗,它應(yīng)該銷毀已經(jīng)創(chuàng)建的單例,以避免懸空資源。換句話說,在調(diào)用這個(gè)方法前,要么全部實(shí)例化,要么根部不實(shí)例化。

那什么時(shí)候會發(fā)送多次呢?

在web項(xiàng)目中,系統(tǒng)中會存在兩個(gè)容器,一個(gè)是 root application context, 另一個(gè)是我們自己的 projectName-servlet context (作為root application context 的子容器),我們可以在業(yè)務(wù)邏輯中加上下段邏輯,避免重復(fù)執(zhí)行。

public void onApplicationEvent(ContextRefreshEvent event) {
    if (event.getApplicationContext().getParent() == null) {
        
    }
}

在 Dubbo 中,也有類似的場景。
在服務(wù)暴露時(shí),ServiceBean 實(shí)現(xiàn)了 ApplicationListener 接口,監(jiān)聽 ContextRefreshedEvent 事件,

public void onApplicationEvent(ContextRefreshedEvent event) {
        if (isDelay() && !isExported() && !isUnexported()) {
            if (logger.isInfoEnabled()) {
                logger.info("The service ready on spring started. service: " + getInterface());
            }
            export();
        }
    }

第一個(gè)方法 isDelay 主要是用來判別是否延遲暴露,因?yàn)?ServiceBean 也繼承了 InitializingBean 接口,在 afterPropertiesSet() 方法的最后一步有一個(gè)判斷,也有可能進(jìn)行服務(wù)暴露。

if (!isDelay()) {
    export();
}

第二個(gè)方法 isExported() 用來判斷該服務(wù)是否已經(jīng)暴露了,避免因?yàn)槭盏蕉鄠€(gè) ContextRefreshedEvent 事件時(shí)導(dǎo)致重復(fù)暴露,出現(xiàn)問題。記錄的兩個(gè)變量:

private transient volatile boolean exported;
private transient volatile boolean unexported;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容