每个JAVA开发人员都熟悉异步事件下发的EventListener模式。许多人也写过用来管理侦听器和下发事件给其他组件的样板代码。侦听器是简单、通用、灵活和容易实现的,但他涉及到其他开发人员写的代码,这可能引起问题:
1、一个低效的侦听器可能花费太长的时间来处理事件,这使得其他侦听器等待并可能引起死锁。
2、在你派发事件给侦听器器的时候,你可以控制下发的线程。但通常实现侦听器的开发人员不能控制事件如何被下发到他们的代码。
3、在一个多媒体应用中,如何同步GUI事件(如动画和用户交互)和其他异步事件流(如语音、声音和视频)不那么清晰。
这篇文章使用Doug Lea的Executor(现在是J2SE 5.0的一部分)使得事件派发更加灵活。你可以使用这种思想在以下方面:
1、允许开发人员使用你的组件作为事件派发策略的插件,这样就可以自定义事件的派发方式。
2、使不同的侦听器相互独立,因此一个低效的侦听器就不会影响其他的了。
3、多元化来自不同异步资源的事件流。
JAVA1.4兼容性注意:这篇文章使用J2SE 5.0的泛型来排除事件和侦听器器的转换。Dispatchable Event Library也包含一个非泛型的版本可以运行在JDK 1.4上。较早的JDK缺少内建的java.util.concurrent包,但你可以下载兼容的后续支持版本。
标准侦听器
在本文示例中,会编写一个使用ClipEven类也报告视频片断事件(如启动和暂停等)和实现ClipListener接口响应事件的视频编辑系统。
import java.util.EventListener;
class ClipEvent extends EventObject {
//...}public interface ClipListener extends EventListener {
public void clipUpdate(ClipEvent e);}
在许多程序中,事件是由一些控制类来创建,他们不仅负责维护相应的侦听器而且派发每一个事件给相应的侦听器。通常,我们可以分离这职责并且将侦听器的维护代理给一个简单的助手类dispatcher。ClipEventDispatcher示例了这种事件派发的方式:
import java.util.*;public class ClipEventDispatcher {
final Collection listeners = new ArrayList();
public synchronized void
addListener(ClipListener l) {
listeners.add(l);
}
public synchronized void
removeListener(ClipListener l) {
listeners.remove(l);
}
public void fireEvent(ClipEvent e) {
for(Iterator i = copyListeners();
i.hasNext();) {
ClipListener l = (ClipListener) i.next();
l.clipUpdate(e);
}
}
private synchronized Iterator copyListeners() {
return new ArrayList(listeners).iterator();
}
}
ClipEventDispatcher暴露出如前面所讨论的典型的下发问题。如果任何一个ClipListener有较慢的clipUpdate方法实现就会导致其他侦听器等待。派发器的作者决定哪一个线程调用fireEvent方法,而ClipListener的开发人员却没有办法自定义事件的下发。
JAVA中灵活的任务执行:Executor接口
J2SE 5.0标准化了java.util.concurrent包,包含来自Doug Lea创建的Executor接口。Executor运行实现了java.lang.Runnable接口的任务。
class MyCoolTask implements Runnable {
public void run() {
//... do useful stuff
}}Thread t = new Thread(new MyCoolTask());t.start();
在Executor使用Runnables是类似的:
Executor e = ...e.execute(new MyCoolTask());
转递给execute方法的Runnable任务包含被Executor调用的run方法。但不像Thread只可以调用启动方法一次,Executors可以运行许多Runnable任务。不同的Executors体现不同的执行任务的策略。例如,J2SE 5.0提供一个Executor作为调度器,这意味着他可以按照配置的时间周期性地运行任务。在下一页的Useful Executors部分详细描述了几种不同的Executor,但首先我们来看一下如何用他们来解决事件派发问题。
用DispatchableEvent增加灵活性
为了使组合Executor和事件更容易,我开发了一个Dispatchable Event Library,他提供了助手类DispatchableEventSupport(用来维护侦听器和事件派发)。在内部,DispatchableEventSupport实例使用一个Executor来触发事件,因此可以改变Executor来自定义事件下发策略。
下面是一个使用DispatchableEvent类库来重写的ClipEventDispatcher示例:
import org.recoil.pixel.dispatchable.*;import org.recoil.pixel.executor.*;public class ClipEventDispatcher {
Executor e = new DirectExecutor(); //[1]
DispatchableEventSupport<ClipListener d =
new DispatchableEventSupport<ClipListener(e);
public void addListener(ClipListener l) {
d.addListener(l);
}
public void removeListener(ClipListener l) {
d.removeListener(l);
}
public void fireEvent(ClipEvent e) {
d.fireEvent(new DispatchableEvent
<ClipListener, ClipEvent(e) {
public void
dispatch( ClipListener l, ClipEvent ce) {
l.clipUpdate(ce); //[2]
}
});
}}
在行[1]上我们使用DirectExecutor来简化重建原始的ClipEventDispatcher行为。事件下发可以通过变化使用的Executor来自定义,或者在DispatchableEventSupport被创建时或者在侦听器增加时。
在行[2]上你只需要如此简单的代码来集成到你的应用中。Dispatchable Event Library处理了事件下发的机制,通常你所需要做的只是调用的回调函数(如clipUpdate)
Dispatchable Event Library详解
Dispatchable Event Library包含几个有用的助手类来派发任何类型的事件。关键的几个类在org.recoil.pixel.dispatchable包中:
DispatchableEventDispatcher:使用Executor触发事件,但不提供侦听器的维护。在你想为现有的事件派发代码增加灵活性是非常有用。
DispatchableEventSupport:大部分应用想要使用这个助手类,他为DispatchableEventDispatcher增加了侦听器维护。如果你了解java.beans.PropertyChangeSupport你会觉得他也很熟悉。
PropertyChangeEventDispatcher:组合了DispatchableEventDispatcher
和PropertyChangeSupport,为PropertyChangeEvents提供了灵活的派发策略。这也是一个研究如何将DispatchableEvents与现有代码集成的好例子。
DispatchableEvent:用来扩展你的事件下发代码的抽象类。
有用的Executors
Dispatchable Event Library的力量来自可以被用来自定义事件下发的可用Executors。下面我来看一下可用的Executors组:
Dispatchable Event Library包含org.recoil.pixel.executor包:
DirectExecutor:DirectExecutor在同一线程内同步调用提供给他的代码。如果和DispatchableEventSupport一起使用这个类,你可以得到通用的侦听器行为,这也是一个有用的缺省值。
AWTExecutor:AWTExecutor在AWT事件派发线程的调度代码。事件与AWTEvents交互。因此,由这个Executor调用的侦听器可以自由地调用更新AWT和Swing GUI组件的方法而不需要使用SwingUtilities.invokeLater(),因为他们已经在正确的线程中被子调用。
MIDPExecutor:MIDPExecutor在J2ME MIDlet中与AWTExecutor一致。他确保你的事件通过需要与MIDlet's GUI交互的callSerially方法下发。
例如,为了在AWT事件派发线程中使用AWTExecutor来下发ClipEvents:
import org.recoil.pixel.dispatchable.*;
import org.recoil.pixel.executors.*;
Executor e = new AWTExecutor();DispatchableEventSupport<ClipListener d =new
DispatchableEventSupport<ClipListener(e);
J2SE5.0内建的Executor
新的J2SE 5.0类java.util.concurrent.Executors被用来创建复杂的线程池。你可以在池中配置线程数量,设置延迟或者周期调度。
例如,使用J2SE 5.0 Executor提供一个容纳5个事件下发线程的线程池
import org.recoil.pixel.dispatchable.*;
import java.util.concurrent.*;
Executor tp = Executors.newFixedThreadPool(5);
DispatchableEventSupport<ClipListener d = new
DispatchableEventSupport<ClipListener(tp);
J2EE并没有提供标准的线程池功能,但Executor可以通过JMS或者消息BEAN来实现提供可配置的事件下发。
问题解决
现在我们已经看到Dispatchable Event Library和一些Executors,我们可以看一上如何使用这些工具来避免常见的侦听器问题。
避免等待
DispatchableEventSupport提供2个addListener方法来避免侦听器等待问题:
public void addListener(L listener); public void addListener(L listener,Executor executor);
addListener(L listener)方法在DispatchableEventSupport被创建的时候共享默认的Executor集合。而addListener(L listener, Executor executor)方法关联自定义的Executor。
这种方式不仅为组件的使用者提供了自定义事件下发的一种好的方式,而且帮助他们通过只有2个参数的addListener方法来分离侦听器。
import org.recoil.pixel.dispatchable.*;public class SharedComponent {
DispatchableEventSupport<ClipListener d =
new DispatchableEventSupport<ClipListener();
public void
addListener(ClipListener l, Executor e) {
d.addListener(l, e);
}
public void fireEvent(ClipEvent e) {
d.fireEvent(new DispatchableEvent
<ClipListener, ClipEvent(e) {
public void
dispatch( ClipListener l, ClipEvent ce) {
l.clipUpdate(ce);
}
});
}
[...]}
给SharedComponent增加侦听器的开发人员被强制为每一个侦听器定义一个Executor。假设每一个开发人员保持Executor为私有的,那么他的侦听器就有一定的分离量。这在他们使用基于线程池的Executor时非常有用。
如果所有相关的代码都在团队的控制下,那么SharedComponent是足够的,但这还不能完全解决等待问题。如果你因为使用遗留的或第三方代码而必须支持低效的侦听器时,你可以通过控制和强制每一个侦听器拥有自己的Executor来增加相互的独立性。
import org.recoil.pixel.dispatchable.*;
import java.util.concurrent.*;
public class DefensiveComponent {
private final
DispatchableEventSupport<ClipListener d =
new DispatchableEventSupport<ClipListener();
public void addListener(ClipListener l) {
Executor e=Executors.newSingleThreadExecutor();
d.addListener(l, e);
}
public void removeListener(ClipListener l) {
d.removeFirstInstanceOfListener(l);
}
public void fireEvent(ClipEvent e) {
d.fireEvent(new DispatchableEvent
<ClipListener, ClipEvent(e) {
public void
dispatch( ClipListener l, ClipEvent ce) {
l.clipUpdate(ce);
}
});
}
[...]}
DefensiveComponent为每一个增加的侦听器附加对应的事件下发线程,这就分离了低效的侦听器并且确保侦听器可以被独立的处理;高效的侦听器不需要等待低效的。这种策略是简单而安全的,但也是高代价的,因为他必须创建和销毁许多线程。在大部分情况下,需要通过Executors创建合理大小的线程池也平衡独立性和代价。
同步多事件流
DispatchableEvent允许你通过一个简单的Executor多元化相应事件来同步来自不同异步资源的事件。
例如,考虑一个支持鼠标和语音识别的多模画板应用。
通常语音识别在一断语音被识别时派发一个事件。想像用户选择一个图形然后说“删除”。显然我们希望鼠标事件被首先处理,否则可能会删除错误的对象。一种简单地解决这个问题的方法是使用AWTExecutor来下发语音事件,他会在事件被收到时将其放在AWT事件队列中,确保首先处理MouseEvents。
这个想法可以扩展到更多的异步事件流,通过将每一个事件源作为引用放到一个共享的基于队列的Executor中。每一个事件根据顺序放在队列中,交叉地下发。
小结
这篇文章专注于可能发生在侦听器范例中的问题。我们看到一个简单的派发类库如何通过Executors被用来自定义事件下发。使用不同的策略你可以将你的组件与子系统(如AWT)集成,你可以通过允许客户定义使用的Executor来给予他们更多的选择,或者你可以从另一方面来分离低效的侦听器来防止等待。