JSF 2.0阅读笔记:事件(一)

本文详细介绍了JavaServer Faces (JSF) 中的事件机制,包括事件的类型、监听方的注册方式、现场信息的传递方式等五个核心要素,并深入探讨了JSF2.0中的FacesEvent、PhaseEvent和SystemEvent等事件类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、什么是事件

什么是事件?简单来说,就是一些由当前程序关注范围之外的原因发起的一些行为(action),而你在程序内部需要对这些行为作出响应和处理。利用事件,能够有效地缩小程序片段所需要关注的范围,也就是减少了程序员在开发当前程序片段时的关注点,实现传说中的高聚合和松耦合。一个事件体系中包含了两个部分,[b]事件的发起方[/b]和[b]事件的监听方[/b](处理方)。在开发事件发起方时,可以完全不知道事件监听方的任何细节,而只需要针对事件体系本身定义五个要素:[color=red]事件的类型、监听方的注册方式、事件触发时现场信息的传递方式、事件的触发和广播时机、监听器返回处理结果的方式[/color]。在开发监听方时,必须清楚以上五个要素,在正确的事件类型上注册监听器,根据现场信息进行恰当的处理,最终向事件发起方提供处理结果,以影响事件发起方的后续处理。换句话说,事件体系为事件的发起方和监听方规定了一种标准化的沟通方式,避免了它们之间互相直接依赖对方的实现细节。

在考察一个事件模型时,我们通常也是从这五个要素着手的:

[b]1. 事件的类型。[/b]概念上来看,事件的类型就是事件的名称,它通常反映了事件触发的时机。例如说,组件上的onclick事件表明了这个事件会在组件被点击时触发。触发事件的主体(例如Button组件),称为事件源。一个事件源有可能触发多种事件,同一种事件也由可能被多个事件源所触发,但对于一个具体的事件,其事件源只有一个。

[b]2. 监听方的注册方式。[/b]为了建立起事件发起方与监听方的关联,事件发起方需要提供一种或多种方式,让监听方注册监听(回调)方法,当事件触发时,事件发起方依次调用已注册的监听方法,通知监听方进行处理。在Java中回调方法必须包装为监听接口的实现类,通常命名为为监听器(Listener)。一个事件上往往可以注册多个监听器,事件触发时这些监听器会被依次通知,因此这种依次触发多个监听器的行为也称为事件的广播(broadcasting)。

[b]3. 事件触发时现场信息的传递方式。[/b]在编写监听器逻辑时,往往需要知道触发事件时在事件发起方的一些上下文信息。有时,监听方能直接获取到发起方的当前上下文信息,因此无需发起方作特别的处理。但大多数情况下,为了方便编写监听器代码,或者应付监听方无法获取到发起方当前上下文的场景,通常会由发起方在触发事件时收集与事件相关的上下文信息,以某种方式传递给事件监听方。这种事件现场信息的传递,通过回调方法上的参数传递方式进行。为了对发起方和监听方的接口依赖进一步解耦,我们往往会对事件现场信息进行抽象,封装为事件类(Event Class)。事件类(携带事件的现场信息)与事件的类型(反映事件触发时机的名称)往往是一一对应的,因此在许多事件编程模型中,会使用事件类来标识对应的事件类型。但应该明确其中的区别,事件的类型是概念上的东西,它的标识方式可以是事件类,也可以是字符串,或者数字。而事件类的实例则是实实在在的事件现场信息。

[b]4.事件的触发和广播时机。[/b]一些简单的事件机制,会在事件触发时立刻创建出事件类实例,然后直接调用监听器的回调方法来广播事件,这种情况我们可以认为事件的触发与广播的时机是一致的。对于一些复杂的场景,例如事件发起方与监听方的处理需要异步或延时进行的时候,就需要引入事件的排队机制。在事件触发时,只是将事件类实例加入队列,在合适的时机再依次广播。

[b]5. 监听器返回处理结果的方式。[/b]灵活的事件发起方允许监听方在处理完毕后,向其返回一些信息,以影响事件发起方的后续处理流程。监听器返回处理结果的方式通常有两种,一是修改某些由双方共享的上下文信息(例如在监听方法中调用FacesContext.responseComplete()通知JSF引擎跳过后续处理),二是通过回调方法的返回值(例如在action方法中返回字符串通知JSF引擎导航到另一页面)。具体的方式和语义,是由事件发起方来定义的。

对于UI控件的事件机制,我们还会关注事件广播时多个针对该事件的监听器的处理顺序。但对于没有明确父子嵌套关系的监听器体系(例如JSF中),我们通常不关心,也无法精确地确定同一个事件的多个监听器的执行顺序。因此这不在本文讨论的范围。

