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范围的属性也与此类似。