Spring源碼探究:事件機制

結合Spring源碼分析Spring事件機制


問題

問題描述:項目中配置事件監(jiān)聽,監(jiān)聽當容器加載完成之后,做一些初始化工作。項目運行之后,發(fā)現初始化工作被重復做了兩次。為了便于分析,去掉代碼中的業(yè)務邏輯,只留下場景。

配置監(jiān)聽器
/**
 * @author jiangwang3
 * @date 2018/6/1.
 */
@Component
public class FreshListener implements ApplicationListener<ContextRefreshedEvent>{
    private final Logger logger = LoggerFactory.getLogger(getClass());
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        //業(yè)務代碼
        logger.error("將有權限人員放入緩存。。。。");
    }
}

配置FreshListener監(jiān)聽器,監(jiān)聽當容器加載完成之后,將管理員名單加入緩存。卻發(fā)現,名單被加載了兩次。WHY???


從源碼的角度探究該問題

由于源碼中的個方法較長,所以只貼出重點且與主題相關的代碼。建議結合本地源碼一起看。

為了說清楚這個問題,咱們需要具備兩個知識點
  1. jdk事件機制
  2. Spring事件機制

jdk事件機制

User實體類
public class User {
    private String username;
    private String password;
    private String sms;
    public User(String username, String password, String sms) {
        this.username = username;
        this.password = password;
        this.sms = sms;
    }  
}
用戶監(jiān)聽器
/**
 * @author jiangwang
 * @date 21:37 2018/6/1
 */
public interface UserListener extends EventListener {
    void onRegister(UserEvent event);
}
發(fā)送短信監(jiān)聽器
/**
 * @author jiangwang
 * @date 21:38 2018/6/1
 */
public class SendSmsListener implements UserListener {
    @Override
    public void onRegister(UserEvent event) {
        if (event instanceof SendSmsEvent) {
            Object source = event.getSource();
            User user = (User) source;
            System.out.println("send sms to " + user.getUsername());
        }
    }
}
User事件
/**
 * @author jiangwang
 * @date 21:39 2018/6/1
 */
public class UserEvent extends EventObject {
    public UserEvent(Object source){
        super(source);
    }
}
發(fā)送短信事件
/**
 * @author jiangwang
 * @date 21:40 2018/6/1
 */
public class SendSmsEvent extends UserEvent {
    public SendSmsEvent(Object source) {
        super(source);
    }
}
服務類,用于存放事件監(jiān)聽,類比容器
public class UserService {
    private List<UserListener> listenerList = new ArrayList<>();
    //當用戶注冊的時候,觸發(fā)發(fā)送短信事件
    public void register(User user){
        System.out.println("name= " + user.getUsername() + " ,password= " + 
                                      user.getPassword() + " ,注冊成功");
        publishEvent(new SendSmsEvent(user));
    }
    public void publishEvent(UserEvent event){
        for(UserListener listener : listenerList){
            listener.onRegister(event);
        }
    }
    public void addListeners(UserListener listener){
        this.listenerList.add(listener);
    }
}
測試類
/**
 * @author jiangwang
 * @date 21:35 2018/6/1
 */
public class EventApp {
    public static void main(String[] args) {
        UserService service = new UserService();
        service.addListeners(new SendSmsListener());
        //添加其他監(jiān)聽器 ...
        User user = new User("foo", "123456", "注冊成功啦?。?);
        service.register(user);
    }
}
運行結果

result.png

啟動項目,模擬用戶注冊,觸發(fā)了短信發(fā)送事件。從上述簡單的模擬事件代碼中,可以歸結出三個名詞,事件(SendSmsEvent)監(jiān)聽器(SendSmsListener),事件源(用戶注冊)??梢詫⑸鲜隽鞒堂枋鰹椋河脩糇?=>觸發(fā)發(fā)送短息事件==>短信監(jiān)聽器監(jiān)聽到消息。
上述代碼有兩個重要接口:

事件監(jiān)聽器接口
/**
 * A tagging interface that all event listener interfaces must extend.
 * @since JDK1.1
 */
public interface EventListener {
}

該接口為標識接口

事件接口
/**
 * <p>
 * The root class from which all event state objects shall be derived.
 * <p>
 * All Events are constructed with a reference to the object, the "source",
 * that is logically deemed to be the object upon which the Event in question
 * initially occurred upon.
 * @since JDK1.1
 */
public class EventObject implements java.io.Serializable {
    private static final long serialVersionUID = 5516075349620653480L;
    /**
     * The object on which the Event initially occurred.
     */
    protected transient Object  source;
    /**
     * Constructs a prototypical Event.
     * @param    source    The object on which the Event initially occurred.
     * @exception  IllegalArgumentException  if source is null.
     */
    public EventObject(Object source) {
        if (source == null)
            throw new IllegalArgumentException("null source");
        this.source = source;
    }
    /**
     * The object on which the Event initially occurred.
     * @return   The object on which the Event initially occurred.
     */
    public Object getSource() {
        return source;
    }
    /**
     * Returns a String representation of this EventObject.
     * @return  A a String representation of this EventObject.
     */
    public String toString() {
        return getClass().getName() + "[source=" + source + "]";
    }
}

該接口中僅有source參數,無特殊含義,類似于存放數據源

Spring事件機制

對比上面jdk事件的Demo,咱們分析spring源碼

spring源碼探究—容器 一文中,我們分析了Spring中bean是如何加載的,并且分析了項目啟動的入口,不做贅敘,將其作為已知條件。

進入AbstractApplicationContext的refresh()方法
@Override
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
            prepareRefresh();
            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);
            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);
                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);
                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);
                // Initialize message source for this context.
                initMessageSource();
                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();
                // Initialize other special beans in specific context subclasses.
                onRefresh();
                // Check for listener beans and register them.
                registerListeners();
                // Instantiate all remaining (non-lazy-init) singletons.
                finishBeanFactoryInitialization(beanFactory);
                // Last step: publish corresponding event.
                finishRefresh();
            }
        }
    }