下面,就以这五要素为主线,来看看JSF2.0中的事件体系。

[b]二、JSF2.0中的事件[/b]

在JSF1.2中,已经提供了两类事件,[b]FacesEvent[/b][color=blue](重复一下,FacesEvent是事件类,其实例将携带某次具体事件的现场信息,在这里则用来标识一种事件类型)[/color]与[b]PhaseEvent[/b]。在JSF2.0中,又再加入了一类新的事件,[b]SystemEvent[/b]。

在JSF2.0的规范文档(JSR314)的Sec3.4.1中,给出了事件模型的API总图,如下:

事件类静态类图

[img]http://dl.iteye.com/upload/attachment/383426/761cb12c-0ba2-3b4f-b265-0d73217ce004.png[/img]

监听器类静态类图

[img]http://dl.iteye.com/upload/attachment/383428/e0f2e050-d2ea-3908-bd79-5acb4bdba8df.png[/img]

[b]2.1 FacesEvent[/b]

FacesEvent这个术语不太好翻译,鉴于FacesContext可以理解为JSF的应用上下文,而且FacesEvent的两个主要子类:ActionEvent与ValueChangeEvent都是在InvokeApplication阶段触发的,我们也可以把FacesEvent理解为[b]JSF的应用事件[/b]。事实上,在JSF2.0规范中,关于FacesEvent的章节(Sec3.4.2)标题就是《Application Events》.

FacesEvent是JSF组件体系的一个重要组成部分,它是一类由组件发起的事件。我们知道,面向组件开发的一个重要特征就是把UI分离为展现与行为两部分,其中UI行为就以组件事件的方式来体现。具体到JSF中,就是FacesEvent。在JSF中,FacesEvent抽象类和相关的处理逻辑构成了组件事件的整体框架。并且基于该体系,提供了ActionEvent与ValueChangeEvent两套实现,供应用开发程序员使用。下面我们先来看一下最基本的FacesEvent的五要素:

[b][color=blue]a. 事件类型[/color][/b]
FacesEvent。这是一种依附在组件上,为组件服务的事件。其事件源为组件实例。

[b][color=blue]b. 监听方注册方式[/color][/b]
通过调用组件实例上的addFacesListener方法注册监听器。监听器必须实现FacesListener接口。默认情况下,注册到组件上的监听器会被看作是组件的附加对象(AttachedObject),在ViewState中保存状态。因此FacesListener的实现类[color=red]必须有一个public的无参构造器[/color],有状态的监听器应该实现StateHolder接口。针对不同的FacesEvent子类(见下文c),可能需要特定的FacesListener具体类与之对应。因此在UIComponent.addFacesListener()方法上,用javadoc建议具体的组件实现应该提供强类型的接口方法,接受具体的FacesListener子类作为参数。
[quote]
protected abstract void addFacesListener(FacesListener listener)

Add the specified FacesListener to the set of listeners registered to receive event notifications from this UIComponent. It is [color=blue]expected[/color] that UIComponent classes acting as event sources will have corresponding [color=blue]typesafe[/color] APIs for registering listeners of the required type, and the implementation of those registration methods will delegate to this method. For example:

public class FooEvent extends FacesEvent { ... }

public interface FooListener extends FacesListener {
public void processFoo(FooEvent event);
}

public class FooComponent extends UIComponentBase {
...
public void addFooListener(FooListener listener) {
addFacesListener(listener);
}
public void removeFooListener(FooListener listener) {
removeFacesListener(listener);
}
...
}

[/quote]

[b][color=blue]c. 现场信息的传递方式[/color][/b]
通过抽象类FacesEvent的具体子类实例携带。抽象类FacesEvent上只携带了两样信息:[color=blue]作为事件源的UIComponent实例[/color],和[color=blue]指定该事件应该在哪个JSF请求处理生命周期阶段进行广播的phaseId[/color]。其他信息则由具体的子类进行扩展。具体子类还应该实现以下抽象方法:
[code]
public abstract boolean isAppropriateListener(FacesListener listener)
判断传入的FacesListener子类是否能用于处理本事件,例如可以通过instanceof判断传入的FacesListener是否所需的特定子类型。

public abstract void processListener(FacesListener listener)
向传入的listener广播事件。注意FacesListener本身是个标志接口,不提供实际的回调接口。在本方法中需要把传入的FacesListener实例向下转型,再调用其中的回调接口方法。
[/code]

在概念上,编写FacesEvent子类的程序员必须清楚与之对应的FacesListener具体子类的接口细节。

