经典Java EE企业应用实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第3章 深入使用JSF

前一章已经介绍了JSF RI框架的基本用法,掌握前一章所介绍的知识已经可以能基本利用JSF进行简单的MVC开发了,但实际上JSF的功能远不止这些。由于JSF本质上是基于组件的MVC框架的,它不再是传统的基于请求/响应的MVC框架,因此JSF的UI组件与传统桌面应用程序中UI组件相似,这些UI组件可以激发事件,而开发者则负责为这些事件实现监听器,并将这些监听器绑定到UI组件上即可。

与所有的MVC框架一样,JSF也需要处理数据类型转换和输入校验两个问题,这两个问题实际上是息息相关的,用户提交的请求参数总是String类型的,而JSF框架则负责将这些参数转换为所需的目标类型,转换后JSF还可以对这些参数进行有效性校验——判断这些参数是否符合业务逻辑需要。JSF提供强大的转换器支持和校验器支持,本章将会详细介绍JSF内置转换器的用法,还会介绍如何开发自定义的转换器;本章也会详细介绍JSF内置校验器的用法,当然也会介绍如何开发自定义的校验器。

与所有的MVC框架一样,JSF也提供了强大的国际化支持,JSF的国际化支持非常简单,而且使用起来非常方便。本章也将详细介绍JSF国际化的相关内容。

3.1 JSF事件机制

JSF模拟了传统的C/S编程模式,它隐藏了传统Web编程的请求-响应通信模式,而是模拟了C/S编程中的事件通信机制。上一章介绍托管Bean时已经指出:JSF允许直接将UI组件的行为绑定到托管Bean,托管Bean中定义的方法可以处理Action事件、处理值改变(Value Change)事件等,实际上这就是JSF提供的事件机制。

除此之外,JSF还提供了更加完善的事件机制:它不仅提供了从java.util.EventObject派生出的多个事件类,还提供了从java.util.EventListener派生出的多个监听器接口——如果开发者熟悉AWT、Swing的事件编程模型,将非常容易理解JSF事件机制,并可以很轻松地利用JSF事件机制来开发Web应用。

为了更好地理解JSF事件机制,本书将先回顾Java事件驱动模型。

3.1.1 Java事件模型概述

在现代面向对象编程中,应用程序的各组件往往以事件机制来进行通信。在事件通信机制中,主要涉及3类对象:

Event Source(事件源):事件发生的场所,通常就是各个组件,例如按钮、窗口、菜单等。对于JSF而言,各种UI组件都可作为事件源。

Event(事件):事件封装了GUI组件上发生的特定事情(通常就是一次用户操作)。如果程序需要获得GUI组件上所发生事件的相关信息,都通过Event对象来取得。

Event Listener(事件监听器):负责监听事件源所发生的事件,并对各种事件做出相应的响应。

提示

有过JavaScript、VB等编程经验的读者都知道,事件响应的动作实际上就是一系列的程序语句,通常以方法的形式组织起来。但Java是面向对象的编程语言,方法不能独立存在,所以必须以类的形式来组织这些方法,所以事件监听器的核心就是它所包含的方法——这些方法也被称为事件处理器(Event Handler)。当事件源上的事件发生时,事件对象会作为参数传给事件处理器(即事件监听器的实例方法)。

当用户单击页面上的一个按钮或者单击某个超链接时,这些动作就会激发一个相应的事件,该事件又会被JSF实现封装成一个相应的Event对象,该事件就会触发事件源上注册的事件监听器(由java.util.Listener接口来体现),事件监听器调用对应的事件处理器(事件监听器里的实例方法)来做出相应的响应处理。

Java事件处理机制是一种委派式(Delegation)事件处理方式:普通组件(事件源)将整个事件处理委托给特定的对象(事件监听器);当该事件源发生指定的事件时,就通知所委托的事件监听器,由事件监听器来处理这个事件。

每个组件均可以针对特定的事件指定一个或多个事件监听对象,每个事件监听器也可监听一个或多个事件源。因为同一个事件源上可能发生多种事件,委派式事件处理方式可以把事件源上所有可能发生的事件分别授权给不同的事件监听器来处理;同时也可以让一类事件都使用同一个事件监听器来处理。

