`
kidneyball
  • 浏览: 328991 次
  • 性别: 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
分享到:
评论

相关推荐

    一个简单的jsf例子------JSF2学习笔记1

    **JSF2学习笔记1——理解JavaServer Faces 2.0框架** JavaServer Faces (JSF) 是一种基于组件的Web应用程序开发框架,由Sun Microsystems(现为Oracle Corporation的一部分)开发,旨在简化用户界面构建。JSF2是该...

    良葛格_JSF学习笔记.rar

    JSF(JavaServer Faces)是Java平台上用于构建用户界面的Web开发框架,它提供了一种组件化、事件驱动的方式来创建动态、交互式的Web应用程序。在这个“良葛格_JSF学习笔记”中,我们将会深入探讨JSF的核心概念、工作...

    《JSF入门》简体中文版.rar

    而《Garfield的SCJP阅读笔记》.rar文件可能是关于Sun Certified Programmer for the Java SE Platform(SCJP)考试的学习资料,SCJP是Java开发者的基础认证,对于深化Java语言的理解大有裨益,但与JSF主题不直接相关...

    JavaEE5实战笔记04JSF的一些补充

    本篇实战笔记主要补充了在JavaEE5中使用JSF的一些关键点,特别是关于流程转向和界面参数传递。 1. **流程转向**: 在JSF中,流程转向通常通过`faces-config.xml`文件中的`&lt;navigation-rule&gt;`元素来定义。例如,当...

    JSF+Spring+Hibernate相关技术文档

    JSF 2.0规范(其中包含的JSF_20规范.pdf和JSF.pdf文档可能详细阐述了这一版本的特性)引入了许多改进,如异步处理、更强大的Facelets视图技术以及对CDI(Contexts and Dependency Injection)的支持,使得开发者可以...

    jee6 学习笔记 5 - Struggling with JSF2 binding GET params

    在Java企业版(Java EE)6的开发过程中,JSF(JavaServer Faces)2.0是用于构建用户界面的重要组件。这篇"jee6 学习笔记 5 - Struggling with JSF2 binding GET params"主要探讨了开发者在使用JSF2绑定GET参数时可能...

    jbpm_websale.rar_JSF_java JBPM_jbpm_jbpm websale_websale jb

    JSF,全称为JavaServer Faces,是Java平台上的一个用于构建用户界面的MVC(Model-View-Controller)框架。在这个示例中,JSF负责处理用户的交互,展示页面,并将这些交互传递到后端处理。 在【jbpm_websale】项目中...

    AppFuse学习笔记(J2EE入门级框架)

    AppFuse 是一个由 Matt Raible 创建的开源项目,它为初学者提供了一个基础的 J2EE 框架,用于演示如何集成多个流行的技术,如 Spring、Hibernate、iBatis、Struts、xDcolet、JUnit、Taperstry 和 JSF 等。...

    JavaEE5学习笔记01-JTA和数据库事务

    - **JSF1.2**:JavaServer Faces,提供了一个基于组件的UI框架,用于构建用户界面。 - **JSTL1.2**:JavaServer Pages Standard Tag Library,提供了一套标准的标签库,用于简化JSP页面的开发。 - **JTA1.1**:Java ...

    【读书笔记】Java参考大全-J2EE5版本

    JSF2.0在J2EE5中引入,进一步增强了其功能,如Facelets视图技术、复合组件支持和AJAX集成。 5. **持久化API (JPA)**:JPA是Java Persistence API,它是J2EE5的一部分,提供了一种标准的方式来管理和持久化Java对象...

    清晰的技术资料学习笔记

    9. **JSF入門.pdf**:这是一份JSF的基础教程,可能涵盖了JSF生命周期、组件模型、事件处理、渲染树等基础知识,适合JSF初学者。 10. **慣用句840.pdf**:这可能是840个常用的日语惯用句,对于提高日语水平和理解...

    WAS 8.5 Liberty学习笔记.pdf

    - Liberty与Tomcat的对比中,Liberty提供了更全面的Java EE标准支持,包括JSP2.2, Servlet3.0, JSF2.0, JPA2.0, JDBC4.0等。 - Liberty支持OSGi编程模型,并提供对Eclipse和IBM Rational Application Developer...

    appfuse 学习笔记

    ### Appfuse 学习笔记 #### 一、Appfuse 简介 Appfuse 是一个开源框架,旨在帮助开发者高效地构建企业级应用。通过提供一套完善的架构模板、最佳实践和技术栈组合,使得开发者能够专注于业务逻辑的实现,而不是...

    j2ee体系chm帮助文档大全

    这个压缩包里涵盖了从基础到高级的各种主题,包括Hibernate学习笔记、J2EE全实例教程、JSF中文教程、Java设计模式、XML指南以及Struts2.0中文帮助手册等,对于想要深入理解J2EE框架和技术的开发者来说,无疑是一份...

    整理后java开发全套达内学习笔记(含练习)

    事件(环境,状况) ['sә:kәmstәns] crash n.崩溃,破碎 [kræʃ] cohesion 内聚,黏聚,结合 [kәu'hi:ʒәn] (a class is designed with a single, well-focoused purpose. 应该不止这点) command n. 命令,指令 ...

    appfuse 2.0.2PDF格式文档

    AppFuse提供了从旧版本到新版本的迁移指南,包括从2.0到2.0.1再到2.0.2的版本更新笔记,以及在不同版本间迁移时可能遇到的问题和解决方案。 综上所述,AppFuse 2.0.2不仅是一个框架,更是一套完整的Web应用开发解决...

Global site tag (gtag.js) - Google Analytics