[b][color=blue]d. 事件的触发与广播时机[/color][/b]
FacesEvent依附于组件,因此触发FacesEvent的前提是组件实例已经创建好,组件树已经成型,也就是JSF生命周期的“[color=green]Restore View[/color]”阶段完成之后。在“[color=green]Restore View[/color]”后的任何阶段中,都可以调用组件上的queueEvent方法来触发一个FacesEvent事件。顾名思义,调用该方法并不会立刻广播事件,只是把FacesEvent的实例加入到一个队列中。在该实例的getPhaseId()所指定的JSF生命周期阶段处理执行完毕后,才广播该事件。在JSF的六个生命周期阶段中,“恢复视图”阶段时组件树还没有建立起来,“渲染阶段”阶段执行完毕后组件已经废弃了,因此事实上会广播FacesEvent的阶段只有中间的四个:"[color=green]Apply Request Values[/color]","[color=green]Process Validations[/color]","[color=green]Update Model Values[/color]",与"[color=green]Invoke Application[/color]"。

[b][color=blue]e. 监听器返回结果处理[/color][/b]
FacesEvent是比较抽象的组件事件机制。在FacesEvent接口上甚至没有定义回调接口,一切都留给子类去扩展。自然在规范中也没有对FacesEvent回调接口的返回值作出规定和限制。在后面关于FacesEvent的子类ActionEvent的讨论中,可以看到回调接口返回值的例子。当然,根据JSF规范,在监听器的处理逻辑中,随时可以根据需要调用FacesContext.renderResponse()通知JSF引擎在处理完本阶段后直接进入渲染阶段。也可以调用FacesContext.responseComplete()方法通知JSF引擎在处理完本阶段立刻终止本次请求处理生命周期。

作为参考,我们可以进一步关注一下JSF 2.0 API中关于FacesEvent的实现细节。

通过阅读JSF2.0 API代码,我们可以发现关于FacesEvent的实现代码主要由组件的公共基类UIComponentBase与组件树根节点类UIViewRoot来共同负责的。

UIComponentBase负责维护一个FacesListener的列表,用于记录注册到组件上的FacesListener。组件类上提供broadcast(FacesEvent event)方法,用于向该组件的所有FacesListener广播一个FacesEvent。其默认实现也由UIComponentBase提供。

public void broadcast(FacesEvent event)
throws AbortProcessingException {

if (event == null) {
throw new NullPointerException();
}
if (event instanceof BehaviorEvent) {
BehaviorEvent behaviorEvent = (BehaviorEvent) event;
Behavior behavior = behaviorEvent.getBehavior();
behavior.broadcast(behaviorEvent);
}

if (listeners == null) {
return;
}

for (FacesListener listener : listeners.asArray(FacesListener.class)) {
if (event.isAppropriateListener(listener)) {
event.processListener(listener);
}
}
}

其中关于BehaviorEvent的部分我们暂时不管(关于Behavior体系将另文讨论)。可以看到广播事件的高层逻辑非常简单,依次判断每个监听器是否适用于该事件,然后调用event上的processListener方法来实际调用监听器上的回调方法。

UIComponentBase只关心组件自身的事件广播。而组件树中所有组件的事件广播,则由UIViewRoot负责统一协调。

首先,我们来看UIComponentBase上用于触发事件的queueEvent方法的默认实现
[code]
public void queueEvent(FacesEvent event) {

if (event == null) {
throw new NullPointerException();
}
UIComponent parent = getParent();
if (parent == null) {
throw new IllegalStateException();
} else {
parent.queueEvent(event);
}
}
[/code]
可以看出,普通组件自身并不维护通过queueEvent方法排队FacesEvent实例,而是把它委托给在组件树中的父组件处理。这样层层委托,最终就传递到了UIViewRoot的queueEvent方法中。再看UIViewRoot的queueEvent方法
[code]
public void queueEvent(FacesEvent event) {

if (event == null) {
throw new NullPointerException();
}
// We are a UIViewRoot, so no need to check for the ISE
if (events == null) {
int len = PhaseId.VALUES.size();
List<List<FacesEvent>> events = new ArrayList<List<FacesEvent>>(len);
for (int i = 0; i < len; i++) {
events.add(new ArrayList<FacesEvent>(5));
}
this.events = events;
}
events.get(event.getPhaseId().getOrdinal()).add(event);
}
[/code]
在这里,UIViewRoot维护着一个List<List<FacesEvent>>列表,保存所有参加排队的FacesEvent实例。这个列表根据FacesEvent实例上的getPhaseId()值来分类储存。