提示

委派式事件处理方式明显“抄袭”了人类社会的分工协作,例如某个单位发生了火灾,该单位通常不会自己处理该事件,而是将该事件委派给消防局(事件监听器)处理;如果发生了打架斗殴事件,则委派给公安局(事件监听器)处理;而消防局、公安局也会同时监听多个单位的火灾、打架斗殴事件。这种委派式的处理方式将事件源和事件监听器分离,从而提供更好的程序模型,有利于提高程序的可维护性。

图3.1显示了Java事件处理流程示意图。

图3.1 Java事件处理流程示意图

从图3.1中可以看出,当外部动作在AWT组件上进行动作时,系统会自动生成事件对象,这个事件对象是java.util.EventObject子类的实例,该事件对象会作为参数传给事件处理器方法,程序可通过该方法的形参来访问到该事件对象。

Java事件机制涉及3个成员:事件源、事件和事件监听器,其中事件源最容易创建,只要通过new来创建一个AWT组件,该组件就是事件源;事件的产生无须程序员关心,它是由系统自动产生的;所以,实现事件监听器是整个事件处理的核心。

事件监听器必须实现事件监听器接口,AWT提供了大量的事件监听器接口用于实现不同类型的事件监听器,用于监听不同类型的事件。AWT中提供丰富的事件类,用于封装不同组件上所发生的特定操作:AWT的事件类都是java.awt.AWTEvent类的子类,它是java.util.EventObject类的子类。

Java为不同事件提供了不同的事件监听器接口,当用户需要为某个组件实现事件监听器时,通常只需创建这些监听器接口的实现类即可。Java事件机制中事件监听器接口都是java.util.EventListener的子接口。

3.1.2 Java事件模型示例

下面以一个简单的HelloWorld程序来示范简单的Java事件机制。

程序清单:codes/03/3.1/EventQs.java

      public class EventQs
      {
          private Frame f = new Frame("测试事件");
          private Button ok = new Button("确定");
          private TextField tf = new TextField(30);
          public void init()
          {
              //注册事件监听器
              ok.addActionListener(new OkListener());
              f.add(tf);
              f.add(ok , BorderLayout.SOUTH);
              f.pack();
              f.setVisible(true);
          }
          //定义事件监听器类
          class OkListener implements ActionListener
          {
              //下面定义的方法就是事件处理器,用于响应特定的事件
              public void actionPerformed(ActionEvent e)
              {
                  System.out.println("用户单击了ok按钮");
                  tf.setText("Hello World");
              }
          }
          public static void main(String[] args)
          {
              new EventQs().init();
          }
      }

上面程序中粗体字代码用于注册事件监听器,斜体字定义的方法就是事件处理器。当程序中的“确定”按钮被单击时,该处理器被触发,将看到程序中tf文本框内变为"Hello World",而程序控制台打印出“用户单击了ok按钮”字符串。

从上面程序中可以看出,实现AWT事件处理机制的步骤如下:

1 实现事件监听器类,该监听器类是一个特殊的Java类,必须实现一个XxxListener接口。

2 创建普通组件(事件源),创建事件监听器对象。

3 调用addXxxListener方法将事件监听器对象注册给普通组件(事件源)。这样当事件源上发生指定事件时,AWT会触发事件监听器,由事件监听器调用相应的方法(事件处理器)来处理事件,事件源上所发生的事件会作为参数传入事件处理器。

3.1.3 JSF事件模型

JSF事件模型与Java事件模型基本相似,同样由3个部分组成:

事件源:JSF页面上所有UI组件都可作为事件源。

事件对象:当用户对UI组件执行动作时,JSF会为之创建对应的事件对象。

事件监听器:JSF的事件监听器比较灵活,既可直接使用托管Bean的某个方法来处理用户事件,也可使用专门的事件监听器对象。

JSF内置支持4种事件对象:

Action事件:当用户单击某个<h:commandButton…/>、<h:commandLink…/>生成的组件时,JSF将会触发Action事件。

