ContextRefreshedEvent事件使用不当引发了线上问题

ContextRefreshEvent是Spring容器加载完发送的一个事件,在工作中有很多实现逻辑使用了该机制。

常见的使用姿势如下:

@Component
public class ExampleListener {
    @EventListener
    public void handleEvent(ContextRefreshedEvent event) {
    业务实现的逻辑
    }
}

当调用 bstractApplicationContext.refresh 法进行加载或者刷新容器后,会在最后一步调用 finishRefresh 方法,发布一些相关的事件。

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);
}

但是在工作有一次使用了类似上述姿势的代码,却导致了bug,经过日志排查发现 handleEvent 方法调用了多次,即发布了多次 ContextRefreshedEvent 事件,我以前误认为只会发送一次。

在Spring的文档注释中提示到:

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

即当 ApplicationContext 进行初始化或者刷新时都会发送该事件。

只有 finishRefresh() 方法里面会发送该事件,而该方法又只有 refresh() 方法调用,

在 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.

简单翻译总结下:

加载或者刷新配置文件,由于这是一个启动方法,如果失败,它应该销毁已经创建的单例,以避免悬空资源。换句话说,在调用这个方法前,要么全部实例化,要么根部不实例化。

那什么时候会发送多次呢?

在web项目中,系统中会存在两个容器,一个是 root application context, 另一个是我们自己的 projectName-servlet context (作为root application context 的子容器),我们可以在业务逻辑中加上下段逻辑,避免重复执行。

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

在 Dubbo 中,也有类似的场景。
在服务暴露时,ServiceBean 实现了 ApplicationListener 接口,监听 ContextRefreshedEvent 事件,

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

第一个方法 isDelay 主要是用来判别是否延迟暴露,因为 ServiceBean 也继承了 InitializingBean 接口,在 afterPropertiesSet() 方法的最后一步有一个判断,也有可能进行服务暴露。

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

第二个方法 isExported() 用来判断该服务是否已经暴露了,避免因为收到多个 ContextRefreshedEvent 事件时导致重复暴露,出现问题。记录的两个变量:

private transient volatile boolean exported;
private transient volatile boolean unexported;

你可能感兴趣的:(ContextRefreshedEvent事件使用不当引发了线上问题)