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

2.5 托管Bean和表达式语言

托管Bean是JSF应用中最重要的应用组件,因此开发JSF应用的重要任务就是开发托管Bean。JSF框架对托管Bean没有太多额外的要求,托管Bean完全可以是普通POJO(Plain Old Java Object)。在典型情况下,一个托管Bean与一个应用页面结合,托管Bean定义了与页面中UI组件关联的属性和方法。

2.5.1 托管Bean的属性和表达式语言

JSF托管Bean的两个基本功能就是:

提供一组属性与页面中UI组件对应。

提供一组对页面组件执行功能的方法。

JSF的托管Bean与普通Java Bean几乎完全一样,只要为它提供一个无参数的构造器,并为它的每个属性提供setter和getter方法即可。托管Bean的属性可以绑定到如下形式之一:

绑定到一个组件的值(通过UI组件的value属性执行绑定)。

绑定到一个组件实例(通过UI组件的binding属性执行绑定)。

绑定到一个转换器实例(通过<f:converterXxx…/>标签绑定)。

绑定到一个验证器实例(通过<f:validatorXxx…/>标签执行绑定)。

绑定到一个监听器实例(通过<f:xxxListener…/>标签绑定)。

关于把Bean属性绑定到转换器、验证器、监听器的情形,将在本书第3章进行更详细的解释,此处主要介绍将Bean属性绑定到组件的值和组件实例这两种情形。

下面以NetBeans开发为例来介绍将托管Bean属性绑定到UI组件属性的例子。NetBeans为开发JSF应用提供了很好的支持。

只要按第1章介绍的方式创建一个Web应用,并在如图1.34所示的窗口中勾选JavaServer Faces选项即可,表示为应用增加JSF支持。Web应用创建完成即可在项目管理窗口看到如图2.11所示的界面。

图2.11 使用NetBeans创建的JSF应用

注意

从图2.11可以看出,我们在NetBeans中开发JSF应用时使用了WebLogic服务器,其实开发JSF应用使用WebLogic服务器有点大材小用,开发JSF应用使用Tomcat就足够了,而且Tomcat的启动速度比WebLogic要快得多。

在NetBeans中编写JSF视图页面可以看到如图2.12所示的界面。

图2.12 使用NetBeans编写JSF视图页面

从图2.12可以看出,NetBeans为编写JSF视图页面提供了基本的标签提示,这可以降低开发者开发JSF页面的难度。

在如图2.12所示的界面中为JSF应用编写JSF页面,编写JSF视图页面与编写普通JSP页面并无太大差别,只是需要大量使用JSF标签而已。编写完的视图页面代码如下:

程序清单:codes\02\2.5\bindingProperty\web\welcomeJSF.jsp

      <%@page contentType="text/html" pageEncoding="UTF-8"%>
      <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
      <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
      <f:view>
      <html>
          <head>
              <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
              <title>JSP Page</title>
          </head>
          <body>
              <h1><h:outputText value="添加图书"/></h1>
              <h:form>
              书名:<h:inputText label="书名:" value="#{bookBean.name}"/><br/>
              ISBN:<h:inputText label="ISBN:" value="#{bookBean.isbn}"/><br/>
              简介:<h:inputTextarea rows="4" cols="50" label="描述:"
                  value="#{bookBean.desc}"/><br/>
              <h:commandButton value="添加"/>
              </h:form>
          </body>
      </html>
      </f:view>

从上面的粗体字代码可以看出,通过UI组件的value属性即可把该组件的值绑定到托管Bean的属性,例如<h:inputText label="书名:" value="#{bookBean.name}"/>表明将该单行文本框绑定到托管Bean的name属性。

接下来开始编写托管Bean,编写托管Bean按如下步骤进行。

1 单击NetBeans的主菜单“File-> New File”,即可打开如图2.13所示的窗口。

图2.13 为JSF应用创建托管Bean

2 按图2.13所示输入托管Bean的相关信息之后,单击“Next”按钮,即可看到如图2.14所示的对话框。

3 在图2.14中填写好托管Bean的配置信息之后,单击“Finish”按钮完成创建。接下来就开始为托管Bean编写Java代码,这里不再赘述了。

4 编写完托管Bean之后,接下来打开JSF的配置文件,我们会发现该NetBeans已经配置了BookBean。虽然几乎所有的IDE工具都会为开发者自动配置托管Bean,但开发者依然需要知道配置托管Bean的更多细节。

图2.14 填写托管Bean的配置信息

在JSF配置文件里由<faces-config…/>的子元素<managed-bean.../>来配置管理所有的托管Bean,JSF中<managed-bean.../>元素的内部结构如图2.15所示。

图2.15 <managed-bean…/>元素的内部结构

从图2.15可以看出,<managed-bean…/>元素必须包含如下三个子元素:

<managed-bean-name.../>:该元素接受一个字符串参数值,用于指定托管Bean的名称。

<managed-bean-class.../>:该元素接受一个字符串参数值,用于指定托管Bean的全限定类名。

<managed-bean-scope…/>:该元素只接受request、session、application三个值之一,该元素用于指定托管Bean的有效范围。

上面三个子元素中<managed-bean-scope…/>决定了该托管Bean的有效范围,三个值对应的有效范围如下:

request:对于一次请求有效。该范围的Bean实例会随着请求的结束而销毁。

session:对于一次用户会话有效,该范围的Bean实例会随着用户会话的结束而销毁。

application:对于整个Web应用有效,只要Web应用处于运行状态,该范围的Bean实例将一直有效。

对于大部分托管Bean而言,它仅仅充当业务控制器的角色,因此只需将它的作用范围设置为request即可。但还有另外一些托管Bean,它们可能需要在session、application范围一直有效,后面会有关于session、application范围内有效的托管Bean的更详细介绍。