值改变事件:当用户在输入框中输入数据导致UI组件的本地值发生改变时触发。

数据模型事件:由UIData组件触发。当数据表的某行被选中时触发。

生命周期事件:当JSF应用的生命周期从一个阶段进入另一个阶段时触发。

图3.2显示了JSF事件类的继承关系图。

图3.2 JSF事件类的继承关系图

与Java事件机制完全类似的是,JSF同样为不同事件对象提供了不同的监听器接口,JSF事件监听器接口的继承关系如图3.3所示。

图3.3 JSF事件监听器接口的继承关系图

将图3.2与图3.3放在一起对比不难发现,JSF同样为每种事件提供了对应的事件监听器,上面4种JSF事件对应的监听器接口关系如表3.1所示。

表3.1 JSF事件类型和监听器接口

至此,我们不难发现JSF事件机制和Java事件机制非常相似,接下来的注册事件监听器的方法则存在一定的差异:Java事件机制采用addXxxListener方法来注册事件监听器,而JSF事件机制则采用自定义标签、配置文件的方式来注册事件监听器。

为了向UI组件上注册事件监听器,JSF提供了如下自定义标签:

<f:actionListener…/>:该标签用于为指定UI组件绑定Action事件监听器。

<f:valueChangeListener…/>:该标签用于为指定UI组件绑定值改变事件监听器。

<f:phaseListener…/>:该标签用于绑定生命周期事件监听器。

使用上面3个标签来绑定事件监听器的语法格式如下:

      <h:UITag ….>
          <!-- 为其父标签注册事件监听器 -->
          <f:xxxListener type="监听器实现类"/>
      </h:UITag>

从上面语法中可以看出,只要将<f:xxxListener…/>标签放在某个可视化UI标签之内,这就表明为该UI标签注册了对应的事件监听器;使用<f:xxxListener…/>标签时可以指定如下两个属性:

type属性:该属性值就是用户提供的监听器实现类。

binding属性:该属性引用一个监听器对象。

JSF事件机制与Java事件机制还有一个显著的区别:Java事件机制中事件监听器必须实现一个独立的监听类,但JSF事件机制则允许直接使用托管Bean中某个方法作为事件处理方法。唯一的要求是该方法必须使用特定的方法签名。

Action事件对应的事件处理方法的方法签名是:

      public void xxx(ActionEvent event)

值改变事件对应的事件处理方法的方法签名是:

      public void xxx(ValueChangeEvent event)

一旦在托管Bean类中提供了满足上面签名的事件处理方法之后,接下来就可以通过为UI标签增加如下两个属性来绑定事件处理方法:

actionListener:该属性的属性值是一个方法表达式,用于绑定Action事件的事件处理方法。

valueChangeListener:该属性的属性值是一个方法表达式,用于绑定值改变事件的事件处理方法。

例如,如下代码为<h:commandButton…/>绑定一个Action事件处理方法:

      <!-- 使用xxxBean的yyyMethod作为该按钮的Action事件处理方法 --
      <h:commandButton actionListener="#{xxxBean.yyyMethod}" value="单击我"/>

上面指定将xxxBean托管Bean的yyyMethod方法作为<h:commandButton…/>的Action事件监听器,因此必须为xxxBean托管Bean类提供如下签名方法:

      public void yyyMethod(ActionEvent ae)

例如,如下代码为<h:inputText…/>绑定一个值改变事件处理方法:

      <!-- 使用xxxBean的yyyMethod作为该按钮的值改变事件处理方法 --
      <h:inputText valueChangeListener="#{xxxBean.yyyMethod}" value="单击我"/>

上面指定将xxxBean托管Bean的yyyMethod方法作为<h:inputText…/>的值改变事件处理方法,因此必须为xxxBean托管Bean类提供如下签名方法:

      public void yyyMethod(ValueChangeEvent ae)

理解了JSF事件模型之后,接下来将用不同的示例来分别介绍Action事件、值改变事件、生命周期事件以及相应的处理。

3.1.4 Action事件

Action事件类是javax.faces.event.ActionEvent,它对应的监听器是ActionListener,JSF目前只有<h:commandButton…/>、<h:commandLink…/>两个标签生成的UI组件可以触发Action事件。

