前言:设计模式,一个老掉牙的话题,一个永恒的话题。所有的Java开发,都必须经过的话题,都必须学习的话题。但是其实很多时候自己只是看了,了解了,然后忘了。所以还是想从头梳理一遍重要的设计模式,给自己看。不求逻辑多清晰,内容多完善,UML图画的多严谨,理解、会意就好。
在介绍观察者模式的最后(查看设计模式私人笔记-观察者模式),我们最后提到了观察者模式的局限性,并引入了两个新的模式,事件监听模式和发布订阅模式。
首先说明一点,我查看了不少资料,事件监听器模式和发布订阅模式都不属于经典的GOF模式。在我看来,这两种模式都是观察者模式的延展。 下面就对这事件监听模式做一个简单介绍,并和观察者模式进行一个对比。
事件监听模式,广泛的存在GUI,Servet,Spring等等地方。举一个简单的例子:
在Servet中,为我们提供了ServletContextListener接口,用于监听ServletContext上下文的创建事件。一旦Servlet上下文创建成功,Servlet容器就会对外抛出一个事件对象,告知所有注册在Servlet容器中的用于监听ServletContext相关事件的监听器。
例如上面代码,是经典的Spring容器加载的监听器,就是实现了ServletContextListener,要让监听器起作用,只需要配置在web.xml中即可:
这一步就相当于在注册监听器。
那么事件监听模式到底是怎么样的呢?在java.util包中,其实已经为我们做好了事件监听模式的相关一些支持。
这是第一个接口,用于标记事件监听器,类似观察者模式中的Observer接口,只是其中并没有定义任何方法,这仅仅只是一个标记接口。
这个类定义了一个事件对象,这个事件对象用于在被监听者和监听器之间传递数据。我们可以这样理解EventObject:
大家应该还记得这个观察者模式的类图,在观察者模式中,当主题状态变化的时候,由Subject调用notifyChange方法,在该方法中,依次调用Observer的update方法,而update方法中的第一个参数,Subject就是主题对象,也就可以理解为这里面的事件对象的事件源。意思就是之前是直接调用方法,而这里变成了传递一个事件对象。
我们可以来看看JAVA中自己对事件监听的使用模式。我们使用AWT中的例子来说明(没有用过也无所谓,看懂代码就行)。
首先来看awt中事件监听器的基础接口,ActionListener,在这里定义了actionPerformed方法,这个方法就是监听器监听到某个事件之后要做的逻辑。该方法传入一个ActionEvent对象,我们来看看这个事件对象的代码:
首先我们看到的是AWTEvent类,这个类是ActionEvent的基类。可以看到这个类继承了EventObject,并且在这个类中包含了一些自己的属性(状态),比如id(简单理解为事件序列号),consumed(是否被处理)等;关键的,在构造方法中,对事件对象本身拥有的属性做了初始化操作。
可以看到,ActionEvent其实和AWTEvent结构是一模一样的,只是它又多了几个这个事件相关的属性。并且同样也在构造器中进行了初始化操作。
事件相关的基础类看完了,我们来看下事件源,就是我们的主题对象,在swing中,有一个基础的类:组件(JComponent)定义了基础的组件,比如按钮,等;
我只列出了事件相关的主要代码,可以看到,在JComponent中,定义了一个EventListenerList对象,这个对象又是一个什么?
我仅仅也只列出了其中一点关键代码,可以看到其实EventListenerList里面维护了一个数组,而数组里面存放的就是监听器类型和对应的监听器实例,一组一组存放(在add方法中可以看到);
到具体的组件,比如按钮:
同样简化的代码,可以看到这里的处理方式。
· 按钮基类(AbstractButton)继承JComponent,所以他里面也维护一个EventListenerList;
· AbstractButton中定义了添加和移除ActionListener的代码;
· 当一个action施加在按钮上,会调用fireActionPerformed方法来处理,可以看到,该方法得到按钮上绑定的所有监听器,并筛选出监听ActionEvent的ActionListener,然后创建一个ActionEvent,并触发每一个ActionListener的actionPerformed方法,完成监听器的调用。
我们简单画一个图来说明这个过程:
这是三个组件之间的关系 下面是事件触发的说明图:
整个流程非常清晰了。
那简单的研究了Java中事件监听的使用方式,我们来使用事件监听来改写我们的案例。需求:假设现在有一个报价系统,当某个产品的价格发生变化时,就会通过短信发送给相关客户,也会通过邮件给该客户发送价格变动情况。
依葫芦画瓢,我们先定义我们自己的事件对象:
· 继承EventObject;
· 我们的事件是在传递一个改变的价格,所以这个价格应该包含在事件对象中;
· 事件监听器接口。注意,非常重要的一点,我们这个监听器定义出来之后,只能接收(监听)类型为PriceChangeEvent的事件。
接着让我们的两个监听动作实现PriceChangeListener
接下来完成我们的价格改变核心服务:
· 定义了一个listeners,用来持有当前注册的监听器;
· 这里只简单演示了一个addListener方法,再次注意,我的代码因为只监听价格变动事件,所以全部维护的都是PriceChangeEvent及PriceChangeListener,实际开发中,可能一个项目会有多种事件需要分别处理,那么架构上面会有变化。
· 重点提供了一个publishEvent,遍历注册的监听器,处理事件。这个方法类似观察者模式中的notifyObservers方法。
· 在priceChange方法中,一旦发现价格变化,我们就创建并抛出一个构造好的PriceChangeEvent,监听器就可以方便的感知了。
下面贴出测试代码:
至此,一个基本的事件监听模式代码完成。
谈谈两者的区别:
· 直观上的看,事件监听模式在观察者模式之上,增加了一个事件对象,把通过notifyObserver方法传递的内容包装到了事件对象中;
· Java中的事件监听模式更灵活,因为Observable是一个类,并且如何维护注册的观察者的相关代码,都已经实现了,其中的关键方法setChanged还是protected的,在代码结构上不够灵活;
· 从代码实现上来看,观察者模式比事件监听模式更简单,角色更少。
· 从扩展性来说,事件监听模式更容易扩展,特别是涉及到事件传播,事件分类处理这种情况。
说到事件监听,我们真不用自己去处理,Spring已经为我们完美的处理了事件监听模式相关内容。下面我们来看看Spring中的事件监听模式。
首先来看看Spring中事件机制的模型:
可以看到,整个Spring的事件机制结构还是比较简单的,主要有以下几个角色:
· ApplicationEvent:Spring中所有事件对象都继承自ApplicationEvent。
· Spring中提供了两种事件类型,第一种是Spring容器自己的事件,一种是用户自定义的事件。Spring中是根据事件类型来区分不同的事件处理。
· Spring中通过ApplicationEventPublisher接口来发布事件,类似前面PublicPriceService中的publishEvent方法:
· Spring的最强大容器接口ApplicationContext实现了ApplicationEventPublisher接口,并在AbstractApplicationContext类中实现。代码如下,可以清楚的看到,最终调用了ApplicationEventMulticaster来发布消息。
· ApplicationEventMulticaster把责任分离出来,专门用于负责事件监听器和事件发布相关处理,我们来看看该接口一些重要方法:
最后,我们来看看Spring中事件监听器的使用。这里,我们就使用Spring来完成之前的示例。Spring中使用事件监听,有两种使用方式:
· 通过实现ApplicationListener,并配置完成;
· 通过@EventListener标签完成;
首先我们来创建对应的价格变动事件对象:
然后我们定义价格变动业务类:
到这里,两种方式都是相同的,主要的区别还是在监听器的实现上。我们先看第一种,实现ApplicationListener的方式:
注意,需要添加@Component标签,把监听器注册到Spring容器中;
最后,来完成测试:
我们测试代码直接使用Spring的Javaconfig+springtest完成的。
好,接下来我们看看使用@EventListener来完成的例子。PriceChangeEvent,PublicPriceService和我们的测试代码都是相同的,只是需要修改一下两个监听器的代码:
注意几个点:
· 需要作为事件监听器处理的逻辑直接写到方法里面,方法上面打上@EventListener标签即可;
· 方法需要传入一个ApplicationEvent类型,相当于声明了一个ApplicationListener的类;
· 使用@EventListener的好处在于,可以极大的减少监听器类的数量,非常方便管理。在一个类中,可以把大部分的监听器逻辑都完成。但是问题在于插拔不太方便,而且这个类也容易出现大量代码。
· 可以在方法上同时配合@Async标签,将事件监听器直接变为异步消息处理。当然,同时需要在PriceListener上添加@EnableAsync注解。
如果项目依赖Spring,那么遇到类似的情况,我建议直接使用Spring的事件机制来完成即可。如果仅仅是一个很小的项目,其中一个地方需要使用到观察者模式,建议可以使用Java提供的Observer和Observable;如果觉得Java提供的观察者模式在性能上,或者实现上不能满足要求(比如提到过Java实现的观察者模式,可能出现刚添加的观察者无法接收到消息的情况),可以自己使用Java提供的事件机制来完成。
说到发布-订阅模式和观察者模式,马上会想到head first设计模式里面的一句话:
出版者+订阅者=观察者模式
那么,究竟发布-订阅模式和观察者模式是否相同呢?
我们来看一下,标准的发布-订阅模式的核心结构:
1,Publisher,发布者,作为事件源,可以看做观察者模式中的subject;
2,发布者发出一个消息;但是注意对比观察者模式,观察者模式中,主题对象是持有注册的监听者的列表的,由主题对象负责通知监听者。而发布-订阅模式,很明显可以看到,在Publisher后增加了一个中间层,姑且叫做Borker(信息代理);Publisher产生的消息都先扔给Borker;
3,消息分发给信息代理,由信息代理根据消息的内容,将消息分配到不同的topic(主题)上;
4,Subscriber,订阅者,作为监听者,将自己注册在不同的主题上;
5,当该主题产生消息,就会把该消息分配给该主题上订阅的Subscriber执行消息。
综上流程,可以看到,发布-订阅和观察者模式,最大的区别在于,发布-订阅模式中的发布者和订阅者是完全松耦合的,彼此不知道对方的存在。发布者发布了一个消息到某个主题上,如果该主题没有被订阅,那么该消息也不会被消费。
除了常见的JMS消息框架,ActiveMQ等;其实我们对比事件监听模式可以发现,在Spring中的时间监听模式,实际上也可以看做一种发布-订阅模式的实现。
看这个Spring的事件监听图,Spring的事件发布者(即调用了ApplicationEventPublisher.publish方法或者Spring容器本身的事件发布),并不知道监听器的存在,因为ApplicationContext中并没有直接对ApplicationListener的引用,而是通过ApplicationEventMulticaster来负责消息的处理的。那么,这个ApplicationEventMulticaster就可以理解为发布-订阅模式中的Broker。
虽然在Spring中没有直接提供Topic的概念,但是实际上Spring完全可以通过ApplicationEvent的类型来作为topic。某一种类型的ApplicationEvent就是一个主题。比如所有监听ContextRefreshEvent的ApplicationListener就是订阅了ContextRefreshEvent这个主题,这个就涉及到了主题的过滤。
在Spring中,是怎么完成主题的过滤和分发呢?
1,注册监听器。
在调用ApplicationEventMulticaster.addApplicationListener方法注册的时候,我们来看看关键代码:
可以看到,首先同步retrievalMutex成员变量,然后使用defaultRetriever的applicationListeners添加监听器。
我们可以看到这两个变量究竟是什么,retrievalMutex仅仅是对defaultRetriever的引用,就是用来做同步用的;而defaultRetriever实际上是一个ListenerRetriever,而这个ListenerRetriever是AbstractApplicationEventMulticaster声明的一个内部类,我们看下关键代码:
可以看到,实际上,真正的监听器实例都是放在ListenerRetriever中的applicationListeners中的。
2,发布消息。
发布消息,实际上调用的是SimpleApplicationEventMulticaster,该类继承于AbstractApplicatinoEventMulticaster。其中的最重要的方法就是multicastEvent:
该方法接受一个ApplicationEvent参数和一个ResolvableType参数,当然,我们平时开发中更多调用的是:
其实第二个ResolvableType参数为空。
· 首先根据ApplicationEvent对象,获取其泛型类型,包装为一个ResolvableType;比如我们的
其对应的ResolvableType的目标类型为PriceChangeEvent;
调用getApplicationListeners获取订阅了该类型(就是上面说的主题)的监听器:
可以看到,这段代码最重要的就是使用了缓存来缓存已经查找出来的事件类型-监听器队列;该缓存的定义:
final Map retrieverCache = new ConcurrentHashMap(64);
那么,我们对AbstractApplicationEventMulticaster的理解应该更深刻一些:
· 1,defaultRetriever,这个ListenerRetriever中存放的是所有的监听器;
· 2,retrieverCache,这里面缓存的是应过滤之后的事件类型-监听器队列;
接下来要看的就是真正去执行事件类型-监听器队列匹配的retrieveApplicationListeners方法:
该方法做了简化,代码很简单,执行匹配,如果要缓存,放到传入的retriever中;最后来看看方法中的匹配方法supportsEvent:
代码很简单,如果是监听器实现了GenericApplicationListener或者SmartApplicationListener,都认为匹配成功(因为这两个事件监听器接口,是在实现类中去判断事件是否匹配,所以这里必须匹配成功,才能把所有事件全部传给他们处理);否则直接判定事件泛型类型是否匹配。
至此,完成整个按照事件类型过滤和分发的过程。
如果是普通的集中式应用内部的发布-订阅,我建议使用Spring的事件监听即可,一般情况下使用默认的通过事件类型进行发布订阅即可,如果有特殊订阅模式,也可以自己通过实现GenericApplicationListener或者SmartApplicationListener再做二次分发,也支持异步处理;
如果是分布式环境,那就只能使用第三方消息中间件了。