组件模型的关键考验就是:能否从第三方供应商购买组件,并把它们插入应用程序?与可购买可视 Swing 组件一样,也可以购买 Java ServerFaces (JSF) 组件!需要一个好玩的日历?可以在开源实现和商业组件之间选择。可以选择购买一个,而不是自行开发复杂的基于 Web 的 GUI 组件。
JSF 拥有一个与 AWT 的 GUI 组件模型类似的组件模型。可以用 JSF 创建可重用组件。但不幸的是,存在一个误解:用 JSF 创建组件很困难。不要相信这些从未试过它的人们的 FUD!开发 JSF 组件并不困难。由于不用一遍又一遍重复相同的代码,可以节约时间。一旦创建了组件,就可以容易地把组件拖到任何 JSP、甚至任何 JSF 表单中,如果正在处理的站点有 250 个页面,这就很重要了。JSF 的大多数功能来自基类。因为所有的繁重工作都由 API 和基类完成,所以 JSF 把组件创建变得很容易。
贯穿这个系列,我一直在试图帮助您克服造成许多 Java 开发人员逃避使用 JSF 技术的 FUD。我讨论了对这项技术的基本误解,介绍了它的底层框架和它最有价值的开发特性。有了这些基础工作之后,我认为您已经可以采取行动,开发自己的定制 JSF 组件了。使用 JSF 的东西,我敢保证要比您想像的要更加容易,而且从节约的时间和精力上来说,回报如此之多,多得不能忽略。
这篇文章中的示例是用 JDK 1.5 和 Tomcat 开发的。请单击页面顶部的 示例代码 下载示例源代码。注意,与以前的文章不同,这篇文章没有关联的 build 文件,因为我特意把它留给您作为一个练习了。只要设置 IDE 或编译器,把 /src 中的类编译到 /webapp/WEB-INF/classes ,并在 /webapp/WEB-INF/lib 中包含所有 JAR 文件(以及 servlet-api.jar 和 jsp-api.jar ,它们包含在 Tomcat 中)。
JSF 组件模型与 AWT GUI 组件模型类似。它有事件和属性,就像 Swing 组件模型一样。它也有包含组件的容器,容器也是组件,也可以由其他容器包含。从理论上说,JSF 组件模型分离自 HTML 和 JSP。JSF 自带的标准组件集里面有 JSP 绑定,可以生成 HTML 渲染。
JSF 组件的示例包括日历输入组件和 HTML 富文本输入组件。您可能从来没时间去编写这样的组件,但是如果它们已经存在,那会如何呢?通过把常用功能变成商品,组件模型降低了向 Web 应用程序添加更多功能的门槛。
组件的功能通常围绕着两个动作:解码和编码数据。 解码 是把进入的请求参数转换成组件的值的过程。 编码 是把组件的当前值转换成对应的标记(也就是 HTML)的过程。
JSF 框架提供了两个选项用于编码和解码数据。使用 直接实现 方式,组件自己实现解码和编码。使用 委托实现 方式,组件委托渲染器进行编码和解码。如果选择委托实现,可以把组件与不同的渲染器关联,会在页面上以不同的方式渲染组件;例如多选列表框和一列复选框。
因此,JSF 组件由两部分构成:组件和渲染器。JSF 组件 类定义 UI 组件的状态和行为; 渲染器 定义如何从请求读取组件、如何显示组件 —— 通常通过 HTML 渲染。渲染器把组件的值转换成适当的标记。事件排队和性能验证发生在组件内部。
在图 1 中可以看到数据编码和解码出现在 JSF 生命周期中的什么阶段(到现在,我希望您已经熟悉 JSF 生命周期了)。
图 1. JSF 生命周期和 JSF 组件
|
所有 JSF 组件的基类是
UIComponent
。在开发自己的组件时,需要继承
UIComponentBase
,它扩展了
UIComponent
并提供了
UIComponent
中所有抽象方法的默认实现。
组件拥有双亲和标识符。每个组件都关联着一个 组件类型 ,组件类型用于在 face 的上下文配置文件(faces-config.xml)中登记组件。可以用 JSF-EL (表达式语言)把 JSF 组件绑定到受管理的 bean 属性。可以把表达式关联到组件上的任何属性,这样就允许用 JSF-EL 设置组件的属性值。在创建使用 JSF-EL 绑定的组件属性时,需要创建值绑定表达式。在调用绑定属性的 getter 方法时,除非 setter 方法已经设置了值,否则 getter 方法必须用值绑定获得值。
组件可以作为
ValueHolder
或
EditableValueHolder
。
ValueHolder
与一个或多个
Validator
和
Converter
相关联;所以 JSF UI 组件也与
Validator
和
Converter
关联(请参阅
参考资料
获得更多关于 JSF 验证和转换的内容。)
像表单字段组件这样的组件拥有一个
ValueBinding
,它必须绑定到 JavaBean 的读写属性。组件可以调用
getParent
方法访问它们的双亲,也可以调用
getChildren
方法访问它们的子女。组件也可以有
facet 组件
,facet 组件是当前组件的子组件,可以调用
getFacets
方法访问它,这个方法返回一个映射。Facets 是著名的子组件。
这里描述的许多组件的概念将会是接下来展示的示例的一部分,所以请记住它们!
|
我们用一个又好又容易的示例来开始 JSF 组件的开发:我将展示如何渲染 Label 标记(示例:
<label>Form Test</label>
)。
下面是我要采取的步骤:
-
扩展 UIComponent
-
创建一个类,扩展
UIComponent
- 保存组件状态
- 用 faces-config.xml 登记组件
-
创建一个类,扩展
-
定义渲染器或者内联地实现它
- 覆盖 encode
- 覆盖 decode
- 用 faces-config.xml 登记渲染器
-
创建定制标记,继承 UIComponentTag
- 返回渲染器类型
- 返回组件类型
- 设置可能使用 JSF 表达式的属性
Label 示例将演示 JSF 组件开发的以下方面:
- 创建组件
- 直接实现渲染器
- 编码输出
- 把定制标记与组件关联
返回 图 1 ,可以看到在这个示例中会有两个生命周期属性在活动。它们是 Apply Request Value 和 Render Response 。
在图 2 中,可以看到在 JSP 中如何使用 Label 标记的(
<label>Form Test</label>
)。
图 2. 在 JSP 中使用 JSF 标记
第一步是创建一个组件,继承
UIOutput
,后者是
UIComponent
的子类。 除了继承这个类之外,我还添加了组件将会显示的 label 属性,如清单 1 所示:
清单 1. 继承 UIComponent 并添加 label
|
接下来要做的是保存组件状态。JSF 通常通过会话、隐藏表单字段、cookies 等进行实际的存储和状态管理。(这通常是用户配置的设置)。要保存组件状态,需要覆盖组件的
saveState
和
restoreState
方法,如清单 2 所示:
清单 2. 保存组件状态
|
可以注意到,我使用的是 JDK 1.5。我对编译器进行了设置,所以我必须指定 override 注释,以便指明哪些方法要覆盖基类的方法。这样做可以更容易地标识出 JSF 的钩子在哪。
创建组件的最后一步是用 faces-config.xml 登记它,如下所示:
|
下面要做的是内联地定义渲染器的功能。稍后我会介绍如何创建独立的渲染器。现在,先从编码 Label 组件的输出、显示 label 开始,如清单 3 所示:
清单 3. 编码组件的输出
|
注意,响应写入器(
javax.faces.context.ResponseWriter
)可以容易地处理 HTML 这样的标记语言。清单 3 的代码输出 <label> 元素体内的 label 的值。
下面显示的 family 属性用来把 Label 组件与渲染器关联。虽然目前 Label 组件还不需要这个属性(因为还没有独立的渲染器),但是在这篇文章后面,在介绍如何创建独立渲染器的时候,会需要它。
|
如果正在使用来自 Sun Microsystems 的 JSF 参考实现(不是 MyFaces 实现),那么就不得不在组件创建代码中添加下面一段:
|
Sun 的 JSF RI 期望,在组件没有渲染器的时候,渲染器会发送一个空指针异常。MyFaces 实现不要求处理这个需求,但是在代码中包含以上方法依然是个好主意,这样组件既可以在 MyFaces 环境中工作也可以在 JSF RI 环境中工作了。
|
JSF 组件不是天生绑定到 JSP 上的。要连接起 JSP 世界和 JSF 世界,需要能够返回组件类型的定制标记(然后在 faces-context 文件中登记)和渲染器,如图 3 所示。
图 3. 连接 JSF 和 JSP
注意,由于没有独立的渲染器,所以可以给
getRendererType()
返回 null 值。还请注意,必须已经把
label
属性的值从定制标记设置到组件上,如下所示:
|
记住,
Tag
设置从 JSP 到 Label 组件的绑定,如图 4 所示。
图 4. 绑定 JSF 和 JSP
现在要做的全部工作就是创建一个 TLD(标记库描述符)文件,以登记定制标记,如清单 4 所示:
清单 4. 登记定制标记
|
一旦定义了 TLD 文件,就可以开始在 JSP 中使用标记了,如下面示例所示:
|
现在就可以了 —— 开发一个简单的 JSP 组件不需要更多了。但是如果想创建稍微复杂一些的组件,针对更复杂的使用场景时该怎么办?请继续往下看。
|
在下一个示例中,我将介绍如何创建这样一个组件(和标记),它可以记住最后一个人离开的位置。Field 组件把多个组件的工作组合到一个组件中。复合组件是 JSF 组件开发的重点,会节约大量时间!
Field 组件把标签、文本输入和消息功能组合到一个组件。Field 的文本输入功能允许用户输入文本。如果有问题(例如输入不正确),它的标签功能会显示红色,还会显示星号(*)表示必需的字段。它的消息功能允许它在必要的时候写出出错消息。
Field 组件示例演示了以下内容:
- UIInput 组件
- 处理值绑定和组件属性
- 解码来自请求参数的值
- 处理出错消息
与 Label 组件不同,Field 组件使用独立渲染器。如果为一个基于 HTML 的应用程序开发组件,那么不要费力使用独立渲染器。这么做是额外的无用功。如果正在开发许多 JSF 组件,打算卖给客户,而针对的客户又不止一个,那么就需要独立的渲染器了。简而言之,渲染器适用于商业框架的开发人员,不适用于开发内部 Web 应用程序的应用程序开发人员。
由于我已经介绍了创建组件、定义渲染器以及创建定制标记的基本步骤,所以这次我让代码自己说话,我只点出几个重要的细节。在清单 5 中,可以看到在典型的应用程序示例中如何使用 Field 标记的:
清单 5. Field 标记
|
以上标记输出以下 HTML:
|
图 5 显示了浏览器中这些内容可能显示的效果。
图 5. Field 组件
清单 6 显示了创建 Field 组件的代码。因为这个组件负责输入文本而不仅仅是输出它(像 Label 那样),所以要从继承
UIInput
开始,而不是从继承
UIOutput
开始。
清单 6. Field 继承 UIInput
|
可以注意到,代表片段中遗漏了编码方法。这是因为编码和解码发生在独立的渲染器中。我稍后会介绍它。
虽然 Label 组件只有一个属性(JSP 属性),可是 Field 组件却有多个属性,即
label
、
errorStyle
、
errorStyleClass
和
value
。
label
和
value
属性位于 Field 组件的核心,而
errorStyle
和
errorStyleClass
是特定于 HTML 的。因为这些属性是特定于 HTML 的,所以不需要让它们作为 Field 组件的属性;相反,只是把它们作为组件属性进行传递,只有渲染器知道这些属性。
像使用 Label 组件时一样,需要用定制标记把 Field 组件绑定到 JSP,如清单 7 所示:
清单 7. 为 FieldComponent 创建定制标记
|
从概念上说,在上面的代码和 Label 组件之间找不出太大区别。但是,在这个示例中,
setProperties
方法有些不同:
|
虽然
label
属性传递时的方式与前面的示例相同,但是
errorStyleClass
和
errorStyle
属性不是这样传递的。相反,它们被添加到 JSF 组件的
属性映射
中。
Renderer
类会使用属性映射去渲染类和样式属性。这个设置允许特定于 HTML 的代码从组件脱离。
这个修订后的
setProperties
方法实际的值绑定代码也有些不同,如下所示。
|
这个代码允许 Field 组件的
value
属性绑定到后台 bean。出于示例的原因,我把
CDManagerBean
的 title 属性绑定到 Field 组件,像下面这样:
value="#{CDManagerBean.title}
。值绑定是用
Application
对象创建的。
Application
对象是创建值绑定的工厂。这个组件拥有保存值绑定的特殊方法,即
setValueBinding
;可以有不止一个值绑定。
最后介绍渲染器,但并不是说它不重要。独立渲染器必须考虑的主要问题是解码(输入) 和编码(输出)。Field 组件做的编码比解码多得多,所以它的渲染器有许多编码方法,而只有一个解码方法。在清单 8 中,可以看到 Field 组件的渲染器:
清单 8. FieldRenderer 扩展自 Renderer
|
正如前面提到的,渲染器做的主要工作就是解码输入和编码输出。我先从解码开始,因为它是最容易的。
FieldRenderer
的 decode 方法如下所示:
|
Label 组件不需要进行解码,因为它是一个
UIOutput
组件。
Field
组件是一个
UIInput
组件,这意味着它接受输入,所以
必须
进行解码。decode 方法可以从会话、cookie、头、请求等处读取值。在大多数请问下,decode 方法只是像上面那样从请求参数读取值。
Field 渲染器
的 decode 方法从组件得到
clientId
,以标识要查找的请求参数。给定组件容器的路径,
clientId
被计算成为组件的全限定名称。而且,因为示例组件在表单中(是个容器),所以它的
clientid
应当是
nameOfForm:nameOfComponent
这样的,或者是示例中的
cdForm:artist、cdForm:price、cdForm:title
。decode 方法的最后一步是把提交的值保存到组件(稍后会转换并验证它,请参阅
参考资料
获取更多关于验证和转换的内容)。
编码方法没什么惊讶的。它们与 Label 组件中看到的类似。第一个方法
encodeBegin
,委托给三个帮助器方法
encodeLabel
、
encodeInput
和
encodeMessage
,如下所示:
|
encodeLabel
方法负责在出错的时候,把标签的颜色改成红色(或者在样式表中指定的其他什么颜色),并用星号 (*) 标出必需的字段,如下所示:
|
首先,
encodeLabel
方法检查是否有错误,如果有就输出
errorStyle
和
errorStyleClass
(更好的版本是只有在它们不为空的时候才输出 —— 但是我把它留给您做练习!)。然后帮助器方法会检查组件是不是必需的字段,如果是,就输出星号。
encodeMessages
和
encodeInput
方法做的就是这件事,即输出出错消息并为 Field 组件生成 HTML 输入的文本字段。
您可能已经注意到,有一个方法我还没有介绍。这个方法就是这个类中的“黑马”方法。如果您阅读
Renderer
(所有渲染器都要扩展的抽象类)的 javadoc,您可能会感觉到这样的方法是不需要的,现有的就足够了:这就是我最开始时想的。但是,您和我一样,都错了!
实际上,基类
Renderer
并不
自动调用
Renderer
子类的相关转换器 —— 即使
Renderer
的 javadoc 和 JSF 规范建议它这样做,它也没做。MyFaces 和 JSF RI 拥有为它们的渲染器执行这个魔术的类(特定于它们的实现),但是在核心 JSF API 中并没有涉及这项功能。
相反,需要使用方法
getConvertedValues
锁定相关的转换器并调用它。清单 9 显示的方法根据值绑定的类型找到正确的转换器:
清单 9. getConvertedValues 方法
|
清单 9 的代码添加了
Render
javadoc 和 JSF 规范都让您相信应当是自动执行的功能,而实际上并不是。另一方面,请注意如果
没有
独立的
Renderer
,就
不需要
以上(
getConvertedValues
)方法。
UIComponentBase
类(Field 组件的超类)在直接渲染器的情况下提供了这个功能。请接受我的建议,只在特别想尝试或者在编写商业框架的时候,才考虑采用渲染器。在其他情况下,它们不值得额外的付出。
如果想知道如何把组件和渲染器关联,那么只要看看图 6 即可。
图 6. 把渲染器映射到组件
定制标记有两个方法,分别返回组件类型和渲染器类型。这些方法用于查找配置在 faces-config.xml 中的正确的渲染器和组件。请注意(虽然图中没有)组件必须返回正确的 family 类型。
|
通过这些内容,您已经切实地了解了 JSF 组件开发的核心。当然,在这个领域还有许多其他主题需要涉及 —— 包括发出组件事件、国际化组件、创建
UICommand
样式的组件,以及更多。