Action事件的监听器类只需实现ActionListener接口,并实现该接口中定义的如下事件处理方法即可:

      public void processAction(ActionEvent event)

假设有如下视图页面:

程序清单:codes\03\3.1\ActionEvent\welcome.jsp

      <h1><h:outputText value="猜图书"/></h1>
      <h:form prependId="false" id="addForm">
      <!-- 将下面UI组件的值绑定到Bean属性 -->
      书名:<h:inputText id="name" value="#{bookBean.name}"/><br/>
      <!-- 将下面UI组件本身绑定到Bean属性 -->
      价格:<h:inputText id="price" binding="#{bookBean.price}"/><br/>
      <h:commandButton value="处理">
          <!-- 使用专门的标签来绑定事件监听器 -->
          <f:actionListener
              type="org.crazyit.jsf.CrazyActionListener"/>
      </h:commandButton>
      <br/>
      </h:form>

在浏览器中浏览该页面将看到如图3.4所示的效果。

图3.4 应用首页效果

如果希望用户在如图3.4所示页面的第一个文本框内输入一个字符串,若输入正确,程序将会修改第二个文本框的值,并改变它的背景色。

看上面页面代码中的粗体字代码,它负责为页面中第一个文本框绑定了Action监听器,该Action监听器的实现类是CrazyActionListener,该实现类代码如下:

程序清单:codes\03\3.1\ActionEvent\WEB-INF\src\org\crazyit\jsf\CrazyActionListener.java

      public class CrazyActionListener
          implements ActionListener
      {
          //编写处理Action事件的方法
          public void processAction(ActionEvent event)
          {
              //获取当前的FacesContext对象
              FacesContext context =
                  FacesContext.getCurrentInstance();
              //获取JSF页面中<f:view.../>元素
              UIViewRoot viewRoot = context.getViewRoot();
              //通过ID获取<f:view.../>内的<h:form.../>子元素
              UIComponent comp = viewRoot.findComponent("addForm");
              //通过ID获取<h:form.../>内的第一个<h:inputText.../>子元素
              UIInput input = (UIInput)comp.findComponent("name");
              //通过ID获取<h:form.../>内的第二个<h:inputText.../>子元素
              HtmlInputText price = (HtmlInputText)comp
                  .findComponent("price");
              if (input.getValue().equals("疯狂Java讲义"))
              {
                  price.setSize(60);
                  price.setValue("99.0元");
                  price.setStyle("background-color:#9999ff;"
                      + "font-weight:bold");
              }
          }
      }

上面CrazyActionListener类就是系统提供的Action事件监听器,它实现了ActionListener接口,并实现了该接口内的public void processAction(ActionEvent event)方法,该方法就是Action事件的事件处理方法——当UI组件上的Action事件发生时,该方法将会被触发。

如果用户在如图3.4所示页面的第一个文本框内输入“疯狂Java讲义”,将可以看到如图3.5所示的效果。

图3.5 Action事件监听器的处理效果

从图3.5可以看出,这个示例应用的效果与前一章介绍的示例(使用托管Bean的处理方法来监听Action事件)的效果差不多,但明显前一章介绍的示例实现起来更简单,这是因为在托管Bean内可以非常方便地访问页面上的UI组件或UI组件的值。采用Action事件监听器的方式时,为了访问页面上UI组件或UI组件的值,程序必须利用JSF的UIViewRoot提供的树形机构来获取UI组件,因此复杂得多。

提示

JSF页面中所有UI组件都必须放在<f:view…/>元素内,该元素就对应于页面的UIViewRoot对象,而其他UI组件之间也有非常清晰的父子关系,因此所有UI组件将形成一棵以UIViewRoot为根的UI组件树。

通过上面介绍不难发现一点:如果需要在Action事件处理方法内访问UI组件或UI组件的值,使用托管Bean的方法来监听UI组件是更好的选择,因为JSF允许直接将UI组件或UI组件的值绑定到托管Bean的属性。事实上,如果考虑到编程的简洁性,笔者推荐尽量使用托管Bean的方法来监听UI组件的Action方法。