除此之外,<managed-bean…/>元素还可包含零个到多个<managed-property…/>子元素、一个<map-entries…/>子元素或一个<list-entries…/>子元素,它们都是用于初始化托管Bean的属性,本节后面会介绍如何来初始化托管Bean的属性。例如,NetBeans配置该托管Bean的示例代码如下:

      <managed-bean>
          <managed-bean-name>bookBean</managed-bean-name>
          <managed-bean-class>org.crazyit.jsf.BookBean</managed-bean-class>
          <managed-bean-scope>request</managed-bean-scope>
      </managed-bean>

接下来还需要为该应用增加导航规则,在faces-config.xml文件中增加如下配置片段即可。

      <navigation-rule>
          <from-view-id>/welcomeJSF.jsp</from-view-id>
          <navigation-case>
              <from-outcome>success</from-outcome>
              <to-view-id>/show.jsp</to-view-id>
          </navigation-case>
      </navigation-rule>

5 上面的配置片段配置了当/welcomeJSF.jsp页面返回success时将进入/show.jsp页面,为此我们还需要为该应用增加一个JSP页面。单击NetBeans主菜单“File -> New File”即可打开如图2.13所示的对话框,选择创建JSF Page即可。

6 创建完成后,即可进入如图2.12所示的编辑界面,通过该界面编辑show.jsp页面。show.jsp页面代码如下:

程序清单:codes\02\2.5\bindingProperty\web\show.jsp

      <%@page contentType="text/html" pageEncoding="UTF-8"%>
      <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
      <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
      <f:view>
          <html>
              <head>
                  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
                  <title>添加结果</title>
              </head>
              <body>
                  <h1>添加结果</h1>
                  书名:<h:outputText value="#{bookBean.name}" /><br/>
                  ISBN:<h:outputText value="#{bookBean.isbn}" /><br/>
                  简介:<h:outputText value="#{bookBean.desc}" /><br/>
              </body>
          </html>
      </f:view>

上面粗体字代码中同样将托管Bean的属性绑定到UI组件的属性——从上面代码可以看出执行绑定时用到了形如#{bookBean.desc}的字符串,这是一个典型的表达式语言(Expression Language,EL)。

提示

几乎所有的MVC框架都应该提供强大的表达式语言,这样才能让视图页面以简单、明了的方式在视图页面上访问程序状态,为此,Struts 2提供了OGNL表达式语言。此处的JSF也提供了表达式语言。但Struts 1没有提供表达式语言支持,因此开发者使用Struts 1标签库就变成了一种折磨,笔者往往推荐放弃使用Struts 1标签库,而是改为使用JSTL标签库。

JSF表达式语言的形式是所有位于#号之后的一对花括号之中的字符串("#{...}"),EL对于JSF的作用非常大,它通常用来将UI绑定到托管Bean、Model组件。JSF中的表达式语言通常具有延迟求值的特征,通常会等到视图被显示时才对表达式语言求值,而不是在编译时求值。

与JSP 2、Struts 2中EL最大的不同是,JSF中的EL不仅可以访问、输出Bean的属性,还可以更新Bean属性值(当把输入组件的值绑定到托管Bean的属性时)。

JSF EL由JSP 2的表达式语言发展而来,因此使用起来非常简单。当然,JSF EL和JSP 2 EL还是有一定区别的,它们之间的关键不同点如下:

