Spring

Spring事件监听就是这么简单

2019-01-17  本文已影响0人  ZIxuAN_ae3f

听到监听这个词,不难理解,一个事物根据另一个事物的变化自发的作出响应,而且每次都作出同样的响应。就像点击按钮一样。每次点击登入按钮,都会访问登入接口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中使用)

上一篇下一篇

猜你喜欢

热点阅读