Spring事件监听就是这么简单

听到监听这个词,不难理解,一个事物根据另一个事物的变化自发的作出响应,而且每次都作出同样的响应。就像点击按钮一样。每次点击登入按钮,都会访问登入接口url,这就是监听。

那么监听需要哪些条件呢。三要素,1.事件2.监听器3.触发动作。点击按钮就是事件,点击之后要怎么处理,就是监听器的事了。那么问题来了,监听器肯定有很多个,每个监听器职责不一样,它们怎么知道要监听哪个事件的。当然是事先就告诉它们了。也就是说在发布事件的时候,所有监听器就已经准备就绪,然后根据事件类型匹配对应监听器。

image

事实上,spring就是这么干的。

有几个问题:

1.spring的监听器是怎么注册的?在何时注册的?

2.这些事件是如何发布的?

3.事件是怎么找到对应监听器的?

带着这几个问题往下看。。。

先看最简单的一种实现方式。

1.创建一个监听器,实现ApplicationListener接口,泛型中指定事件类型

public class PrintListener implements ApplicationListener<DemoEvent> {
    @Override
    public void onApplicationEvent(DemoEvent event) {
        System.out.println("调用DemoEvent的print方法输出其内容:");
        event.print();
    }
}

2.创建一个事件,继承ApplicationEvent抽象类

public class DemoEvent extends ApplicationEvent {
    private String text;

    /**
     * Create a new ApplicationEvent.
     *
     * @param source the object on which the event initially occurred (never {@code null})
     */
    public DemoEvent(Object source) {
        super(source);
    }

    public DemoEvent(Object source, String text) {
        super(source);
        this.text = text;
    }

    public void print() {
        System.out.println("print event content:" + this.text);
    }
}

3.注册监听器到容器中,发布事件。

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        //注册监听器
        context.addApplicationListener(new PrintListener());
        //发布事件
        context.publishEvent(new DemoEvent(new Object(),"hello world."));
    }

}

接下来到源码中去看下。
我们在执行scontext.addApplicationListener的时候,最终走到了一个处理事件的广播类。如下。其实就是把所有的listener加到ListenerRetriever类下的Set集合applicationListeners