如果Action事件处理方法无须访问页面的UI组件和UI组件的值,而是对整个JSF应用进行某种设置,那么使用专门的Action事件监听器也会比较便捷,后面介绍JSF程序国际化时我们会再次看到相关示例。

至于使用托管Bean的方法来监听UI组件的Action事件,上一章介绍托管Bean时已经介绍了相关内容,故此处不再赘述。

3.1.5 值改变事件

值改变事件类是javax.faces.event.ValueChangeEvent,它对应的监听器是ValueChangeListener,几乎所有输入组件的值发生改变时都可以触发值改变事件。

本示例应用模拟一个用户注册的小用例,当用户在用户名文本框内输入字符串后,系统立即向用户提示该用户名是否可用。为了达到这个目的,我们利用JSF提供的值改变事件机制来实现。

下面应用首先采用托管Bean的事件处理方法来监听UI组件的值改变事件。用户注册的输入页面代码如下:

程序清单:codes\03\3.1\ValueChange\regist.jsp

      <h1><h:outputText value="注册用户"/></h1>
      <h:form>
      <!-- 将下面UI组件的值绑定到Bean属性 -->
      <!-- 使用valueChangeListener属性指定用托管Bean的
          方法来监听该组件的值改变事件 -->
      用户名:<h:inputText value="#{registBean.name}"
      valueChangeListener="#{registBean.judgeName}"/>
      <h:outputText rendered="false" style="color:red"
          binding="#{registBean.tip}"/><br/>
      密码:<h:inputSecret value="#{registBean.pass}"/><br/>
      <h:commandButton value="注册"/>
      </h:form>

从上面页面代码中粗体字代码看到,程序使用了valueChangeListener属性指定由registBean的judgeName方法来处理该组件的值改变事件。

为了让registBean的judgeName方法能处理值改变事件,只要让该方法满足如下签名即可:

      public void judgeName(ValueChangeEvent ve)

下面是本应用中registBean托管Bean的源代码。

程序清单:codes\03\3.1\ValueChange\WEB-INF\src\org\crazyit\jsf\RegistBean.java

      public class RegistBean
      {
          private String name;
          private String pass;
          //绑定UI组件本身的属性
          private HtmlOutputText tip;
          //无参数的构造器
          public RegistBean()
          {
          }
          //初始化全部属性的构造器
          public RegistBean(String name
              , String pass , HtmlOutputText tip)
          {
              this.name = name;
              this.pass = pass;
              this.tip = tip;
          }
          //省略name属性的setter和getter方法
          …
          //省略pass属性的setter和getter方法
          …
          //省略tip属性的setter和getter方法
          …
          //监听ValueChange事件的事件处理方法
          public void judgeName(ValueChangeEvent ve)
          {
              //使用一个字符串数组模拟数据库中已存在的用户名
              String[] existNames =
              {
                  "crazyit.org",
                  "leegang.org",
                  "crazyit"
              };
              //获取用户新输入的值
              String name = ve.getNewValue().toString();
              for (int i = 0; i < existNames.length; i++)
              {
                  //如果用户输入的用户名是数据库中已存在用户名
                  if (existNames[i].equals(name))
                  {
                      tip.setValue(name + "用户名已经存在!");
                      //让tip组件显示出来
                      tip.setRendered(true);
                      return;
                  }
              }
              tip.setValue("恭喜您,"+ name + "用户名可用!");
              //让tip组件显示出来
              tip.setRendered(true);
          }
      }

上面托管Bean代码中粗体字代码通过ValueChangeEvent事件对象获得了用户新输入的用户名,这样即可判断用户新输入的用户名是否存在,从而向用户输出提示信息。

使用浏览器浏览上面页面,如果用户在第一个文本框内输入新的用户名并按回车键,将看到如图3.6所示的效果。

图3.6 使用托管Bean的方法处理值改变事件

与Action事件类似的是,JSF不仅可以采用托管Bean的方法来处理值改变事件,也可以采用专门的监听器来监听值改变事件,此时我们需要实现一个值改变事件监听器,实现该监听器必须实现ValueChangeListener接口,并实现该接口所定义的方法。

