转换和验证
虽然在 JSF Web 应用程序中使用转换和验证不一定要理解 JavaServer Faces 生命周期的基础知识,但是在深入转换和验证内容之前,最好对一些基本知识做一回顾。此外,掌握一点 JSF 生命周期技巧可以极大地帮助简化 Web 应用程序的开发工作。还有助于更好地理解 JSF 的可插入能力。
图 1 描绘了我们所说的“基本 JSF 生命周期”。 基本 是在暗示这只是一个典型的处理所提交表单值的请求-响应(request-and-response)场景。
显然,不同的场景对这里重点描述的生命周期有不同的影响。我们将在本文稍后介绍其中一些场景。现在,只需要注意转换和验证过程发生在应用请求值、处理验证 和呈现响应 阶段即可。
我们将在稍后介绍为什么转换和验证会在这些阶段出现,但是首先让我们澄清一个更基本的问题:转换 是什么?简单地说,转换是确保数据拥有正确的对象或者类型的过程。下面是两个典型的转换:
-
字符串值可以转换为 java.util.Date。
-
字符串值可以转换为 Float。
至于验证,它用于确保数据包含所期望的内容。下面是两个典型的验证:
-
java.util.Date 的格式为 MM/yyyy。
-
Float 在 1.0 和 100.0 之间。
关注生命周期阶段
转换和验证的主要目的是确保在更新模型数据之前已经经过了正确的无害处理。之后,当需要调用应用程序方法用这些些数据实际做一些事情 时,就可以有把握地假定模型的某些状态。转换和验证使您可以侧重于业务逻辑,而不是侧重于对输入数据进行繁琐的资格认定,比如 null 检验、长度限定、范围边界,等等。
因此,在更新模型数据 生命周期阶段中,在组件数据被绑定到 backing bean 模型之前 进行转换和验证处理是有道理的。正如图 1 所示,转换发生在应用请求值阶段,而验证发生在处理验证阶段。图 2 突出显示了这些阶段。
关于 immediate 属性
注意,图 2 中描绘的转换和验证过程表示了将 UIInput 组件的 immediate 属性设置为 false 时的应用程序流程。如果这个属性设置为 true,那么转换和验证会发生在生命周期更早的时期,即应用请求值阶段(参见图 3)。对使用 immediate 属性的详细讨论超出了本文的范围,但是在某些情况下,比如管理动态清单(可能您还记得,本系列的上一篇文章中曾介绍过),它很有用,它甚至可以绕过验证(在与 UICommand 组件结合使用时)。能想像一个需要完全绕过验证的应用程序吗?
图 3 展示了当 immediate 属性设置为 true 时,在 JSF 应用程序生命周期中的哪些地方进行转换和验证。
实际的例子
下面,我们将用一个示例应用程序展示所讨论的概念。本月的示例应用程序将展示 JSF 的转换和验证能力。记住,这个示例应用程序非常简单,没有追求一些不必要的面面俱到:无论如何,我们的目的不是构建一个在真实世界中使用的应用程序!这个示例应用程序将展示以下几点:
-
使用标准 JSF 转换器转换表单字段数据。
-
使用标准 JSF 验证组件验证表单字段数据。
-
如何编写自定义转换器和验证器。
-
如何在 faces-config.xml 文件中注册自定义转换器和验证器。
-
如何定制默认错误消息。
这个示例应用程序是一个简单的用户注册表单。我们的目标是收集用户数据,比如姓名、年龄、电子邮箱地址和电话号码。然后,我们将展示如何利用 JSF 转换和验证确保收集的数据对于模型是适合的。
这个应用程序使用了三个 JSPi 页:
-
index.jsp 将用户定向到 UserRegistration.jsp。
-
UserRegistration.jsp 包含应用程序的表单字段。
-
results.jsp 通知应用程序用户已经注册。
我们将首先分析编写 JSF 转换过程的选择。
JSF 转换
如前所述,转换是确保数据对象或者类型正确的一个过程,因此,我们将字符串值转换为其他类型,比如 Date 对象、基本浮点型或者 Float 对象。可以使用自带的转换器,也可以编写自定义的转换器。
JSF 提供了许多标准数据转换器。也可以通过实现 Converter 接口插入自定义转换器,但是这些将在后面进行介绍。下表显示了 JSF 进行简单数据转换所使用的转换器 id 及其对应的实现类。大多数数据转换是自动发生的。
javax.faces.BigDecimal | javax.faces.convert.BigDecimalConverter |
javax.faces.BigInteger | javax.faces.convert.BigIntegerConverter |
javax.faces.Boolean | javax.faces.convert.BooleanConverter |
javax.faces.Byte | javax.faces.convert.ByteConverter |
javax.faces.Character | javax.faces.convert.CharacterConverter |
javax.faces.DateTime | javax.faces.convert.DateTimeConverter |
javax.faces.Double | javax.faces.convert.DoubleConverter |
javax.faces.Float | javax.faces.convert.FloatConverter |
图 4 展示了用户年龄的默认转换。JSF 标签配置如下:
|
各种情况的转换器
UserRegistration.user.age 表示一个值绑定属性,它的类型为 int。对于基本型或者 BigInteger/ BigDecimal 的绑定,JSF 选择了标准转换器。不过,还可以通过 <f:converter/> 标签,利用一个特定的转换器来增加粒度,如下所示。
|
在图 5 中,可以看到 JSF 使用标准转换器的场景。在这种情况下,虽然年龄实际上是一个有效的整数,但转换仍然会失败,因为该值不是短整型的。
选择日期格式样式
尽管在默认情况下,JSF 可以很好地处理基本型及类似的类型,但是在处理日期数据时,必须指定转换标签 <f:convertDateTime/>。这个标签基于 java.text 包,并使用短、长和自定义样式。下面是一个例子:
|
这个例子展示了如何用 <f:convertDateTime/> 确保用户的生日可以转换为格式为 MM/yyyy(月/年)的日期对象。请参阅 JSF 的 java.text.SimpleDataFormat (在 参考资料 中),以获取模式列表。
其他样式
除了可以转换日期和时间格式外,JSF 还提供了处理像百分数或者货币数据这类值的特殊转换器。这个转换器处理分组(如逗号)、小数、货币符号等。例如,以下 <f:convertNumber/> 的用法就是处理货币的一种技巧:
|
在图 6 中,可以看到一些格式编排不正确的货币数据,以及所导致的转换错误。
自定义转换
如果需要将字段数据转换为特定于应用程序的值对象,则需要自定义数据转换,如下面例子所示:
-
String 转换为 PhoneNumber 对象 (PhoneNumber.areaCode、PhoneNumber.prefix、 ...)。
-
String 转换为 Name 对象 (Name.first、Name.last)。
-
String 转换为 ProductCode 对象 (ProductCode.partNum、ProductCode.rev、 ...)。
要创建自定义转换器,必须完成以步骤:
-
实现 Converter 接口(也就是 javax.faxes.convert.Converter)。
-
实现 getAsObject 方法,它将一个字段(字符串)转换为一个对象(例如,PhoneNumber)。
-
实现 getAsString 方法,它将一个对象(如 PhoneNumber)转换为一个字符串。
-
在 Faces 上下文中注册自定义转换器。
-
用 <f:converter/> 标签在 JSP 中插入这个转换器。
您可以自己看到如何在 JSF 应用程序生命周期中加入这些步骤。在图 7 中,JSF 在应用请求值阶段调用自定义转换器的 getAsObject 方法。转换器必须在这里将请求字符串转换为所需的对象类型,然后返回这个对象,将它存储在相应的 JSF 组件中。如果该值被返回呈现在视图中,那么 JSF 将在呈现响应阶段调用 getAsString 方法。这意味着转换器还要负责将对象数据转换回字符串表示形式。
图 7. 自定义转换器 getAsObject 和 getAsString 方法
创建自定义转换器
我们将使用一个案例分析来展示 Converter 接口、getAsObject 和 getAsString 方法的实现,同时还将展示如何在 Faces 上下文中注册这个转换器。
这个案例分析的目的是将一个单字段字符串值转换为一个 PhoneNumber 对象。我们将一步一步地完成这个转换过程。
第 1 步:实现 Converter 接口
这一步实现 Converter 接口。
|
第 2 步:实现 getAsObject 方法
这一步将一个字段值转换为一个 PhoneNumber 对象。
|
第 3 步:实现 getAsString 方法
这一步将一个 PhoneNumber 对象转换为一个字符串。
|
第 4 步:在 faces 上下文中注册自定义转换器
第 4 步可以以两种方式执行。第一种选择使用(比如)arcmind.PhoneConverter 的 id 来注册 PhoneConverter 类。JSP 页中的 <f:converter/> 标签会使用这个 id。下面是第 4 步的选项 1 的代码:
|
另一种方法是注册 PhoneConverter 类来自动处理所有 PhoneNumber 对象,如下所示。
|
第 5 步:在 JSP 中使用转换器标签?
自然,下一步的执行取决于所选的注册方法。如果选择使用 arcmind.PhoneConverter 的 id 来注册 PhoneConverter 类,那么就使用 <f:converter/> 标签,如下所示。
|
如果选择注册 PhoneConverter 类来自动 处理所有 PhoneNumber,那么就不需要在 JSP 页中使用 <f:converter/> 标签。下面是第 5 步的不带转换器标签的代码。
|
这样,我们已经完成了这个示例应用程序的转换处理代码!到目前为止完成的应用程序如下所示。
JSF 验证
如前所述,JSF 验证可以确保应用程序数据包含预期的内容,例如:
-
java.util.Date 为 MM/yyyy 格式。
-
Float 在 1.0 和 100.0 之间。
在 JSF 中有 4 种验证:
-
自带验证组件。
-
应用程序级验证。
-
自定义验证组件(它实现了 Validator 接口)。
-
在 backing bean 中的验证方法(内联)。
我们将在下面的讨论中介绍并展示每一种形式。
JSF 验证生命周期和组件
图 9 显示了用户注册表单中名字字段的生命周期案例分析。代码引用被有意解释为伪代码(pseudo-code)。
下面是 JSF 提供的一组标准验证组件:
-
DoubleRangeValidator:组件的本地值必须为数字类型,必须在由最小和/或最大值所指定的范围内。
-
LongRangeValidator:组件的本地值必须为数字类型,并且可以转换为长整型,必须在由最小和/或最大值所指定的范围内。
-
LengthValidator:类型必须为字符串,长度必须在由最小和/或最大值所指定的范围内。
标准验证
在我们的示例应用程序中,用户的年龄可以是任意有效的整数(byte、short、int)。因为将年龄设置为(比如说)-2 是无意义的,所以可能要对这个字段添加一些验证。下面是一些简单的验证代码,用以确保年龄字段中的数据模型完整性:
|
完成年龄字段后,可能希望指定对名字字段的长度加以限制。可以像这样编写这个验证:
|
图 10 显示了由上面标准验证示例所生成的默认详细验证消息。
尽管 JSF 自带的验证在许多情况下都可以满足,但是它有一些局限性。在处理电子邮件验证、电话号码、URL、日期等数据时,有时编写自己的验证器会更好一些,不过我们将在稍后对此进行讨论。
应用程序级验证
在概念上,应用程序级验证实际上是业务逻辑验证。JSF 将表单和/或字段级验证与业务逻辑验证分离开。应用程序级验证主要需要在 backing bean 中添加代码,用这个模型确定绑定到模型中的数据是否合格。对于购物车,表单级验证可以验证输入的数量是否有效,但是需要使用业务逻辑验证检查用户是否超出了他或者她的信用额度。这是在 JSF 中分离关注点的另一个例子。
例如,假定用户单击了绑定到某个操作方法的按钮,那么就会在调用应用程序阶段调用这个方法(有关的细节,请参见上面的图 1)。假定在更新模型阶段进行了更新,那么在对模型数据执行任何操纵之前,可以添加一些验证代码,根据应用程序的业务规则检查输入的数据是否有效。
例如,在这个示例应用程序中,用户单击了 Register 按钮,这个按钮被绑定到应用程序控制器的 register() 方法。我们可以在 register() 方法中添加验证代码,以确定名字字段是否为 null。如果该字段为 null,那么还可以在 FacesContext 中添加一条消息,指示相关组件返回到当前页。
其实它现在并不是业务规则逻辑的一个好例子。更好的例子是检查用户是否超出了她或者她的信用额度。在该例中,不是检查字段是否为空,我们可以调用模型对象的方法来确保当前用户已经不在系统中。
图 11 描绘了这个过程。
注意在 register() 方法中,消息是如何以 ${formId}:${fieldId} 的形式添加到 FacesContext 中的。图 12 显示了消息与组件 id 之间的关系。
应用程序级验证的优缺点
应用级验证非常直观并且容易实现。不过,这种形式的验证是在其他形式的验证(标准、自定义、组件)之后发生的。
应用程序级验证的优点如下:
-
容易实现。
-
不需要单独的类(自定义验证器)。
-
不需要页编写者指定验证器。
应用程序级验证的缺点如下:
-
在其他形式的验证(标准、自定义)之后发生。
-
验证逻辑局限于 backing bean 方法,使得重用性很有限。
-
在大型应用程序和/或团队环境中可能难于管理。
最终,应用程序级验证只应该用于那些需要业务逻辑验证的环境中。
自定义验证组件
对于标准 JSF 验证器不支持的数据类型,则需要建立自己的自定义验证组件,其中包括电子邮件地址和邮政编码。如果需要明确控制显示给最终用户的消息,那么还需要建立自己的验证器。在 JSF 中,可以创建可在整个 Web 应用程序中重复使用的可插入验证组件。
创建自定义验证器的步骤如下,我们将一步步地分析:
-
创建一个实现了 Validator 接口的类 (javax.faces.validator.Validator)。
-
实现 validate 方法。
-
在 faces-confix.xml 文件中注册自定义验证。
-
在 JSP 页中使用 <f:validator/> 标签。
下面是创建自定义验证器的分步示例代码。
第 1:实现 Validator 接口
第一步是实现 Validator 接口。
|
第 2 步:实现验证方法
接下来,需要实现 validate 方法。
throws ValidatorException { |
第 3 步:在 FacesContext 中注册自定义验证器
您现在应该熟悉在 FacesContext 中注册自定义验证器的代码了。
|
第 4 步:在 JSP 中使用 <f:validator/> 标签
<f:validator/> 标签声明使用 zipCodeValidator。<f:attribute/> 标签将 plus4Optional 属性设置为 true。注意,它定义了 inputText 组件的属性,而不是 验证器的属性!
|
为了读取 zipCodeinputText 组件的 plus4Optional 属性,请完成以下步骤::
|
总体而言,创建自定义验证器是相当直观的,并且可以使该验证在许多应用程序中重复使用。缺点是必须创建一个类,并在 faces 上下文中管理验证器注册。不过,通过创建一个使用这个验证器的自定义标签,使其看上去像是一个自带的验证,可以进一步实现自定义验证器。对于常见的验证问题,如电子邮件验证,这种方法可以支持这样一种设计理念,即代码重用和一致的应用程序行为是最重要的。
backing bean 中的验证方法
作为创建单独的验证器类的替代方法,可以只在 backing bean 的方法中实现自定义验证,只要这个方法符合 Validator 接口的 validate 方法的参数签名即可。例如,可以编写以下方法:
|
之后,可通过如下所示的 validator 属性在 JSF 中使用这个方法:
|
JSF 用 validateEmail 方法对绑定到 user.email 模型属性的 inputText 组件值进行自定义验证。如果电子邮件格式无效,那么就在相关组件的 faces 上下文中添加消息。考虑到这种验证方法实际上是 backing bean 的一部分,为什么通常必须用某个值与相关组件的关联来评估该值,而不是直接检查本地 bean 属性呢?线索就在前面的生命周期图中。如果现在不能马上找到答案,也不要担心,我们将在本文的最后对此加以说明。
默认验证
注意上面 email 标签的 required 属性。利用 required 属性是一种默认 验证形式。如果这个属性是 true,那么相应的组件必须有一个值。一个重要的说明:如果 required 属性为 false,那么就不用对这个标签/组件指派验证,这样,JSF 将跳过对这个组件的验证,并让值和组件的状态保持不变。
图 13 概述了我们讨论过的验证形式。
自定义消息
您可能注意到了,JSF 提供的默认转换和验证消息非常长,这会让那些总是输入无效表单数据的最终用户感到困惑和恼火。幸运的是,您可以通过创建自己的消息资源绑定来改变 JSF 提供的默认消息。jsf-impl.jar (或类似的文件中)中包含了一个 message.properties 文件,该文件包含图 14 所示的默认消息。
通过创建自己的 message.properties 文件并断开指定场所的 faces 上下文中绑定的消息资源,您可以更改默认消息,如图 15 所示。
关于在 JSF 中创建自定义转换和验证消息的更多内容请参前阅 参考资料。
处理 JSF 生命周期
我们在本文前面留下了一些问题让您考虑,现在可以解决它们了!我们提到的一件事是对 UICommand 按钮使用 immediate 属性,比如 commandLink 或者 commandButtons。现在请您考虑希望在什么样的场景中跳过验证。
基本上只要用户需要输入数据,就需要对这个数据进行验证。不过,如果整个数据项是可选的,那么就不需要进行验证。一种避免 JSF 生命周期的验证阶段的方法是利用 UICommand 组件的 immediate 属性,该属性可以在处理验证阶段之前 的应用请求值阶段期间(而不是在处理验证阶段 之后 的调用应用程序阶段)强制调用这个操作。
immediate 属性允许您通过标准浏览规则控制页流程,并绕过验证。可以针对特定的场景实现这项技术,比如带有可选步骤和/或表单的在线向导(如当用户单击 Skip 按钮以进入下一视图),或者在用户因为某种原因而取消某个表单的情况下。
我们在本文中留下的第二个问题是:既然验证方法实际上是 backing bean 的一部分,那么为什么通常必须利用组件关联来判断它的值。请参阅前面的 JSF 应用程序生命周期,看看您能否找到答案。
这里的密诀是:尽管 validateEmail 嵌入验证方法是实际的 backing bean 的一部分,但是该方法必须通过组件关联来引用这,而不是直接访问本地属性来引用值。由于验证发生在组件值绑定到模型之前(在更新模型值阶段),所以模型处于未知状态。因此,必须编写嵌入自定义验证逻辑,就像使用一个自定义 Validator 对象处理验证一样。这也解释了维护相同方法签名的需求。
这些尚待解决的枝节问题有什么意义呢,当然,它们最终将我们带回 JSF 应用程序生命周期。将这些问题汇总在一起,就能体现充分理解生命周期的重要性 —— 向后、向前或由内向外,这样您就可以在需要的时候操纵它。
结束语
在本文中我们讨论了相当多的 JSF 转换和验证的基本内容。事实上,我们讨论了在自己的应用程序中使用这些过程需要知道的大部分内容(至少对这个版本的 JSF 而言)!