今天我们来谈一谈Java里面的Event事件机制。
程序的结构在不同的时代是在变化的。刚学编程序的时候,老师总会讲,程序是一个流程,线性执行,分支跳转,循环,有时候加上递归。我们总是能一步步跟踪下来,知道程序确切的运行次序。后来就会碰到事件的概念,程序先是准备好了,然后等待你做出某种反应,输入,键,鼠标,程序获取事件的消息,执行相关的动作。这种程序结构之下,不再有事先确定的运行次序了。刚开头遇到这种程序的时候,还真有点不习惯。印象最深刻的就是将近三十年前刚学到Windows编程的时候,看的第一个程序简直把人弄懵了。我把程序片段贴下来,你们当欣赏历史文物看看:
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow){
RegisterClass (&wndclass)
hwnd = CreateWindow( szAppName, // window class name
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0)) {
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){
switch (message) {
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
GetClientRect (hwnd, &rect) ;
DrawText (hdc, TEXT ("Hello, World!"), -1, &rect,DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
EndPaint (hwnd, &ps) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
你看上面的C程序代码,刚开头就是创建了一个窗口,然后一个死循环等待消息while (GetMessage (&msg, NULL, 0, 0)),消息来了就分派出去,交给相应的程序来处理。这个处理的代码就是WndProc(),里面根据不同的消息类型执行不同的动作(第一次瞪大了眼睛打着灯笼才终于看到了“Hello World!”)。
Windows上写一个Hello World就这么麻烦!但是它生命力顽强,最后成长为主流的模式。我想这种基于事件驱动的方式是一个主要的优势。程序像一个服务机构,客户有什么需求,发来消息,服务机构去处理。这种程序结构很贴近人的行为。
我们可以进一步联想到面向对象编程的范式,它认为一个系统是由一个个对象构成的,每个对象有自己的功能和数据,独立成单元,发生什么事件后,通过事件消息传递给别的对象进行通知处理。这个范式最后成为主流,现在几乎所有的系统都是按照这个范式构建的。
这种方式为什么好呢?理论上的研究表明,需求构成了“问题空间”,程序构成了“解决空间”,两个空间的相似度决定了解决的困难度。传统上,我们要解决一个问题,程序给我们提供的却是寄存器、内存、加法这一类机器的概念术语,所以那个时候将问题空间映射到解决空间的时候很复杂。而面向对象事件驱动的模型,跟现实世界的问题空间有一定程度的相似度,我们能够比较省力地进行映射,解决问题。这个现实世界,本就是一个一个主体客体构成,它们发生了什么事情,本来就是通过消息传递方式联系在一起的。
聊了这些理论上的东西,我们回过头来看Java是怎么弄的。在Java的事件概念中,有三个东西要了解,一个是事件源,一个是事件消息,一个是事件监听者。这三个词字如其意,不解释。流程是事件源发生了某事件的时候,发出事件消息,由事件监听者接受处理。事件从事件源到监听者的传送是通过对监听者的方法调用进行的(这一点有点意外,刚开头还以为是某种“消息传递”机制。这个机制是存在的,在探讨更大规模的软件结构的时候,会介绍到。我们现在讨论的是一种代码结构,还不是软件结构,更加不是系统结构。我这里用代码结构、软件结构、系统结构三个词表达不同层次的程序规模。)
我们先来看如何定义事件。在java.util中提供了EventObject,所有的事件都要继承它。我们自己来定义一个,代码如下(MyEvent.java):
package com.test;
import java.util.EventObject;
public class MyEvent extends EventObject {
private int state;
private String msg;
public MyEvent(Object source) {
super(source);
state = ((Source)source).getState();
msg = ((Source)source).getMessage();
}
public int getSourceState() {
return this.state;
}
public String getSourceMessage() {
return this.msg;
}
}
上面定义了一个由Source触发出来的事件MyEvent。构造函数可以有多个,我这里只用了一个默认的,传递的值是source。event中简单记录了状态和一条消息。
我们再看怎么定义事件监听者,代码如下(StateChangeListener.java):
package com.test;
import java.util.EventListener;
public class StateChangeListener implements EventListener {
public void handleEvent(MyEvent event) {
System.out.println(event.toString() + " fire event " + event.getSourceMessage());
}
}
按照要求,所有事件监听者要实现java.util.EventListener接口,这个接口是一个空的,只是一个标记(Tagging),习惯上会加一个handleEvent()方法,这个方法利用传递过来的事件消息进行相应处理。
好,现在我们提供一个事件源程序,代码如下(Source.java):
package com.test;
import java.util.EventListener;
import java.util.HashSet;
import java.util.Set;
public class Source {
private int state = 0;
private String msg = "";
Set<EventListener> listeners = new HashSet<EventListener>();
public void addStateChangeListener(StateChangeListener listener) {
listeners.add(listener);
}
public void notifyListener() {
for (EventListener listener : listeners) {
try {
((StateChangeListener)listener).handleEvent(new MyEvent(this));
} catch (Exception e) {
}
}
}
public void changeState() {
state = (state == 0 ? 1 : 0);
msg = "State Changed.";
notifyListener();
}
public int getState() {
return this.state;
}
public String getMessage() {
return this.msg;
}
}
Listener中有一个 HashSet结构,Setlisteners = new HashSet(); 将所有相关的Listener都存放在这里。一旦state状态发生了改变时,它会逐个触发listener,调用listener里面的handleEvent方法,即这一句:((StateChangeListener)listener).handleEvent(new MyEvent(this)); 从这代码,我们可以了解到,所谓的消息传递,其实内在的实现不过就是方法的调用。所以有些人坚持认为这不是事件机制。
最后,用一个测试程序测试上面的事件过程,代码如下(TestEvent.java):
package com.test;
public class TestEvent {
public static void main(String[] args) {
Source source = new Source();
source.addStateChangeListener(new StateChangeListener());
source.changeState();
}
}
测试程序有新建一个事件源,把listener进行注册,之后触发事件。运行结果如下:
com.test.MyEvent[source=com.test.Source@6d06d69c] fire event State Changed.
从设计模式的角度,上述介绍的Java事件处理机制是监听器Listener模式。我们还可以用观察者Observer模式来实现,把事件源当成消息发布者,监听者当成消息接收者。
消息发布者的代码如下(Publisher):
package com.test;
public class Publisher {
public EventRegistry events;
public Publisher() {
this.events = new EventRegistry("change");
}
public void change() {
events.notify("change");
}
}
这里我们用到一个注册表,集中存放事件对应的接收者。代码如下(EventRegistry.java):
package com.test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class EventRegistry {
Map<String, List<Subscriber>> subscribers = new HashMap<>();
public EventRegistry(String eventType) {
this.subscribers.put(eventType, new ArrayList<>());
}
public void subscribe(String eventType, Subscriber subscriber) {
List<Subscriber> users = subscribers.get(eventType);
users.add(subscriber);
}
public void notify(String eventType) {
List<Subscriber> users = subscribers.get(eventType);
for (Subscriber subscriber : users) {
subscriber.handle(eventType);
}
}
}
这个模式中还需要一个消息接收者,代码如下(Subscriber.java):
package com.test;
public class Subscriber {
public void handle(String eventType) {
System.out.println("changed.");
}
}
我们最后写一个测试程序,代码如下(Demo.java):
package com.test;
public class Demo {
public static void main(String[] args) {
Publisher publisher = new Publisher();
publisher.events.subscribe("change", new Subscriber());
publisher.change();
}
}
运行一下,结果changed.打印出来了。
两种设计模式各有有用的场景。程序结构上,稍微有一些差别。监听器模式有三种角色:事件源、事件和监听者。观察者模式有两个角色:发布者和接收者。
对模式的进一步了解,大家可以看设计模式的书。最经典的要数GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》,不过这本书对经验不丰富的人来讲不是很好懂,感觉比较抽象,要多读多练习。未来我会有讲座讲常见的十几种设计模式,并且会选用一些大家熟悉的程序片段。到那个时候,我们再细聊。
对于Java的事件处理,一般的书和文章上就介绍到这里,不过这是进阶讲座,我还想继续往下探讨。
在上面的代码里,监听器模式下,事件注册是由事件源本身来负责的,与之对应的,观察者模式下有一个EventRegistry单独注册事件。有单独注册机构会使得程序耦合度更低一点,结构更好一点。从人的行为来看,事件源就应该不负责监听者注册的,事件源就应该只管自己本身的业务逻辑,触发了事件后通知相关方即可。所以对各种教材和文章关于Java事件的介绍,我对这一点是很有意见的,他们都不提及这一点,把一堆注册代码放在事件源里面,把事件源弄得很重,结构比较难看。
天下文章一大抄,人云亦云,有价值的不多。二十年以来,Internet迅猛发展,获取信息空前便利,反面就是在信息的海洋中如何拾贝成了一个大问题,人们花了大量的时间去搜索,过滤无效的信息,被信息淹没了。更有甚者,很多人出于无知传播很多错误的知识,还有个别人为了利益故意散播假知识,
自古以来,都是劝告学习者博学强记,那是因为古代获取知识手段比较难,记录也不容易。而今天我们获取知识很简单了,记录也很容易了,在这种情况下,我们更要注意的是深思明辨慎取,集中精力和时间在有价值的信息中学习进步。宋代大儒王安石曾写道:此所以学者不可以不深思而慎取之也。
当然,这不是Java语言的问题,这些是程序结构和框架要解决的,我们就试着把上面的监听器模式修改一下,把监听器注册的机构单独拿出去,让程序结构更加清晰。
为了不跟上面的原始代码混在一起,我们都加上Ex前后缀。
先定义事件,代码如下(MyEventEx.java):
package com.test.event;
import java.util.EventObject;
public class MyEventEx extends EventObject {
private static final long serialVersionUID = 1L;
private int state;
private String msg;
public MyEventEx(Object source) {
super(source);
state = ((SourceEx)source).getState();
msg = ((SourceEx)source).getMessage();
}
public int getSourceState() {
return this.state;
}
public String getSourceMessage() {
return this.msg;
}
}
再定义一个新的事件源,代码如下(SourceEx.java):
package com.test.event;
public class SourceEx {
private int state = 0;
private String msg = "";
public ListenerRegistry registry;
public void fireEvent(String event) {
System.out.println("Source - fire event - notify listener");
registry.notifyListener(event, this);
}
public void changeState() {
state = (state == 0 ? 1 : 0);
msg = "State Changed.";
System.out.println("Source - "+msg);
fireEvent("change");
}
public int getState() {
return this.state;
}
public String getMessage() {
return this.msg;
}
}
跟以前不同,这一次改造之后,事件源相对比较简单,只用管自己的业务代码,监听器的注册工作交给ListenerRegistry。
好,我们来看ListenerRegistry.java:
package com.test.event;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ListenerRegistry {
Map<String, List<ExEventListener>> listeners = new HashMap<>();
public ListenerRegistry(String eventType) {
this.listeners.put(eventType, new ArrayList<>());
}
public void addListener(String eventType, ExEventListener listener) {
List<ExEventListener> users = listeners.get(eventType);
users.add(listener);
}
public void notifyListener(String eventType, SourceEx source) {
List<ExEventListener> users = listeners.get(eventType);
for (ExEventListener listener : users) {
System.out.println("Registry - listener");
try {
listener.handleEvent(new MyEventEx(source));
} catch (Exception e) {
}
}
}
}
这个注册机构还考虑了扩展性,可以根据不同事件名称,分别设置一组监听器来响应。这个实现借鉴了观察者模式。
我们还扩展了EventListener,不再只是一个tagging接口,增加了handleEvent()方法,代码如下(ExEventListener.java):
package com.test.event;
import java.util.EventListener;
import java.util.EventObject;
public interface ExEventListener extends EventListener {
void handleEvent(EventObject event);
}
定义一个具体的listener,代码如下(ExStateChangeListener.java):
package com.test.event;
import java.util.EventObject;
public class ExStateChangeListener implements ExEventListener {
public void handleEvent(EventObject event) {
System.out.println("listener-"+((MyEventEx)event).getSourceMessage()+" handled.");
}
}
最后,我们用一个测试程序把它们连起来,代码如下(TestEventEx.java):
package com.test.event;
public class TestEventEx {
public static void main(String[] args) {
SourceEx source = new SourceEx();
ExEventListener listener = new ExStateChangeListener();
ListenerRegistry registry = new ListenerRegistry("change");
registry.addListener("change", listener);
source.registry = registry;
source.changeState();
}
}
整个程序跟前面的基本一样,仅仅是结构上增加了一个Registry机构,让事件源变得简单。这么一个改动,让整个结构合理多了也好看多了,比较符合人的行为和本能的理解。好的结构总能给人一种愉悦感,把程序当成一件艺术作品去欣赏,去体会逻辑的精确之美和秩序之美。
好,以上就是Java事件处理的基本内容。我们还要往下扩展探讨。
我们看到了无论哪种方式,其实都是事件源调用监听者的相应方法。如果有多个监听者,进行一些复杂的处理,这个过程有可能是很耗时的。有些场景下,事件源不需要等待监听器处理完毕,那么我们可以考虑多线程的办法,将监听器放在单独的一个线程中执行。这就是大家说的异步的方式,还可以通过回调机制实现当监听者完成处理后进行相应动作。
我们按照这个思路修改一下程序。
先重新定义一个事件,代码如下(MyEvent2.java):
package com.test.event;
import java.util.EventObject;
public class MyEvent2 extends EventObject {
private static final long serialVersionUID = 1L;
private int state;
private String msg;
public MyEvent2(Object source) {
super(source);
state = ((Source2)source).getState();
msg = ((Source2)source).getMessage();
}
public int getSourceState() {
return this.state;
}
public String getSourceMessage() {
return this.msg;
}
}
为了能把监听者放到一个单独的线程里面,我们提供一个新的借口继承Callable。代码如下
(CallableEventListener.java):
package com.test.event;
import java.util.EventListener;
import java.util.concurrent.Callable;
public interface CallableEventListener extends EventListener, Callable<String> {
}
重新实现监听者,实现call()方法,代码如下(StateChangeListener2.java):
package com.test.event;
public class StateChangeListener2 implements CallableEventListener {
MyEvent2 event;
public StateChangeListener2(MyEvent2 event) {
this.event = event;
}
public String call() throws Exception {
System.out.println("Listener - Processing");
System.out.println("Listener - fire event " + event.getSourceMessage());
return "SUCCESS";
}
}
然后重写事件源,代码如下(Source2.java):
package com.test.event;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Source2 {
private int state = 0;
private String msg = "";
Set<Class<?>> listenerclss = new HashSet<Class<?>>();
public void addStateChangeListener(Class<?> listener) {
listenerclss.add(listener);
}
public void notifyListener() throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> returnList = new ArrayList<Future<String>>();
for (Class<?> listenercls : listenerclss) {
Class<?> partypes[] = new Class[1];
partypes[0] = MyEvent2.class;
Constructor<?> ct = listenercls.getConstructor(partypes);
Object arglist[] = new Object[1];
arglist[0] = new MyEvent2(this);
CallableEventListener eventlistener = (CallableEventListener)ct.newInstance(arglist);
Future<String> future = executorService.submit(eventlistener);
returnList.add(future);
}
for (Future<String> fs : returnList){
try{
while(!fs.isDone());
System.out.println("Source - "+fs.get());
}catch(Exception e){
e.printStackTrace();
}finally{
executorService.shutdown();
}
}
}
public void changeState() {
state = (state == 0 ? 1 : 0);
msg = "State Changed.";
System.out.println("Source - "+msg);
try {
notifyListener();
} catch (Exception e) {
e.printStackTrace();
}
}
public int getState() {
return this.state;
}
public String getMessage() {
return this.msg;
}
}
在新的实现中,我们的监听器注册表不再是对象,而是一个类。 见代码Set<class<class
当事件触发时,我们生成线程池,还用Future预备接受返回值。</class</class
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> returnList = new ArrayList<Future<String>>();
之后,利用reflection将生成监听器实例,并提交给线程运行:
CallableEventListener eventlistener = (CallableEventListener)ct.newInstance(arglist);
Future<String> future = executorService.submit(eventlistener);
好,有了这一些准备后,我们看最后的测试代码。代码如下(TestEvent2.java):
package com.test.event;
public class TestEvent2 {
public static void main(String[] args) {
Source2 source = new Source2();
try { source.addStateChangeListener(Class.forName("com.test.event.StateChangeListener2"));
} catch (ClassNotFoundException e) {
}
source.changeState();
}
}
对事件源动态注册监听器,事件触发后监听器异步响应,并可以拿返回值。这些目标都达到了。
运行结果如下:
Source - State Changed.
Listener - Processing
Listener - fire event State Changed.
Source - SUCCESS
到此为止,我们介绍了Java事件的基本概念,基本程序结构,监听器注册中心,异步处理等等。有了这些之后,我们其实可以把这些全部结合起来,设计一个事件处理框架,提供事件注册中心,支持不同的行为模式,如同步还是异步,选择监听器模式还是观察者模式,要不要Callback,规定事件响应配置文件等等。当然本讲座的任务不是设计框架,只是告诉大家这些知识点可以往那些地方延伸。这些事情其实是有人做过的,我们未来介绍框架设计的讲座里,会专门讲到Spring Event,并且会探讨如何自己从头设计事件处理框架。
框架不神秘,都是在实际的工程问题中产生的。这里面重点的学习方法就是扩展,看到一个点之后,往深处扩展,往广处扩展。观察一下数据量,个位数的时候这么写很好?数十个上百个的时候呢?千个万个的时候呢?看到写死的代码,就要问一句有没有办法不写死?有没有办法让用户配置?看到一个JVM内部组件之间的通信,就扩展想想JVM之间怎么通信。看到不同作用的代码写在一起,就试着分开不同的角色,各负其职,增加聚合度,减少耦合度。
人类社会的演化又何尝不是这样呢?远古时代一个家庭或者家族要负责所有的事情,捕鱼打猎种地纺织接生治病挖沟盖房找盐制造工具,后来出现了各种各样的职业,大家只做自己擅长的一摊。是这样构成了社会网络,互相支持,促进了文明的发展。现代社会分工是越来越细了。
经济学鼻祖亚当斯密在其名著《国富论》中把经济增长的原因归结为分工。这就是自由市场经济的理论基础,市场经济开创了人类社会的繁荣与活力。
前面提到过,系统实现的难度在于“问题空间”和“解决空间”之间映射的复杂度。抓住这个要领,思考人类社会本身作为一个系统是如何解决问题的,在面向对象编程范式之下,我们通常都能找到好办法解决问题。不同领域之间的交叉互动,经常会带来意想不到的收获,所以,现代社会,学者博闻广识仍然是一件大好事。
《诗经》里唱咏道:他山之石,可以攻玉。