JSF使用井号(#)来标记表达式语言的开始,而JSP使用美元符号($)。

JSP 2 EL只是访问并输出变量、Bean的值;而JSF EL不仅可以访问、输出托管Bean的属性,还可以更新托管Bean的属性。

JSF EL支持访问表达式,因此也允许引用对象方法。

JSF EL不支持内嵌函数。

JSF EL中内置对象和JSP 2 EL中内置对象略有不同。

表2.2示范了JSF EL的一些常用示例,通过这些示例读者可以看出,JSF EL不仅可以访问Bean的属性,也可以访问集合,还可以访问嵌套属性。

表2.2 JSF EL的常用示例

从上面示例可以看出,JSF EL的用法十分灵活,它不仅可以访问Bean对象的属性,也可以访问数组和List、Map等集合内的元素。不仅如此,JSF EL还支持大量功能丰富的运算符,JSF EL所支持的运算符如表2.3所示。

表2.3 JSF EL支持的运算符

正如前面示例中看到的,JSF EL表达式中不仅可以访问Bean的属性及List、Map集合的元素,也可以调用Bean的方法,这种访问Bean方法的表达式被称为方法表达式——JSF EL的方法表达式无须使用完整的方法签名,只要指定托管Bean和方法名即可。

学习过JSP 2表达式语言的读者应该还记得:JSP 2的EL一共提供了11个内置对象,通过这些内置对象允许开发者在JSP 2 EL中获取请求头及获取page、request、session和application范围的属性值等。

JSF EL也提供了11个内置对象,但JSF EL提供的11个内置对象和JSP 2 EL提供的11个内置对象略有差异。下面是JSF EL提供的11个内置对象的说明。

applicationScope:与JSP 2 EL中applicationScope内置对象的作用基本相同,用于访问application范围的属性值。

cookie:与JSP 2 EL中cookie内置对象的作用基本相同,用于获取指定的Cookie值。

facesContext:访问当前请求对应的FacesContext对象。

header:与JSP 2 EL中header内置对象的作用基本相同,用于获取请求头的值。

headerValues:与JSP 2 EL中headerValues内置对象的作用基本相同,用于获取指定请求头的值。与header的区别在于,该对象用于获取请求头值为数组的值。

initParam:与JSP 2 EL中initParam内置对象的作用基本相同,用于获取请求Web应用的初始化参数。

param:与JSP 2 EL中param内置对象的作用基本相同,用于获取指定请求参数的值。

paramValues:与JSP 2 EL中paramValues内置对象的作用基本相同,用于获取指定请求参数的值。与param的区别在于,该对象用于获取请求参数值为数组的值。

requestScope:与JSP 2 EL中requestScope内置对象的作用基本相同,用于获取request范围的属性值。

sessionScope:与JSP 2 EL中sessionScope内置对象的作用基本相同,用于获取session范围的属性值。

view:访问所有视图组件所在的根UIComponent。

上面11个内置对象中有9个对象的作用与JSP 2 EL的内置对象基本相同,因此无须进行更多介绍。但facesContext和view是JSF独有的内置对象,其中facesContext代表当前请求对应的FacesContext对象,如果希望在视图页面中访问JSF Context相关信息(关于FacesContext的用法,可以查看JSF的API文档),即可通过该对象来完成——实际上,我们很少需要直接使用facesContext内置对象。

对于view内置对象而言,如下属性比较常用:

viewId:获取当前视图的ID。

renderKitId:获取当前视图所使用的绘制器的ID。

locale:获取访问当前视图的用户所用的语言、国家选项。

上面介绍了通过为UI组件指定value属性把该组件的值绑定到托管Bean的用法,接下来介绍通过为UI组件指定binding属性将该组件实例绑定到托管Bean的用法。下面先看一个简单的页面。

程序清单:codes\02\2.5\bindingInstance\welcome.jsp

      <%@page contentType="text/html" pageEncoding="UTF-8"%>
      <%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
      <%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
      <f:view>
      <html>
          <head>
              <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
              <title>JSP Page</title>
          </head>
          <body>
              <h1><h:outputText value="添加图书"/></h1>
              <h:form>
              <!-- 将下面UI组件的值绑定到Bean属性 -->
              书名:<h:inputText value="#{bookBean.name}"/><br/>
              <!-- 将下面UI组件本身绑定到Bean属性 -->
              价格:<h:inputText binding="#{bookBean.price}"/><br/>
              <h:commandButton value="处理" action="#{bookBean.process}"/><br/>
              </h:form>
          </body>
      </html>
      </f:view>

上面页面中的粗体字代码通过binding属性将指定UI组件本身绑定到bookBean的price属性。当程序试图把UI组件本身绑定到托管Bean的属性时,有两点需要指出:

视图页面中为UI组件使用binding属性执行绑定。

托管Bean必须为该属性使用UI组件匹配的属性。例如,上面UI组件由inputText标签生成,则托管Bean对应的price属性的类型应该使用UIInput及其子类。

为上面页面定义如下托管Bean。

程序清单:codes\02\2.5\bindingInstance\WEB-INF\src\org\crazyit\jsf\BookBean.java

      public class BookBean
      {
          private String name;
          //绑定UI组件本身的属性
          private HtmlInputText price;
          //无参数的构造器
          public BookBean()
          {
          }
          //初始化全部属性的构造器
          public BookBean(String name, HtmlInputText price)
          {
              this.name = name;
              this.price = price;
          }
          //省略name属性的setter和getter方法
          …
          //省略price属性的setter和getter方法
          …
          //编写处理导航的方法
          public String process()
          {
              if (name.equals("疯狂Java讲义"))
              {
                  price.setValue("99.00元");
                  price.setStyle("background-color:#11ff11");
              }
              return null;
          }
      }

从上面粗体字代码可以看出,该托管Bean中price属性的类型是HtmlInputText类型,这与页面中的inputText组件的类型是匹配的。

提示

关于JSF所提供的UI组件,本章后面专门有一节来进行更详细的介绍。

上面托管Bean中对用户请求处理非常简单:它判断当name属性值等于“疯狂Java讲义”字符串时,程序将price的value设置为99.00元,并将该price组件的背景色设为绿色(这是通过CSS样式来控制的)。由于该托管Bean的process方法只是返回一个空字符串,因此无须为它配置导航规则,只要配置该托管Bean即可。

将该JSF应用部署在WebLogic应用服务器(Tomcat亦可)中,使用浏览器来访问该JSF应用,可以看到如图2.16所示的效果。

图2.16 将UI组件实例绑定到Bean属性

从图2.16可以看出,我们在“书名”文本框中输入“疯狂Java讲义”后单击“处理”按钮,即可看到第二个文本框的内容变为99.00元,且背景色变为绿色。

介绍到这里,可能有读者发现这个JSF应用有点类似于Ajax技术的效果,但实际上并没有采用Ajax技术(当然,JSF规范也可采用Ajax来实现,这取决于JSF本身的实现。),这个JSF应用与Ajax技术主要有两个区别:

该应用依然以同步方式提交请求。

该应用的视图状态是保存在服务器端的,这意味着即使用户刷新该页面,第二个文本框的背景色、值依然不会改变。

不仅如此,Ajax技术由程序员自己通过JavaScript脚本来控制HTML页面显示,因此控制起来更加灵活;但JSF应用则通过在托管Bean中编程来控制指定的UI组件,这种方式的灵活性就差了很多。

从上面介绍不难看出,当程序把UI组件本身绑定到托管Bean的属性后,托管Bean将可以获得对该UI组件全部的控制,因此可以在托管Bean内对该UI组件进行更全面的改变。

除了可以将托管Bean的属性绑定到UI组件的属性、UI组件本身之外,还可以将Bean的属性绑定到转换器、验证器、监听器,这些内容将在第3章作进一步介绍。

2.5.2 托管Bean的方法

前面已经介绍了有关托管Bean的属性的知识,JSF托管Bean中两大主要成分就是属性和方法,托管Bean的方法主要有如下几种:

处理导航的方法。

处理Action事件的方法。

处理Value Change事件的方法。

处理输入校验的方法。

其中处理导航的方法是前面已经多次见到的方法,关于这种方法有如下两点说明:

通常用commandButton或者commandLink两个标签中的action的属性将单击事件绑定到托管Bean内处理导航的方法。

托管Bean内处理导航的方法签名为public String xxx()形式,其中方法名是随意的,但该方法返回值必须是String类型,这个String类型的返回值就是一个逻辑视图,JSF的导航规则将会根据该逻辑视图导航到真实的物理视图。

关于处理导航的方法前面已经看到了多个示例,故此处就不再介绍了。接下来介绍在Bean中编写处理Action事件的方法。

关于在托管Bean内编写处理Action事件的方法,同样有两点需要说明:

通常用commandButton或者commandLink两个标签中的actionListener属性将单击事件绑定到托管Bean内处理Action事件的方法。

托管Bean内处理Action事件的方法签名为public void xxx(ActionEvent ae)形式,其中方法名是随意的,但该方法返回值应该声明为void。而且该方法必须接受一个ActionEvent类型的形参,当该方法被触发时,该形参就代表了触发该方法的事件对象。

上面方法中的ActionEvent是java.util.EventObject的子类,因此它完全可以通过getSource()方法来获得触发该事件的事件源。除此之外,它也提供了一系列JSF相关的方法,用于获取更多关于JSF事件相关的信息。

下面的页面代码片段使用actionListener将按钮的单击事件绑定到Bean的某个方法,那么该方法就是一个处理Action事件的方法。页面代码如下:

      <h:form>
      <!-- 将下面UI组件的值绑定到Bean属性 -->
      书名:<h:inputText value="#{bookBean.name}"/><br/>
      <!-- 将下面UI组件本身绑定到Bean属性 -->
      价格:<h:inputText binding="#{bookBean.price}"/><br/>
      <!-- 使用actionListener绑定到处理Action事件的方法 -->
      <h:commandButton value="处理"
          actionListener="#{bookBean.process}"/><br/>
      </h:form>

上面粗体字代码中指定bookBean内必须包含一个process方法,而且这个方法必须是一个处理Action事件的方法,因此该方法的签名应该是public void process(ActionEvent ae)形式。下面是bookBean的源代码。

程序清单:codes\02\2.5\ActionEvent\WEB-INF\src\org\crazyit\jsf\BookBean.java

      public class BookBean
      {
          private String name;
          //绑定UI组件本身的属性
          private HtmlInputText price;
          //无参数的构造器
          public BookBean()
          {
          }
          //初始化全部属性的构造器
          public BookBean(String name , HtmlInputText price)
          {
              this.name = name;
              this.price = price;
          }
          //省略name属性的setter和getter方法
          …
          //省略price属性的setter和getter方法
          …
          //编写处理Action事件的方法
          public void process(ActionEvent ae)
          {
              if (name.equals("疯狂Java讲义"))
              {
                  price.setValue(ae.getComponent());
                  price.setSize(60);
                  price.setStyle("background-color:#1111ff;"
                      + "font-weight:bold");
              }
          }
      }

上面托管Bean代码中的粗体字代码定义了一个处理Action事件的方法,注意:该方法有一个ActionEvent类型的形参,它代表了触发该方法的事件,通过该ActionEvent对象即可获得和JSF事件有关的大量信息。

该方法无须任何返回值,因此该方法处理结束后,应用将依然停留在原来的页面。将该应用部署在WebLogic服务器中,使用浏览器来访问该应用将看到如图2.17所示的效果。

图2.17 托管Bean中处理Action事件的方法

对于处理Action事件的方法而言,它没有返回值,因此它不会导致应用导航到其他页面,但程序可以通过该方法来改变应用的状态,从而改变该页面的显示。

2.5.3 托管Bean的分类

前面已经介绍了,托管Bean有3个有效范围:request、session和application,对于处理用户请求的托管Bean来说,它的有效范围是request。但对于一个更特殊的要求,则必须将托管Bean放入另外两个有效范围中。

假设现在有一个最普通的用户登录用例:当用户登录成功后,我们必须将登录用户的用户名、性别等信息放入session范围中,这样才能让应用跟踪到该用户的登录状态——但读者可能都注意到一点:托管Bean已经与Servlet API完全分离了,看上去无法访问Servlet API。为了解决这个问题,我们有两种做法:

通过FacesContext获取Servlet API,然后使用原生Servlet API来访问session、application等范围。

通过使用session、application范围的托管Bean。

第一种方式简单、直观,也是笔者看到许多软件公司实际开发所使用的方式——但这种方式有很严重的缺陷,它导致托管Bean与JSF API、Servlet API耦合,这是一种典型的侵入式设计,破坏了托管Bean类的纯洁性;而且托管Bean与JSF API、Servlet API耦合,使得托管Bean难于被测试。

很明显,使用第一种方式的开发者的视野还停留在原始的MVC框架阶段,完全放弃了JSF的优雅设计。下面我们将采用第二种方式来解决这个问题。

首先为应用提供简单的用户登录页面,这个页面使用JSF的UI标签生成了两个单行文本框,这已经没有什么值得介绍了,故不再赘述。

接下来我们为该应用编写一个托管Bean,该托管Bean负责保存需要放入session中的数据。下面是该托管Bean的代码。

程序清单:codes\02\2.5\otherScopeBean\WEB-INF\src\lee\UserBean.java

      public class UserBean
      {
          private String name;
          private String gender;
          //无参数的构造器
          public UserBean()
          {
          }
          //初始化全部属性的构造器
          public UserBean(String name, String gender)
          {
              this.name = name;
              this.gender = gender;
          }
          //省略name属性的setter和getter方法
          …
          //省略gender属性的setter和getter方法
          …
      }

上面的UserBean是最简单的JavaBean,它只提供了name、gender两个属性,接下来将该Bean配置成托管Bean,并将其有效范围设置为session。如下所示就是该托管Bean的配置代码。

      <!-- 配置托管Bean -->
      <managed-bean>
          <!-- 设置托管Bean的名字 -->
          <managed-bean-name>userBean</managed-bean-name>
          <!-- 设置托管Bean的实现类 -->
          <managed-bean-class>lee.UserBean</managed-bean-class>
          <!-- 设置托管Bean实例的有效范围 -->
          <managed-bean-scope>session</managed-bean-scope>
      </managed-bean>

从上面粗体字代码可以看出,userBean被配置成session范围有效,那么这个托管Bean不会用于处理用户请求。

接下来为登录请求编写一个托管Bean,这个托管Bean负责处理登录请求。该托管Bean的代码如下:

程序清单:codes\02\2.5\otherScopeBean\WEB-INF\src\lee\LoginBean.java

      public class LoginBean
      {
          //下面的三个属性都会直接与JSF标签绑定
          private String name;
          private String pass;
          private String err;
          private UserBean user;
          //省略name属性的setter和getter方法
          …
          //省略pass属性的setter和getter方法
          …
          //省略err属性的setter和getter方法
          …
          //user属性的setter和getter方法
          public void setUser(UserBean user)
          {
              this.user = user;
          }
          public UserBean getUser()
          {
              return this.user;
          }
          //该方法被绑定到UI组件(按钮)的action属性
          public String valid()
          {
              if (name.equals("crazyit") && pass.equals("leegang"))
              {
                  //这里实际上应该从数据库读取该用户的状态信息
                  getUser().setName("疯狂Java");                       //①
                  getUser().setGender("男");                            //②
                  return "success";
              }
              setErr("您的用户名和密码不符合");
              return "failure";
          }
      }

上面LoginBean与原来介绍的托管Bean的最大区别在于它包含一个UserBean类型的user属性,如程序中粗体字代码所示。需要注意的是,这个UserBean被配置成一个session范围内有效的托管Bean,那如何让LoginBean获得UserBean呢?这可借助于初始化托管Bean属性的方法。

下面配置片段不仅配置了LoginBean,还利用表达式对LoginBean的user属性执行了初始化。

      <!-- 配置托管Bean -->
      <managed-bean>
          <!-- 设置托管Bean的名字 -->
          <managed-bean-name>login</managed-bean-name>
          <!-- 设置托管Bean的实现类 -->
          <managed-bean-class>lee.LoginBean</managed-bean-class>
          <!-- 设置托管Bean实例的有效范围 -->
          <managed-bean-scope>request</managed-bean-scope>
          <managed-property>
              <property-name>user</property-name>
              <!-- 使用表达式初始化托管Bean的属性 -->
              <value>#{userBean}</value>
          </managed-property>
      </managed-bean>

上面粗体字代码用于初始化login Bean的user属性,指定user属性值时使用了#{userBean}表达式语言——这也是JSF表达式语言的用法,JSF同样会对该表达式求值,从而将名为userBean的托管Bean注入login Bean中。

提示

这种使用表达式语言来初始化Bean属性值的方式,其实就是典型的IoC。也就是说,LoginBean无须以硬编码的方式来获得UserBean实例,而是改为由容器将UserBean实例注入Login Bean,从而实现两个Bean实例的解耦。

通过上面配置文件对托管Bean的管理,就可以让LoginBean获得对UserBean的引用,而UserBean又是session范围内有效的托管Bean——当LoginBean修改了UserBean的属性之后,这种修改将在session范围内有效。正如上面LoginBean代码中①②两处代码所示,LoginBean修改了UserBean对象名字、性别,这就相当于把用户状态存入session范围。

通过这种方式把程序状态放入session中之后,为了访问session范围内的该状态,我们分两种情况进行说明:

如果只是想在视图页面中访问该session范围内的该状态,直接使用JSP 2 EL也可以,使用JSF的EL也可以。如下面代码所示:

      <!-- 使用JSP 2的EL来访问session范围的状态 -->
      session范围的用户名:${sessionScope.userBean.name}<br/>
      session范围的用户名:${sessionScope.userBean.gender}

如果需要在其他托管Bean中访问session范围内的该状态,只要再次将userBean依赖注入到其他托管Bean中即可。

虽然上面示例仅仅示范了session范围内有效的托管Bean,但application范围内有效的托管Bean的用法也与此类似。

通过该示例即可看出,托管Bean的用法其实比较灵活,它并不一定等同于传统MVC框架中的业务控制器,托管Bean覆盖的范围明显更广。其实JSF本身也是一个简单的IoC容器,其名称为JSF Managed Bean Facility(MBF),就像Spring容器管理着数量众多的Bean,而JSF也管理这数量众多的托管Bean——当然,JSF的IoC容器远不如Spring提供的IoC容器。

虽然JSF提供的IoC容器可以管理不同类型的托管Bean,但JSF毕竟只是一个前端框架,因此几乎很少使用JSF来管理业务组件。受JSF管理的托管Bean通常与前端视图、前端控制有关。即使如此,我们依然可以将托管Bean分为如表2.4所示的几类。

表2.4 托管Bean的分类

关于上面托管Bean中的model-bean已有了详细的示例,utility-bean的用法也比较简单,故不再进一步说明。

关于backing-bean、controller-bean两种托管Bean可能让读者感到迷惑,不过如果读者有Struts 1开发经验,那就比较容易理解了。Struts 1开发者经常需要开发两种组件:

ActionForm:基本上每个表单对应一个ActionForm,每个表单控件对应ActionForm的一个属性。

Action:Action通过ActionForm来访问请求参数,并包含一个execute方法来处理用户请求。

把上面两种组件类比到JSF不难发现,ActionForm就等同于JSF的backing-bean,而Action则等同于JSF的controller-bean,只不过JSF设计得更加优雅而已。

可能有读者会问:为何前面的示例都没有明确的backing-bean和controller-bean呢?回答这个问题之前,我们再看看开发Struts 2常常要开发的组件,只有一个:

Action:该Action类里既包含多个属性用于封装请求参数——也就是需要为每个表单控件提供一个与之对应的属性;也需要提供一个execute方法来处理用户请求。

也就是说,Struts 2中的Action等同于Struts 1的Action和ActionForm的混合,那到底是像Struts 1一样将ActionForm、Action分开好呢,还是像Struts 2一样只要Action好呢(虽然Struts 2也可以分开)?各种说法莫衷一是,追求架构的人推荐分开,因为更清晰;追求实用的人推荐合并,因为开发更方便。笔者这里就不硬性地下一个结论了,这完全取决于开发者的自由选择。

最后我们提供一个例子,将前面的LoginBean拆分成backing-bean和controller-bean两个类,再通过依赖注入将backing-bean注入controller-bean中。

首先是LoginBean拆分出来的backing-bean的代码。

程序清单:codes\02\2.5\otherScopeBean2\WEB-INF\src\lee\LoginBackBean.java

      public class LoginBackBean
      {
          //下面的三个属性都会直接与JSF标签绑定
          private String name;
          private String pass;
          private String err;
          //省略name属性的setter和getter方法
          …
          //省略pass属性的setter和getter方法
          …
          //省略err属性的setter和getter方法
          …
      }

从上面代码不难看出,这个backing-bean与Struts 1中的ActionForm类极为相似,它只需页面上每个表单控件(实际由JSF UI组件生成)提供一个属性,并为之提供对应的setter和getter方法即可。

接下来是LoginBean拆分出来的controller-bean的代码。

程序清单:codes\02\2.5\otherScopeBean2\WEB-INF\src\lee\LoginControllerBean.java

      public class LoginControllerBean
      {
          //session范围内UserBean
          private UserBean user;
          //与之管理的backing-bean
          private LoginBackBean backBean;
          //省略user属性的setter和getter方法
          …
          //省略backBean属性的setter和getter方法
          …
          //该方法被绑定到UI组件(按钮)的action属性
          public String valid()
          {
              if (backBean.getName().equals("crazyit")
                  && backBean.getPass().equals("leegang"))
              {
                  //这里实际上应该从数据库读取该用户的状态信息
                  getUser().setName("疯狂Java");
                  getUser().setGender("男");
                  return "success";
              }
              backBean.setErr("您的用户名和密码不符合");
              return "failure";
          }
      }

如果抛开上面LoginControllerBean中的user和backBean两个属性、它们的setter和getter方法,不难发现该Bean类的代码与Struts 1的Action代码基本相似。当然也有一定的区别,Struts 1中Action通过execute方法的形参、以硬编码的方式来获取ActionForm,但JSF的controller-bean则通过依赖注入、以松耦合的方式来获取backing-bean。

为此我们还需要在配置文件中分别配置backing-bean和controller-bean,并将backing-bean注入controller-bean中。配置片段如下所示:

程序清单:codes\02\2.5\otherScopeBean2\WEB-INF\faces-config-beans.xml

      <!-- 配置controller-bean -->
      <managed-bean>
          <!-- 设置托管Bean的名字 -->
          <managed-bean-name>loginController</managed-bean-name>
          <!-- 设置托管Bean的实现类 -->
          <managed-bean-class>lee.LoginControllerBean</managed-bean-class>
          <!-- 设置托管Bean实例的有效范围 -->
          <managed-bean-scope>request</managed-bean-scope>
          <managed-property>
              <property-name>user</property-name>
              <!-- 使用表达式初始化托管Bean的属性 -->
              <value>#{userBean}</value>
          </managed-property>
          <managed-property>
              <property-name>backBean</property-name>
              <!-- 将backing-bean注入controller-bean -->
              <value>#{login}</value>
          </managed-property>
      </managed-bean>
      <!-- 配置backing-bean -->
      <managed-bean>
          <!-- 设置托管Bean的名字 -->
          <managed-bean-name>login</managed-bean-name>
          <!-- 设置托管Bean的实现类 -->
          <managed-bean-class>lee.LoginBackBean</managed-bean-class>
          <!-- 设置托管Bean实例的有效范围 -->
          <managed-bean-scope>request</managed-bean-scope>
      </managed-bean>

上面程序中粗体字代码就是负责配置backing-bean,并将backing-bean注入controller-bean的代码,通过这种方式就将backing-bean、controller-bean分离,从而提供更清晰的程序结构,但这种方式增大了开发量。

将backing-bean、controller-bean分离之后,视图页面的UI组件绑定时也更清晰,普通表单UI组件绑定到backing-bean的属性,按钮UI组件则绑定到controller-bean的方法。代码如下所示:

      <!-- 输出国际化资源文件中的国际化信息 -->
      <h:outputText value="#{msg.namePrompt}"/>
      <!-- 将下面单行输入框的值绑定到login Bean的name属性 -->
      <h:inputText value="#{login.name}" /><br/>
      <!-- 输出国际化资源文件中的国际化信息 -->
      <h:outputText value="#{msg.passPrompt}"/>
      <!-- 将下面单行输入框的值绑定到login Bean的pass属性 -->
      <h:inputText id="pass" value="#{login.pass}"/><br/>
      <!-- 将下面按钮的动作绑定到loginController的valid方法 -->
      <h:commandButton action="#{loginController.valid}"
          value="#{msg.buttonTitle}" />

2.5.4 初始化托管Bean的属性

从前面示例已经看到了如何通过配置文件来初始化托管Bean的属性,JSF配置文件中使用<managed-property…/>子元素来初始化托管Bean的属性,<managed-property…/>子元素的内部结构如图2.18所示。

图2.18 <managed-property…/>子元素的内部结构

从图2.18可以看出,在<managed-property…/>子元素内有如下内容:

<property-name…/>:该元素是必需的,用于指定初始化的属性名。

<property-class…/>:该元素是可选的,用于指定初始化的属性类型,如果没有指定该元素,JSF将会自动判断需要初始化的属性类型。该属性值应该是一个全限定类名。

<map-entries…/>、<value…/>和<list-entries…/>三个互斥子元素必须出现其中之一,分别用于为Map类型的属性、非集合类型的属性、List类型的属性指定初始值。

对于<map-entries…/>元素而言,它用于为Map类型的属性执行初始化,因此该元素内无非是指定多个key-value对而已。<map-entries…/>元素可以包含如下内容:

<key-class…/>:该元素是可选的,用于指定Map中key-value中key的类型。如果没有指定该元素,JSF将会自动判断key的类型。该属性值应该是一个全限定类名。

<value-class…/>:该元素是可选的,用于指定Map中key-value中value的类型。如果没有指定该元素,JSF将会自动判断value的类型。该属性值应该是一个全限定类名。

<map-entry…/>:该元素用于配置一个key-value对。它包含的<key…/>子元素配置key值、<value…/>子元素配置value值。

对于<list-entries…/>元素而言,它用于为数组/List类型的属性执行初始化,因此该元素用于为List集合、数组配置多个元素。<list-entries…/>元素可以包含如下内容:

<value-class…/>:该元素是可选的,用于指定List中集合元素的类型。该属性值应该是一个全限定类名。

<value…/>:该元素用于指定一个集合元素。

需要指出的是,当我们为<key…/>、<value…/>等元素指定值时,这些值不仅可以是直接给出的值,也可以是形如#{xxx}的表达式,通过这种方式即可实现如Spring框架中的依赖注入。

掌握上面介绍的内容之后,我们来看一个对托管Bean进行初始化的例子,先看如下托管Bean代码。

程序清单:codes\02\2.5\initProp\WEB-INF\src\lee\TestBean.java

      public class TestBean
      {
          //下面的三个属性都会直接与JSF标签绑定
          private String name;
          private Map<String, Double> books;
          private List<String> schools;
          //无参数的构造器
          public TestBean()
          {
          }
          //初始化全部属性的构造器
          public TestBean(String name
              , Map<String, Double> books
              , List<String> schools)
          {
              this.name = name;
              this.books = books;
              this.schools = schools;
          }
          //省略name属性的setter和getter方法
          …
          //省略books属性的setter和getter方法
          …
          //省略schools属性的setter和getter方法
          …
      }

上面托管Bean中包含3个属性,其中name属性是String类型的属性,books是Map类型的属性,schools是List类型的属性。下面使用配置文件配置该托管Bean,并为它的3个属性执行初始化,如下配置片段就是配置托管Bean的代码。

程序清单:codes\02\2.5\initProp\WEB-INF\faces-config.xml

      <managed-bean>
          <!-- 设置托管Bean的名字 -->
          <managed-bean-name>test</managed-bean-name>
          <!-- 设置托管Bean的实现类 -->
          <managed-bean-class>lee.TestBean</managed-bean-class>
          <!-- 设置托管Bean实例的有效范围 -->
          <managed-bean-scope>request</managed-bean-scope>
          <!-- 初始化String类型的name属性 -->
          <managed-property>
              <property-name>name</property-name>
              <!-- 指定该属性的数据类型 -->
              <property-class>java.lang.String</property-class>
              <!-- 使用表达式初始化托管Bean的属性 -->
              <value>crazyit.org</value>
          </managed-property>
          <!-- 初始化Map类型的属性 -->
          <managed-property>
              <property-name>books</property-name>
              <map-entries>
                  <key-class>java.lang.String</key-class>
                  <value-class>java.lang.Double</value-class>
                  <!-- 每个map-entry配置一个key-value对 -->
                  <map-entry>
                      <key>疯狂Java讲义</key>
                      <value>99.0</value>
                  </map-entry>
                  <map-entry>
                      <key>疯狂Ajax讲义</key>
                      <value>69.0</value>
                  </map-entry>
                  <map-entry>
                      <key>疯狂XML讲义</key>
                      <value>65.0</value>
                  </map-entry>
              </map-entries>
          </managed-property>
          <!-- 初始化List类型的属性 -->
          <managed-property>
              <property-name>schools</property-name>
              <!-- 列出List的多个集合元素 -->
              <list-entries>
                  <!-- 每个value子元素配置一个List集合元素 -->
                  <value>小学</value>
                  <value>中学</value>
                  <value>疯狂Java实训营</value>
              </list-entries>
          </managed-property>
      </managed-bean>

上面配置文件配置了TestBean,并为它的name、books、schools三个属性都执行了初始化——你可以把它想象成Spring容器中的Bean,区别是JSF容器中的托管Bean可以直接使用页面上的UI组件来访问它们。

接下来在简单页面中使用UI组件来访问托管Bean,页面代码如下:

程序清单:codes\02\2.5\initProp\show.jsp

      <f:view>
      <html xmlns="http://www.w3.org/1999/xhtml">
      <head>
          <title>显示结果</title>
      </head>
      <body>
          <h3>显示结果</h3>
          name属性值:<h:outputText value="#{test.name}" /><br/>
          疯狂Java讲义的价格:<h:outputText
              value="#{test.books['疯狂Java讲义']}"/><br/>
          疯狂Ajax讲义的价格:<h:outputText
              value="#{test.books['疯狂Ajax讲义']}"/><br/>
          疯狂XML讲义的价格:<h:outputText
              value="#{test.books['疯狂XML讲义']}"/><br/>
          学校列表:<h:outputText value="#{test.schools}"/>
      </body>
      </html>
      </f:view>

将该应用部署在WebLogic服务器中,使用浏览器访问上面页面将看到如图2.19所示的效果。

图2.19 初始化托管Bean的属性

从图2.19可以看出,TestBean中3个属性(name、books、schools)都已初始化成功,虽然本示例直接给出了每个属性、每个List集合元素、每个Map Entry的值,实际上完全可使用表达式语言为它们指定值。

除此之外,前面介绍托管Bean配置时看到<managed-bean…/>元素内还可直接指定<map-entries…/>、<list-entries…/>子元素——从图2.15可以看出,<managed-bean…/>元素内以下3个元素是互斥的:

<map-entries…/>子元素。

<list-entries…/>子元素。

0个到多个<managed-property…/>子元素。

这里的<map-entries…/>、<list-entries…/>子元素并不是位于<managed-property…/>元素之内,而是直接位于<managed-bean…/>元素之内,显然它们不是用于初始化Bean的属性,它们的作用是初始化托管Bean,而不是初始化托管Bean的属性——当我们想在应用中初始化Map、List类型的托管Bean时,就可以通过在<managed-bean…/>元素内使用<map-entries…/>、<list-entries…/>子元素初始化它们。下面的配置文件示范了通过配置文件创建List、Map类型的托管Bean。

程序清单:codes\02\2.5\initProp\WEB-INF\faces-config.xml

      <!-- 直接配置一个类型为List的托管Bean -->
      <managed-bean>
          <!-- 设置托管Bean的名字 -->
          <managed-bean-name>trainings</managed-bean-name>
          <!-- 设置托管Bean的实现类 -->
          <managed-bean-class>java.util.ArrayList</managed-bean-class>
          <!-- 设置托管Bean实例的有效范围 -->
          <managed-bean-scope>request</managed-bean-scope>
          <!-- 初始化托管Bean里的集合元素 -->
          <list-entries>
              <!-- 每个value子元素配置一个List集合元素 -->
              <value>疯狂Java实训营</value>
              <value>www.crazyit.org</value>
              <value>广州为学教育</value>
          </list-entries>
      </managed-bean>
      <!-- 直接配置一个类型为Map的托管Bean -->
      <managed-bean>
          <!-- 设置托管Bean的名字 -->
          <managed-bean-name>scores</managed-bean-name>
          <!-- 设置托管Bean的实现类 -->
          <managed-bean-class>java.util.HashMap</managed-bean-class>
          <!-- 设置托管Bean实例的有效范围 -->
          <managed-bean-scope>request</managed-bean-scope>
          <map-entries>
              <!-- 每个map-entry配置一个key-value对 -->
              <map-entry>
                  <key>语文</key>
                  <value>79.5</value>
              </map-entry>
              <map-entry>
                  <key>数学</key>
                  <value>89.5</value>
              </map-entry>
              <map-entry>
                  <key>Java</key>
                  <value>92.0</value>
              </map-entry>
          </map-entries>
      </managed-bean>

上面配置文件中配置了两个初始化Bean,其中trainings Bean是一个List类型的托管Bean,scoresBean是一个Map类型的托管Bean。因为这两个托管Bean本身就是集合对象,因此我们可通过如下代码来访问这两个集合Bean。

      <f:view>
      <h3>显示结果</h3>
      List类型的Bean:<h:outputText value="#{trainings}" /><br/>
      Map类型的Bean:<h:outputText value="#{scores}"/><br/>
      </f:view>

将这个应用部署在WebLogic服务器中,通过浏览器访问上面页面可以看到如图2.20所示的结果。

2.5.5 通过FacesContext访问应用环境

前面介绍了通过session、application范围的托管Bean来将应用状态存入session、application范围内,也介绍了读取session、application范围的应用状态。对于大部分应用场景,程序使用上面介绍的方式来访问session、application范围的应用状态即可。

图2.20 直接创建List、Map类型的Bean

在一些特殊场景下,应用程序依然需要在托管Bean中访问Servlet API,JSF为这种需求提供了支持:通过JSF提供的FacesContext接口访问Servlet原生API。

当JSF应用向客户端呈现一个JSF页面响应时,应用将会创建一个新的视图并将它保存到FacesContext实例中,FacesContext实例中包含了所有与处理请求、创建响应相关联的信息。接下来应用将会获取该视图所需的对象引用,并调用FacesContext.renderResponse,该方法使得应用开始向客户端生成响应。

FacesContext接口包含了如下几个方法:

ResponseStream getResponseStream():返回应用响应对应的字节输出流。

ResponseWriter getResponseWriter():返回应用响应对应的字符输出流。

除此之外,FacesContext接口还提供了一个ExternalContext getExternalContext()方法,该方法返回一个ExternalContext对象,JSF应用通过该对象即可直接访问Servlet相关环境。

ExternalContext类提供了如下常用方法:

getApplicationMap():该方法返回一个Map对象,通过该Map对象即可访问application范围的所有状态数据。

getInitParameter(java.lang.String name):获取Web应用的初始化参数。该方法等同于ServletContext的getInitParameter(java.lang.String name)方法。

getInitParameterMap():获取Web应用所有初始化参数组成的Map对象。

getRequestContextPath():该方法等同于调用HttpServletRequest的getContextPath()方法。

getRequestParameterMap():获取该请求的所有请求参数的Map对象。

getRequestServletPath():该方法等同于调用HttpServletRequest的getServletPath()方法。

getResponse():获取应用原生的响应对象。

getSession(boolean create):获取应用原生的session对象。

getSessionMap():该方法返回一个Map对象,通过该Map对象即可访问session范围的所有状态数据。

redirect(java.lang.String url):执行重定向。

下面通过一个简单应用示范如何通过FacesContext来访问session范围的程序状态,并简单测试了ExternalContext类的几个方法。

程序清单:codes\02\2.5\FacesContext\WEB-INF\src\org\crazyit\jsf\BookBean.java

      public class BookBean
      {
          private String name;
          //绑定UI组件本身的属性
          private Double price;
          //无参数的构造器
          public BookBean()
          {
          }
          //初始化全部属性的构造器
          public BookBean(String name, Double price)
          {
              this.name = name;
              this.price = price;
          }
          //省略name属性的setter和getter方法
          …
          //省略price属性的setter和getter方法
          …
          //编写处理导航的方法
          public String process()
          {
              FacesContext fc = FacesContext.getCurrentInstance();
              //下面几行代码用于测试ExternalContext的方法
              ExternalContext ec = fc.getExternalContext();
              System.out.println(ec.getResponse()
                  instanceof HttpServletResponse);
              System.out.println(ec.getRequestServletPath());
              System.out.println(ec.getRequestContextPath());
              if (name.equals("疯狂Java讲义")
                  && price == 99)
              {
                  //将数据存入session范围
                  ec.getSessionMap().put("website", "crazyit.org");
                  return "success";
              }
              else
              {
                  ec.getSessionMap().put("tip", "您猜错了!");
                  return "failure";
              }
          }
      }

上面程序中第一段粗体字代码示范了通过ExternalContext来模拟Servlet API,后面的两行粗体字代码示范了通过ExternalContext操作session范围的属性。实际上,通过ExternalContext访问application范围的属性也与此类似。