下面采用专门的值改变事件监听器来改写上面示例程序。下面首先提供本示例的监听器类。

程序清单:codes\03\3.1\ValueChange2\WEB-INF\src\org\crazyit\jsf\CrazyValueChangeListener.java

      public class CrazyValueChangeListener
          implements ValueChangeListener
      {
          public void processValueChange(ValueChangeEvent ve)
          {
              FacesContext context =
                  FacesContext.getCurrentInstance();
              //获取JSF页面中<f:view.../>元素
              UIViewRoot viewRoot = context.getViewRoot();
              //通过ID获取<f:view.../>内的<h:form.../>子元素
              UIComponent comp = viewRoot.findComponent("registForm");
              //通过ID获取<h:form.../>内的<h:outputText.../>子元素
              UIOutput tip = (UIOutput)comp.findComponent("tip");
              //使用一个字符串数组模拟数据库中已存在的用户名
              String[] existNames =
              {
                  "crazyit.org",
                  "leegang.org",
                  "crazyit"
              };
              //获取用户新输入的值
              String name = ve.getNewValue().toString();
              for (int i = 0 ; i < existNames.length ; i++)
              {
                  //如果用户输入的用户名是数据库中已存在用户名
                  if (existNames[i].equals(name))
                  {
                      tip.setValue(name + "用户名已经存在!");
                      //让tip组件显示出来
                      tip.setRendered(true);
                      return;
                  }
              }
              tip.setValue("恭喜您,"+ name + "用户名可用!");
              //让tip组件显示出来
              tip.setRendered(true);
          }
      }

细心的读者不难发现,这个值改变事件监听器所包含的方法与前面托管Bean的方法大同小异,只是这个专门的事件监听器需要通过FacesContext来获取页面中ID为tip的UI组件——如上面程序中粗体字代码所示。

提供上面专门的事件监听器之后,接下来需要将它绑定到页面的UI组件,JSF同样为值改变事件提供了专门的标签来绑定事件监听器。下面是本应用的JSF页面代码。

程序清单:codes\03\3.1\ValueChange2\regist.jsp

      <h1><h:outputText value="注册用户"/></h1>
      <h:form id="registForm" prependId="false">
      <!-- 将下面UI组件的值绑定到Bean属性 -->
      用户名:<h:inputText value="#{registBean.name}">
      <!-- 使用标签来绑定事件监听器 -->
      <f:valueChangeListener
          type="org.crazyit.jsf.CrazyValueChangeListener"/>
      </h:inputText>
      <h:outputText id="tip" rendered="false" style="color:red"
      binding="#{registBean.tip}"/><br/>
      密码:<h:inputSecret value="#{registBean.pass}"/><br/>
      <h:commandButton value="注册"/>
      </h:form>

上面程序中粗体字代码就是为UI组件绑定值改变事件监听器的标签,一旦将该监听器绑定到页面中的<h:inputText…/>组件,当用户在该文本框内完成输入并按回车键时就会触发值改变事件,而与之关联的事件监听器就会被触发了。

3.1.6 生命周期事件

生命周期事件类是javax.faces.event.PhaseEvent,该事件对应的监听器接口是:javax. faces.event.PhaseListener,该接口内包含如下3个方法:

afterPhase(PhaseEvent event):重写该方法。该方法的方法体就是开发者希望在指定生命周期之后所做的处理。

beforePhase(PhaseEvent event):重写该方法。该方法的方法体就是开发者希望在指定生命周期之前所做的处理。

PhaseId getPhaseId():重写该方法。该方法返回一个生命周期阶段ID,该监听器将会负责监听该生命周期阶段。

上面3个方法中的第3个方法比较重要,通过重写该方法表明该生命周期事件监听器对哪个生命周期阶段感兴趣。

下面一个示例示范了通过生命周期事件来控制检测用户是否登录。示例通过PhaseListener在生命周期的恢复视图阶段进行登录验证,从而避免在每个页面或者托管Bean中判断用户是否登录,从而提供更好的代码复用和更好的扩展性。本示例应用只是简单地要求用户必须登录系统,因此通过session范围的user属性即可判断。下面是本示例应用的生命周期事件监听器代码。

程序清单:codes\03\3.1\phaseListener\WEB-INF\src\org\crazyit\jsf\LoggedInCheck.java

      public class LoggedInCheck implements PhaseListener
      {
          public PhaseId getPhaseId()
          {
              return PhaseId.RESTORE_VIEW;
          }
          public void beforePhase(PhaseEvent event)
          {
          }
          public void afterPhase(PhaseEvent event)
          {
              FacesContext fc = event.getFacesContext();
              //对login.jsp页面不做检查
              boolean loginPage = fc.getViewRoot()
                  .getViewId().lastIndexOf("login.jsp")
                  > -1 ? true : false;
              if (!loginPage && !loggedIn(fc))
              {
                  NavigationHandler nh = fc.getApplication()
                      .getNavigationHandler();
                  nh.handleNavigation(fc, null, "logout");
              }
          }
          //验证是否已经登录
          private boolean loggedIn(FacesContext fc)
          {
              //读取session中的user属性
              Object user = fc.getExternalContext().getSessionMap()
                  .get("user");
              //如果session范围的user属性存在,即表明用户已经登录
              if (user != null
                  && user.toString().length() > 0)
              {
                  return true;
              }
              return false;
          }
      }

上面监听器代码重写getPhaseId()方法,该方法返回PhaseId.RESTORE_VIEW,这表明该监听器将在恢复视图阶段被触发。上面程序中粗体字代码调用了NavigationHandler来控制导航:如果用户没有登录系统并且不是访问login.jsp页面,JSF将导航到“logout”逻辑视图——相当于执行导航的方法返回了“logout”字符串。

为了让该监听器可以正常工作,同样需要在应用中注册该监听器。JSF中注册生命周期事件监听器有如下几种方法:

在faces-config.xml文件中使用<lifecycle…/>元素来注册生命周期事件监听器。

在JSF页面中使用<f:phaseListener…/>标签来注册生命周期事件监听器。

在<f:view…/>标签中使用beforePhase、afterPhase两个属性指定方法表达值作为监听生命周期事件的处理方法。

由于本应用需要让该生命周期事件监听器来监听整个应用的生命周期事件,因此我们直接在faces-config.xml文件中注册生命周期事件监听器。在faces-config.xml文件的根元素中添加<lifecycle…/>元素即可注册生命周期事件监听器,<lifecycle…/>元素的内部结构如图3.7所示。

图3.7 <lifecycle…/>元素的内部结构

从图3.7可以看出,为了在faces-config.xml文件中配置生命周期事件监听器,只要在该配置文件中增加<lifecycle…/>元素,并在该元素内使用<phase-listener…/>元素指定监听器即可。下面代码片段用于为该示例应用配置生命周期事件监听器。

程序清单:codes\03\3.1\phaseListener\WEB-INF\faces-config.xml

      <lifecycle>
          <!-- 配置生命周期事件监听器 -->
          <phase-listener>org.crazyit.jsf.LoggedInCheck</phase-listener>
      </lifecycle>

通过在faces-config.xml文件中增加上面配置片段,即为该应用注册了生命周期事件监听器。因此当用户访问JSF页面进入恢复视图阶段之后,该监听器将会检查用户是否登录,如果未曾登录,应用将会导航到“logout”逻辑视图,为此我们还需要在faces-config.xml文件中增加配置对“logout”的导航规则。

程序清单:codes\03\3.1\phaseListener\WEB-INF\faces-config.xml

      <navigation-rule>
          <!-- 从任何视图页面开始 -->
          <from-view-id>*</from-view-id>
          <navigation-case>
              <!-- 定义logout导航到/login.jsp页面 -->
              <from-outcome>logout</from-outcome>
              <to-view-id>/login.jsp</to-view-id>
          </navigation-case>
      </navigation-rule>

上面导航规则指定不管起始视图是哪个页面,只要返回“logout”逻辑视图总是导航到/login.jsp页面。因此如果未曾登录系统,试图访问该应用的任何页面都将会导航到如图3.8所示的页面。

