`
kidneyball
  • 浏览: 326945 次
  • 性别: Icon_minigender_1
  • 来自: 南太平洋
社区版块
存档分类
最新评论

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

阅读更多
一、什么是事件

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

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

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

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

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

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

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

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

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

二、JSF2.0中的事件

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

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

事件类静态类图



监听器类静态类图



2.1 FacesEvent

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

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

a. 事件类型
FacesEvent。这是一种依附在组件上,为组件服务的事件。其事件源为组件实例。

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

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 expected that UIComponent classes acting as event sources will have corresponding typesafe 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);
   }
   ...
 }



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

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


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

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

e. 监听器返回结果处理
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方法的默认实现
    public void queueEvent(FacesEvent event) {

        if (event == null) {
            throw new NullPointerException();
        }
        UIComponent parent = getParent();
        if (parent == null) {
            throw new IllegalStateException();
        } else {
            parent.queueEvent(event);
        }
    }

可以看出,普通组件自身并不维护通过queueEvent方法排队FacesEvent实例,而是把它委托给在组件树中的父组件处理。这样层层委托,最终就传递到了UIViewRoot的queueEvent方法中。再看UIViewRoot的queueEvent方法
    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);
    }

在这里,UIViewRoot维护着一个List<List<FacesEvent>>列表,保存所有参加排队的FacesEvent实例。这个列表根据FacesEvent实例上的getPhaseId()值来分类储存。

然后,UIViewRoot中提供了一个broadcastEvents方法,负责广播当前周期的所有FacesEvent事件
    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);
    }

在这里,broadcastEvents方法首先处理getPhaseId()返回PhaseId.ANY_PHASE的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);
        }
    }


用一段话来总结
引用

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


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

(待续)
  • 大小: 72.1 KB
  • 大小: 109.6 KB
0
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics