Spring源码探究:事件机制

结合Spring源码分析Spring事件机制


问题

问题描述:项目中配置事件监听,监听当容器加载完成之后,做一些初始化工作。项目运行之后,发现初始化工作被重复做了两次。为了便于分析,去掉代码中的业务逻辑,只留下场景。

配置监听器
/**
 * @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) {
        //业务代码
        logger.error("将有权限人员放入缓存。。。。");
    }
}

配置FreshListener监听器,监听当容器加载完成之后,将管理员名单加入缓存。却发现,名单被加载了两次。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;
    }  
}
用户监听器
/**
 * @author jiangwang
 * @date 21:37 2018/6/1
 */
public interface UserListener extends EventListener {
    void onRegister(UserEvent event);
}
发送短信监听器
/**
 * @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);
    }
}
发送短信事件
/**
 * @author jiangwang
 * @date 21:40 2018/6/1
 */
public class SendSmsEvent extends UserEvent {
    public SendSmsEvent(Object source) {
        super(source);
    }
}
服务类,用于存放事件监听,类比容器
public class UserService {
    private List<UserListener> listenerList = new ArrayList<>();
    //当用户注册的时候,触发发送短信事件
    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());
        //添加其他监听器 ...
        User user = new User("foo", "123456", "注册成功啦!!");
        service.register(user);
    }
}
运行结果

result.png

启动项目,模拟用户注册,触发了短信发送事件。从上述简单的模拟事件代码中,可以归结出三个名词,事件(SendSmsEvent)监听器(SendSmsListener)事件源(用户注册)。可以将上述流程描述为:用户注册==>触发发送短息事件==>短信监听器监听到消息。
上述代码有两个重要接口:

事件监听器接口
/**
 * 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():监听器注册,类似于上文EventAppservice.addListeners(new SendSmsListener()), 下文重点讲。
  3. finishRefresh():发布事件,类似于上文UserServicepublishEvent(new SendSmsEvent(user)),一会重点讲。
进入registerListeners()方法
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);

从容器中的所有bean中获取实现ApplicationListener接口的类。换言之,如果我们想使用Spring 事件机制来为我们项目服务,那我们所写的监听器必须实现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在监听器接口中添加了onApplicationEvent()方法,便于事件被触发时执行任务,类似于上午UserListener中的onRegister()方法。
回到registerListeners()方法,获取到监听器类之后,存放在了事件广播器(applicationEventMulticaster)中,便于后面使用。

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

这句话类似于UserService中的publishEvent(new SendSmsEvent(user)),而ContextRefreshedEvent类似于上文中的发送短信事件ContextRefreshedEvent代表的事件是容器初始化完成。如果容器初始化完成了,那么所对应的事件监听器将会被触发。继续层层跟进,来到:

publishEvent(Object event, ResolvableType eventType)
跟进看重点代码:
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
进入multicastEvent:
getApplicationListeners(event, type)

这句话的意思是根据事件类型获取监听器。因为咱们在项目里面可能会配置很多监听器,每一个监听器都会有自己所对应的事件类型,只有自己所对应的事件发生了,监听器才会被触发。

继续看multicastEvent中的代码:
invokeListener(listener, event);
进入invokeListener:
doInvokeListener(listener, event);
进入doInvokeListener:
listener.onApplicationEvent(event);

看到了onApplicationEvent在此执行了,类似于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);
            }
        }

判断该容器是否有父容器,若存在入容器,再一次触发父容器中的事件监听器。


回答为什么事件监听器会被执行两次?

从上文的源码分析中,咱们知道了ContextRefreshedEvent事件监听器是在refresh()方法内被触发的,更准确地讲,是refresh()方法中的finishRefresh()触发了ContextRefreshedEvent事件监听器。而我们在spring源码探究—容器 一文中,得出一个结论:子容器可以获取父容器bean,反之不行。这里是因为Spring容器初始化执行refresh()方法时,触发了ContextRefreshedEvent事件监听器,而SpringMvc容器初始化时也执行了refresh()方法,当代码执行到

publishEvent(Object event, ResolvableType eventType);

其中有一段代码判断了是否存在父容器。若存在,会将父容器中的监听器执行一遍。所以再一次触发了ContextRefreshedEvent事件监听器。所以从直观上看,初始化了两次。


解决方案:

  1. 严格控制有且仅有父容器或子容器执行监听器。举例:
/**
 * @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. 监听器方法执行时加锁,举例(伙伴提供):
/**
 * 实现此类, 可以在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地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,884评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,212评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,351评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,412评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,438评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,127评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,714评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,636评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,173评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,264评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,402评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,073评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,763评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,253评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,382评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,749评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,403评论 2 358

推荐阅读更多精彩内容