图3.8 生命周期事件监听器控制的导航

通过如图3.8所示的页面即可登录系统,当用户登录成功后可在session范围内设置一个user属性,接下来就可以正常访问任何JSF页面了。至于本应用中控制用户登录的托管Bean没有什么特别之处,故此处不再赘述。

3.1.7 将监听器绑定到Bean属性

将监听器本身绑定到Bean属性与将UI组件本身绑定到Bean属性的功能基本相似,它们都需要定义一个组件属性来代表监听器、UI组件本身。

当使用<f:xxxListener…/>标签时指定binding属性即可将监听器绑定到Bean属性。下面的页面代码示范了将监听器绑定到Bean属性的用法。

程序清单:codes\03\3.1\listenerBinding\welcome.jsp

      <h1><h:outputText value="猜图书"/></h1>
      <h:form prependId="false" id="addForm">
      <!-- 将下面UI组件的值绑定到Bean属性 -->
      书名:<h:inputText id="name" value="#{bookBean.name}"/><br/>
      <!-- 将下面UI组件本身绑定到Bean属性 -->
      价格:<h:inputText id="price" binding="#{bookBean.price}"/><br/>
      <h:commandButton value="处理">
      <!-- 将监听器绑定到Bean属性 -->
      <f:actionListener binding="#{bookBean.listener}"/>
      </h:commandButton>
      </h:form>

上面页面代码将Action事件的监听器绑定到bookBean托管Bean的listener属性,这意味着该托管Bean的listener属性必须是ActionListener实例。下面是bookBean托管Bean的实现类。

程序清单:codes\03\3.1\listenerBinding\WEB-INF\src\org\crazyit\jsf\BookBean.java

      public class BookBean
      {
          private String name;
          //绑定UI组件本身的属性
          private HtmlInputText price;
          //绑定监听器的属性
          private ActionListener listener;
          //无参数的构造器
          public BookBean()
          {
          }
          //初始化全部属性的构造器
          public BookBean(String name
              , HtmlInputText price , ActionListener listener)
          {
              this.name = name;
              this.price = price;
              this.listener = listener;
          }
          //省略name属性的setter和getter方法
          …
          //省略price属性的setter和getter方法
          …
          //listener属性的setter和getter方法
          public void setListener(ActionListener listener)
          {
              this.listener = listener;
          }
          public ActionListener getListener()
          {
              //采用匿名内部类语法实现一个ActionListener实例
              return new ActionListener()
              {
                  //编写处理Action事件的方法
                  public void processAction(ActionEvent event)
                  {
                      //获取当前的FacesContext对象
                      FacesContext context =
                          FacesContext.getCurrentInstance();
                      //获取JSF页面中<f:view.../>元素
                      UIViewRoot viewRoot = context.getViewRoot();
                      //通过ID获取<f:view.../>内的<h:form.../>子元素
                      UIComponent comp = viewRoot.findComponent("addForm");
                      //通过ID获取<h:form.../>内的第一个<h:inputText.../>子元素
                      UIInput input = (UIInput)comp.findComponent("name");
                      //通过ID获取<h:form.../>内的第二个<h:inputText.../>子元素
                      HtmlInputText price = (HtmlInputText)comp
                          .findComponent("price");
                      if (input.getValue().equals("疯狂Java讲义"))
                      {
                          price.setSize(60);
                          price.setValue("99.0元");
                          price.setStyle("background-color:#9999ff;"
                              + "font-weight:bold");
                      }
                  }
              };
          }
      }

上面托管Bean中listener属性被绑定到一个Action事件监听器,因此托管Bean代码中listener属性的类型就是ActionListener,当程序实现getListener()方法时采用匿名内部类的方式返回了一个ActionListener对象——该对象将会作为页面中Action事件监听器。

细心的读者不难发现,这种方式与前面介绍的采用单独的Action事件监听器的实现代码并没有太大的不同,只是这种方式可以让托管Bean获得事件监听器的引用,从而获取对事件监听器的全部控制,进而简化编程。