這個方法中有三句話與Spring事件相關,把這三句話分析明白了,Spring事件機制也就了然了。挨個分析:

  1. initApplicationEventMulticaster():初始化事件廣播器
  2. registerListeners():監(jiān)聽器注冊,類似于上文EventAppservice.addListeners(new SendSmsListener()), 下文重點講。
  3. finishRefresh():發(fā)布事件,類似于上文UserServicepublishEvent(new SendSmsEvent(user)),一會重點講。
進入registerListeners()方法
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);

從容器中的所有bean中獲取實現ApplicationListener接口的類。換言之,如果我們想使用Spring 事件機制來為我們項目服務,那我們所寫的監(jiān)聽器必須實現ApplicationListener接口。

進入ApplicationListener接口:
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    /**
     * Handle an application event.
     * @param event the event to respond to
     */
    void onApplicationEvent(E event);
}

ApplicationListener接口繼承自jdk事件機制中的EventListener,可以看出Spring事件機制改編自jdk事件機制。Spring在監(jiān)聽器接口中添加了onApplicationEvent()方法,便于事件被觸發(fā)時執(zhí)行任務,類似于上午UserListener中的onRegister()方法。
回到registerListeners()方法,獲取到監(jiān)聽器類之后,存放在了事件廣播器(applicationEventMulticaster)中,便于后面使用。

進入finishRefresh()方法
publishEvent(new ContextRefreshedEvent(this));

這句話類似于UserService中的publishEvent(new SendSmsEvent(user)),而ContextRefreshedEvent類似于上文中的發(fā)送短信事件。ContextRefreshedEvent代表的事件是容器初始化完成。如果容器初始化完成了,那么所對應的事件監(jiān)聽器將會被觸發(fā)。繼續(xù)層層跟進,來到:

publishEvent(Object event, ResolvableType eventType)
跟進看重點代碼:
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
進入multicastEvent:
getApplicationListeners(event, type)

這句話的意思是根據事件類型獲取監(jiān)聽器。因為咱們在項目里面可能會配置很多監(jiān)聽器,每一個監(jiān)聽器都會有自己所對應的事件類型,只有自己所對應的事件發(fā)生了,監(jiān)聽器才會被觸發(fā)。

繼續(xù)看multicastEvent中的代碼:
invokeListener(listener, event);
進入invokeListener:
doInvokeListener(listener, event);
進入doInvokeListener:
listener.onApplicationEvent(event);

看到了onApplicationEvent在此執(zhí)行了,類似于UserService中listener.onRegister(event)
至此,事件機制分析完畢。

咱們再次回到publishEvent(Object event, ResolvableType eventType)中,有這么一段代碼:
// Publish event via parent context as well...
        if (this.parent != null) {
            if (this.parent instanceof AbstractApplicationContext) {
                ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
            }
            else {
                this.parent.publishEvent(event);
            }
        }

判斷該容器是否有父容器,若存在入容器,再一次觸發(fā)父容器中的事件監(jiān)聽器。


回答為什么事件監(jiān)聽器會被執(zhí)行兩次?

從上文的源碼分析中,咱們知道了ContextRefreshedEvent事件監(jiān)聽器是在refresh()方法內被觸發(fā)的,更準確地講,是refresh()方法中的finishRefresh()觸發(fā)了ContextRefreshedEvent事件監(jiān)聽器。而我們在spring源碼探究—容器 一文中,得出一個結論:子容器可以獲取父容器bean,反之不行。這里是因為Spring容器初始化執(zhí)行refresh()方法時,觸發(fā)了ContextRefreshedEvent事件監(jiān)聽器,而SpringMvc容器初始化時也執(zhí)行了refresh()方法,當代碼執(zhí)行到

publishEvent(Object event, ResolvableType eventType);

其中有一段代碼判斷了是否存在父容器。若存在,會將父容器中的監(jiān)聽器執(zhí)行一遍。所以再一次觸發(fā)了ContextRefreshedEvent事件監(jiān)聽器。所以從直觀上看,初始化了兩次。


解決方案:

  1. 嚴格控制有且僅有父容器或子容器執(zhí)行監(jiān)聽器。舉例:
/**
 * @author jiangwang3
 * @date 2018/6/1.
 */
@Component
public class EvolvedFreshListener implements ApplicationListener<ContextRefreshedEvent>{
    private final Logger logger = LoggerFactory.getLogger(getClass());
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (event.getApplicationContext().getParent() == null){
            logger.error("進化版====將有權限人員放入緩存。。。。");
        }

    }
}
  1. 將bean放在子容器中,例如將其配置在SpringMvc容器中,自行實現。
  2. 監(jiān)聽器方法執(zhí)行時加鎖,舉例(伙伴提供):
/**
 * 實現此類, 可以在Spring容器完全初始化完畢時獲取到Spring容器 
 * @author wanghui59@jd.com
 * @since 2017-12-29
 */
public abstract class ContextRefreshListener implements 
                        ApplicationListener<ContextRefreshedEvent> {
    private volatile boolean initialized = false;  
    @Override
    public synchronized void onApplicationEvent(ContextRefreshedEvent event) {
        if (!initialized) {
            System.out.println("加鎖====將有權限人員放入緩存。。。。");
        }
    }
}

項目完整源碼github地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容