public abstract class AbstractApplicationEventMulticaster
        implements ApplicationEventMulticaster, BeanClassLoaderAware, BeanFactoryAware {

    private final ListenerRetriever defaultRetriever = new ListenerRetriever(false);

    final Map<ListenerCacheKey, ListenerRetriever> retrieverCache = new ConcurrentHashMap<>(64);

    @Nullable
    private ClassLoader beanClassLoader;

    @Nullable
    private BeanFactory beanFactory;

    private Object retrievalMutex = this.defaultRetriever;
        /***************************省略一堆代码*************************/
    @Override
    public void addApplicationListener(ApplicationListener<?> listener) {
        synchronized (this.retrievalMutex) {
            // Explicitly remove target for a proxy, if registered already,
            // in order to avoid double invocations of the same listener.
            Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
            if (singletonTarget instanceof ApplicationListener) {
                this.defaultRetriever.applicationListeners.remove(singletonTarget);
            }
            this.defaultRetriever.applicationListeners.add(listener);
            this.retrieverCache.clear();
        }
    }

是不是第一个问题就已经解决了。
我们来看第二个问题和第三个问题,在执行context.publishEvent之后同样走到了AbstractApplicationEventMulticaster类下,你会发现它从我们之前的容器中取出了目前已经注册的所有监听器。也就是上面提到的ListenerRetriever类下的Set集合applicationListeners,然后遍历所有监听器,一个个判断和当前事件是否匹配。

/**
     * Actually retrieve the application listeners for the given event and source type.
     * @param eventType the event type
     * @param sourceType the event source type
     * @param retriever the ListenerRetriever, if supposed to populate one (for caching purposes)
     * @return the pre-filtered list of application listeners for the given event and source type
     */
    private Collection<ApplicationListener<?>> retrieveApplicationListeners(
            ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable ListenerRetriever retriever) {

        List<ApplicationListener<?>> allListeners = new ArrayList<>();
        Set<ApplicationListener<?>> listeners;
        Set<String> listenerBeans;
        synchronized (this.retrievalMutex) {
            listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
            listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
        }
        for (ApplicationListener<?> listener : listeners) {
            if (supportsEvent(listener, eventType, sourceType)) {
                if (retriever != null) {
                    retriever.applicationListeners.add(listener);
                }
                allListeners.add(listener);
            }
        }
/***************************省略,只关注主要的*************************/

注意这里有个supportsEvent方法,匹配对应监听器就是在这里做的。

    protected boolean supportsEvent(
            ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) {

        GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
                (GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
        return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
    }

注意这里在new GenericApplicationListenerAdapter(listener),还记得我们上面自定义的监听器PrintListener吗,把它传进去,目的就是获取泛型实际类型信息,也就是具体监听的事件类型。获取泛型用的是Spring4自带的工具类ResolvableType,这里不作过多描述,感兴趣的自己去了解。

但是,这是比较简单一种使用方式。通常我们不会这么做。
为什么呢?带着这个问题往下看。
我们一般使用@EventListener注解,如图,创建一个事件处理类,并交给spring容器管理,在要监听事件处理的方法上加@EventListener注解即可。

@Component
public class DemoEventHandler {

    @EventListener
    public void handle(DemoEvent event){
        System.out.println("调用MsgEvent的print方法输出其内容handler:");
        event.print();
    }
}

我们知道有一个EventListenerMethodProcessor类,这个类是application启动时自动注册执行的。该类的功能是扫描@EventListener注解并生成一个ApplicationListener实例。
其中有个这样的方法:就是用来扫描容器中bean的方法上所有的@EventListener,循环创建ApplicationListener。

protected void processBean(
      final List<EventListenerFactory> factories, final String beanName, final Class<?> targetType) {
 
   if (!this.nonAnnotatedClasses.contains(targetType)) {
      Map<Method, EventListener> annotatedMethods = null;
      try {
         annotatedMethods = MethodIntrospector.selectMethods(targetType,
               (MethodIntrospector.MetadataLookup<EventListener>) method ->
                     AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class));
      }
      catch (Throwable ex) {
         // An unresolvable type in a method signature, probably from a lazy bean - let's ignore it.
         if (logger.isDebugEnabled()) {
            logger.debug("Could not resolve methods for bean with name '" + beanName + "'", ex);
         }
      }
      if (CollectionUtils.isEmpty(annotatedMethods)) {
         this.nonAnnotatedClasses.add(targetType);
         if (logger.isTraceEnabled()) {
            logger.trace("No @EventListener annotations found on bean class: " + targetType.getName());
         }
      }
      else {
         // Non-empty set of methods
         ConfigurableApplicationContext context = getApplicationContext();
         for (Method method : annotatedMethods.keySet()) {
            for (EventListenerFactory factory : factories) {
               if (factory.supportsMethod(method)) {
                  Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName));
                  ApplicationListener<?> applicationListener =
                        factory.createApplicationListener(beanName, targetType, methodToUse);
                  if (applicationListener instanceof ApplicationListenerMethodAdapter) {
                     ((ApplicationListenerMethodAdapter) applicationListener).init(context, this.evaluator);
                  }
                  context.addApplicationListener(applicationListener);
                  break;
               }
            }
         }
         if (logger.isDebugEnabled()) {
            logger.debug(annotatedMethods.size() + " @EventListener methods processed on bean '" +
                  beanName + "': " + annotatedMethods);
         }
      }
   }
}

这样做有什么好处你应该知道了。如果要监听多个事件,你大可不必针对每个事件都写一个监听类。并且,要在多处监听同一个事件。这种方式灵活方便的多

优化完了监听的实现方式,同样可以优化发布事件。我们上面讲的发布事件是在项目启动的时候做的。那如果是在代码里,例如我们经常用到的异步事件编程。假设在我们在登入网站后,要经过一些与登入无关的操作,比如记录登入时间,地点信息。
在要发时间的地方实现ApplicationContextAware接口,获得ApplicationContext实例,通过context发事件。因为ApplicationContext实现了ApplicationEventPublisher接口,所以它有发事件的功能。

@Service("userService")
public class UserServiceImpl implements UserService,ApplicationContextAware{

    private ApplicationContext applicationContext;

    @Override
    public void login(User user) {
        //TODO 登入校验操作。。。。
        publish(new LoginEvent(new Object(),"我要登入了"));
    }

    public void publish(LoginEvent loginEvent){
        applicationContext.publishEvent(loginEvent);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
@Component
public class UserEventHandler {

    @EventListener
    public void handleLoginEvent(LoginEvent loginEvent){
        System.out.println("监听登入成功");
        //TODO 调用统计接口记录时间,地点
    }
}

...
阅读本文最重要的不是掌握怎么使用,我们应该去理解这种思想。CQRS架构中,读写分离中的写就是发布一个个命令来达到解耦的目的。消息机制中的mq,redis的发布订阅模式,webscoket的广播等等都是类似。同时,通过阅读源码我们也学会了如何获取标示该注解的所有方法,还有在初始化加载的时候,很多数据不是每次都要获取一遍,可以用map作缓存。
最后,提醒一下,不要看到事件就是异步的,要异步可以在处理方法上直接加@Async注解,如果同一个事件的监听器非常多。可以用线程池方式处理(SimpleApplicationEventMulticaster类下有个Executor变量,在广播事件方法multicastEvent中使用)

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

推荐阅读更多精彩内容