然后,UIViewRoot中提供了一个broadcastEvents方法,负责广播当前周期的所有FacesEvent事件
[code]
public void broadcastEvents(FacesContext context, PhaseId phaseId) {

...
boolean hasMoreAnyPhaseEvents;
boolean hasMoreCurrentPhaseEvents;

List<FacesEvent> eventsForPhaseId =
events.get(PhaseId.ANY_PHASE.getOrdinal());

// keep iterating till we have no more events to broadcast.
// This is necessary for events that cause other events to be
// queued. PENDING(edburns): here's where we'd put in a check
// to prevent infinite event queueing.
do {
// broadcast the ANY_PHASE events first
if (null != eventsForPhaseId) {
// We cannot use an Iterator because we will get
// ConcurrentModificationException errors, so fake it
while (!eventsForPhaseId.isEmpty()) {
FacesEvent event =
eventsForPhaseId.get(0);
UIComponent source = event.getComponent();
UIComponent compositeParent = null;
try {
...
source.broadcast(event);
} catch (AbortProcessingException e) {
...
}
...
}
eventsForPhaseId.remove(0); // Stay at current position
}
}

// then broadcast the events for this phase.
if (null != (eventsForPhaseId = events.get(phaseId.getOrdinal()))) {
// We cannot use an Iterator because we will get
// ConcurrentModificationException errors, so fake it
while (!eventsForPhaseId.isEmpty()) {
...
}
}

// true if we have any more ANY_PHASE events
hasMoreAnyPhaseEvents =
(null != (eventsForPhaseId =
events.get(PhaseId.ANY_PHASE.getOrdinal()))) &&
!eventsForPhaseId.isEmpty();
// true if we have any more events for the argument phaseId
hasMoreCurrentPhaseEvents =
(null != events.get(phaseId.getOrdinal())) &&
!events.get(phaseId.getOrdinal()).isEmpty();

} while (hasMoreAnyPhaseEvents || hasMoreCurrentPhaseEvents);
}
[/code]
在这里,broadcastEvents方法首先处理getPhaseId()返回[color=blue]PhaseId.ANY_PHASE[/color]的FacesEvent实例,先获取作为事件源的组件,然后调用组件上的broadcast方法进行广播。然后再处理getPhaseId()返回值为当前阶段的FacesEvent实例。在最外层,有一个do...while循环保证不遗漏在监听器中新加入的FacesEvent事件。

而这个broadcastEvents方法则在UIViewRoot的四个生命周期阶段处理入口方法:processDecodes、processValidators、processUpdates、processApplication中被调用。

public void processDecodes(FacesContext context) {
initState();
notifyBefore(context, PhaseId.APPLY_REQUEST_VALUES);

try {
if (!skipPhase) {
if (context.getPartialViewContext().isPartialRequest() &&
!context.getPartialViewContext().isExecuteAll()) {
context.getPartialViewContext().processPartial(PhaseId.APPLY_REQUEST_VALUES);
} else {
super.processDecodes(context);
}
broadcastEvents(context, PhaseId.APPLY_REQUEST_VALUES);
}
} finally {
clearFacesEvents(context);
notifyAfter(context, PhaseId.APPLY_REQUEST_VALUES);
}
}


用一段话来总结
[quote]
UIViewRoot中持有FacesEvent事件队列,FacesEvent实例中持有事件源组件,事件源组件中持有监听器。事件广播的的源头是UIViewRoot,它通过FacesEvent实例获取到事件源组件,调用事件源组件上的broadcast方法来广播事件,传入FacesEvent实例。事件源组件则把其持有的监听器依次传递给FacesEvent实例的processListener方法,在processListener方法中实际发起对监听方法的调用。
[/quote]

为什么这么绕?因为JSF希望FacesEvent的实现者在定义具体事件与广播过程中具有充分的控制权。从FacesEvent体系的使用方式与抽象层次可以看出,FacesEvent的主要目标用户,是实现JSF规范的程序员和开发组件的程序员。应用程序员所开发的逻辑主要集中在ManagedBean中,而不是针对具体组件进行编程,因此需要定义FacesEvent子类的场景不多。由于默认的FacesListener只能通过编程方式注册,在JSF1.2中,应用程序员编写的逻辑主要在[color=green]Invoke Application[/color]阶段执行,这一阶段已经是FacesEvent广播的最后阶段,此时再去注册FacesListener的实际意义已经不大。因此在JSF1.2中,框架实现者或者组件开发者如果希望让应用开发者监听某种FacesEvent的子类,则必须向应用开发者提供一种在JSF生命周期的早期阶段就能注册监听器的方案,例如后面将要讨论的ActionEvent与ValueChangeEvent。在JSF2.0中,SystemEvent的出现允许应用开发者能方便地参与到构建组件树的过程中。估计在SystemEvent的支持下,应用开发者直接使用FacesEvent机制的场景会越来越多。

(待续)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值