3.6 自定义校验器
读者不难发现JSF内置支持的校验器的校验功能比较有限,实际应用中往往还需要进行更复杂的输入校验,此时就必须开发自定义校验器。
3.6.1 开发自定义校验器
JSF提供了一个javax.faces.validator.Validator接口,JSF校验器都应该实现该接口,实现该接口就必须实现该接口内定义的如下方法:
validate(FacesContext context, UIComponent component, java.lang.Object value):重写该方法进行输入校验。当输入校验失败时,该方法可以抛出一个ValidatorException异常,该异常包含的消息将会作为校验失败的错误提示。
该方法中3个形参的意义如下:
context:当前的FacesContext对象。
component:正被该校验器执行校验的UI组件。
value:正在被校验的UI组件的本地值。
假设现在有一个更复杂的注册页面,该页面中包含用户名、E-mail两个表单域,现在要求用户输入的E-mail必须是一个合法的电子邮件。很显然,JSF内置的输入校验器不能解决这种复杂的验证请求,此时我们必须借助于自定义校验器来解决该问题。
为了验证某个表单域是否为合法的电子邮件,我们开发如下的自定义校验器类。
程序清单:codes\03\3.6\EmailValidator\WEB-INF\src\org\crazyit\validator\EmailValidator.java
public class EmailValidator implements Validator { public void validate(FacesContext context, UIComponent component, Object toValidate) { if (context == null || component == null) { throw new NullPointerException(); } //设置只对输入组件起作用 if (!(component instanceof UIInput)) { return; } //要求被验证的值必须存在 if (null == toValidate) { return; } //如果被校验的值不匹配正则表达式 if (!toValidate.toString().matches( "\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*")) { throw new ValidatorException( new FacesMessage("Email校验失败")); } } }
上面输入校验器的实现类也很简单,它实现了JSF提供的Validator接口,并重写了该接口中定义的方法:在该方法内检查用户输入的E-mail字符串是否匹配指定的正则表达式,如果不能匹配则抛出一个ValidatorException异常。
提供了上面自定义校验器类之后,接下来需要在应用中注册自定义校验器。
3.6.2 注册校验器
在faces-config.xml文件中使用<validator…/>元素即可注册自定义校验器,该元素是faces-config.xml文件中根元素的子元素,该元素的内部结构如图3.29所示。
图3.29 <validator…/>元素的内部结构
从图3.29可以看出,<validator…/>元素内必须指定如下两个子元素:
<validator-id…/>:该元素为自定义校验器指定名称。
<validator-class…/>:该属性指定自定义校验器的实现类。
除此之外,还可以指定可选的<attribute…/>和<property…/>子元素,它们用于为自定义校验器配置属性。
提示
关于英文中attribute和property的区别非常明显(尤其是对程序员而言),但对中文翻译者来说,attribute和property的翻译却让人很头疼,至少笔者授课时都感到费力。通常来说,例如有如下标签<crazyit name="123"…/>,此时可称<crazyit…/>元素具有name属性(attribute);如果一个Java Bean类中定义了setName(String name)和String getName()两个方法,则称该Bean类包含了name属性(property)。希望读者能通过这个例子来体会attribute和property的区别。
例如,为了配置上面的输入校验器,我们可以在faces-config.xml文件中增加如下配置片段。
程序清单:codes\03\3.6\EmailValidator\WEB-INF\faces-config.xml
<validator> <!-- 指定输入校验器的名称 --> <validator-id>emailValidator</validator-id> <!-- 指定输入校验器的实现类 --> <validator-class>org.crazyit.validator.EmailValidator</validator-class> </validator>
由于该输入校验器既没有attribute,也没有property,因此这里配置该自定义校验器时只需指定<validator-id>和<validator-class>两个子元素即可。上面配置片段指定了该自定义校验器的名称是emailValidator,接下来就可根据该名称来使用这个自定义校验器了。
3.6.3 使用自定义校验器
前面已经提到过,引用输入校验一共有3种方式,其中通过validator属性的方式只能引用托管Bean的校验方法来执行校验;也就是说,自定义校验器要么通过<f:validator…/>标签来引用,要么通过专用标签来引用。
不过我们刚刚开发的EmailValidator校验器还没有提供专用标签,因此暂时只能通过<f:validator…/>标签来引用它。
使用<f:validator…/>标签时通过validatorId属性引用已注册的自定义校验器即可,如下页面代码使用了自定义校验器来进行输入校验。
程序清单:codes\03\3.6\EmailValidator\add.jsp
<h:form>
用户名:<h:inputText value="#{userBean.name}"/><br/>
电子邮件:<h:inputText value="#{userBean.email}" id="email">
<!-- 执行输入校验 -->
<f:validator validatorId="emailValidator"/>
</h:inputText>
<h:message for="email" style="color:red;font-weight:bold"/><br/>
<h:commandButton value="添加" action="#{userBean.add}"/>
</h:form>
上面页面代码中粗体字代码就指定了使用emailValidator校验器对userBean.email进行校验,如果用户输入的字符串不是有效的电子邮件,将会看到如图3.30所示的页面。
图3.30 输入校验失败的错误提示
如果想通过专用标签来使用自定义输入校验器,那就需要为自定义校验器开发对应的专用标签。接下来我们继续介绍相关内容。
3.6.4 为自定义校验器开发专用标签
如果使用JSF提供的<f:validator…/>标签来使用自定义标签,则无法为该标签指定自定义属性;为了在自定义标签中使用自定义属性,则可以考虑为自定义校验器提供专用标签。假设现在希望对前面提供的输入校验器进行改进,将它改进为一个正则表达式校验器——这样就具有了更广泛的适用性。
上面自定义校验器中的正则表达式是以硬编码方式写死在程序中的,如果为该输入校验器指定一个pattern参数,允许页面开发者动态地指定正则表达式,那么该输入校验器不仅可以校验E-mail,也可以校验绝大部分字符串。
由于此处开发自定义校验器需要接受页面临时指定的属性,为了让该自定义校验器可以在客户端保存状态,那就需要让该校验器类实现StateHolder接口,实现该接口还需要实现该接口内定义的如下两个方法:
restoreState(FacesContext context, java.lang.Object state):为组件恢复状态的方法。
Object saveState(FacesContext context):保存组件状态的方法。
通过实现上面两个方法,校验器可以告诉JSF实现需要保存和恢复校验器的哪些属性。
除此之外,自定义校验器还应该实现ValueHolder接口定义的isTransient()和setTransient(boolean)方法。如果isTransient()方法返回false,则意味着校验器将会保存和恢复它的状态信息。
下面我们先开发如下自定义校验器类。
程序清单:codes\03\3.6\RegexValidator\WEB-INF\src\org\crazyit\validator\RegexValidator.java
public class RegexValidator implements Validator, StateHolder { public final static String CRAZYIT_REGEX_INVALID = "crazyit_regex_invalid"; private boolean transientValue = false; //定义用于指定正则表达式的属性 private String pattern; //pattern属性的setter和getter方法 public void setPattern(String pattern) { this.pattern = pattern; } public String getPattern() { return this.pattern; } public void validate(FacesContext context, UIComponent component, Object toValidate) { if (context == null || component == null) { throw new NullPointerException(); } //设置只对输入组件起作用 if (!(component instanceof UIInput)) { return; } //要求被验证的值必须存在 if (null == toValidate) { return; } //如果被校验的值不匹配正则表达式 if (!toValidate.toString().matches(pattern)) { //使用国际化消息资源包中key为crazyit_regex的消息作为提示 throw new ValidatorException( MessageFactory.getMessage(CRAZYIT_REGEX_INVALID , component.getId() , pattern)); } } //实现该方法用于保存该校验器的状态 public Object saveState(FacesContext context) { Object[] values = new Object[1]; values[0] = pattern; return values; } //实现该方法用于恢复该校验器的状态 public void restoreState(FacesContext context ,Object state) { Object[] values = (Object[])state; pattern = (String) values[0]; } public boolean isTransient() { return (this.transientValue); } public void setTransient(boolean transientValue) { this.transientValue = transientValue; } }
从上面第一段粗体字代码可以看出,该输入校验器中包括一个名为pattern的属性,因此配置该输入校验器时应该使用<attribute…/>元素来配置该属性,在faces-config.xml文件中增加如下配置片段来配置该输入校验器。
程序清单:codes\03\3.6\RegexValidator\WEB-INF\faces-config.xml
<validator>
<!-- 指定输入校验器的名称 -->
<validator-id>regexValidator</validator-id>
<!-- 指定输入校验器的实现类 -->
<validator-class>org.crazyit.validator.RegexValidator</validator-class>
<!-- 为该输入校验器定义一个属性 -->
<attribute>
<attribute-name>pattern</attribute-name>
<attribute-class>java.lang.String</attribute-class>
</attribute>
</validator>
上面校验器代码中第二段粗体字代码使用了正则表达式来匹配用户输入的字符串,如果不能匹配则抛出一个ValidatorException异常,该异常中的错误消息来自国际化消息资源包中key为crazyit_regex_invalid的消息。
为此我们提供baseName为crazyit_mess的国际化消息资源包(也就是如果应用需要支持简体中文、美式英语两种环境,则需要提供crazyit_mess_zh_CN.properties和crazyit_mess_en_US.properties两份文件)。接下来在faces-config.xml文件中增加如下配置片段。
程序清单:codes\03\3.6\RegexValidator\WEB-INF\faces-config.xml
<application> <!-- 指定自定义国际化消息资源 --> <message-bundle>crazyit_mess</message-bundle> <locale-config> <!-- 指定该应用默认使用的Locale --> <default-locale>zh_CN</default-locale> <!-- 下面列出该应用所支持的全部Locale --> <supported-locale>en_US</supported-locale> </locale-config> </application>
以crazyit_mess_zh_CN.properties文件为例,未用native2ascii命令处理过的原始文件内容如下:
crazyit_regex_invalid=校验失败:{0}的值是无效的。该组件的值必须匹配{1}!
接下来再为上面的校验器开发一个自定义标签,JSF为校验器自定义标签提供了ValidatorELTag基类,该基类是TagSupport的子类,因此ValidatorELTag的子类即可作为自定义标签的处理类。
注意
如果读者对如何开发JSP自定义标签库还不太熟悉,建议先阅读疯狂Java体系的《轻量级Java EE企业应用实战》第2章,那里将会有关于开发JSP自定义标签库的详细介绍。
由于ValidatorELTag已经扩展了TagSupport类,因此通过集成ValidatorELTag来开发自定义标签简单多了,开发者只需重写ValidatorELTag类中的createValidator()方法即可。重写该方法返回程序实际所用的校验器——JSF页面中使用该标签的地方就会使用该方法返回的校验器来执行输入校验。
下面是本应用为RegexValidator输入校验器提供的校验器标签处理类。
程序清单:codes\03\3.6\RegexValidator\WEB-INF\src\org\crazyit\taglib\RegexValidatorTag.java
public class RegexValidatorTag
extends ValidatorELTag
{
private static String validatorID = null;
//为自定义标签定义一个属性
private String pattern = null;
public RegexValidatorTag()
{
super();
if (validatorID == null)
{
validatorID = "regexValidator";
}
}
public void setValidatorID(String validatorID)
{
this.validatorID = validatorID;
}
//pattern属性的setter和getter方法
public void setPattern(String pattern)
{
//将pattern字符串中斜线(/)替换成反斜线(\)
this.pattern = pattern.replace("/" , "\\");
}
public String getPattern()
{
return this.pattern;
}
//ValidatorELTag子类必须重写的方法
//该方法用于创建实际执行校验的校验器
protected Validator createValidator()
throws JspException
{
FacesContext facesContext =
FacesContext.getCurrentInstance();
RegexValidator result = null;
//根据已注册的输入校验器ID来创建输入校验器
if (validatorID != null)
{
result = (RegexValidator)facesContext
.getApplication()
.createValidator(validatorID);
}
result.setPattern(pattern);
return result;
}
}
上面自定义标签的实现代码比较简单,该标签也需要指定一个pattern属性,因此为该属性提供了setter和getter方法;除此之外,该标签处理类重写了createValidator方法,该方法根据已经注册的输入校验器来创建RegexValidator校验器实例,并设置了RegexValidator的pattern属性。
需要注意的是,上面程序中粗体字代码会将pattern字符串中斜线(/)替换成反斜线(\),这是因为在默认情况下,JSP自定义标签的属性值不允许使用反斜线(\),而反斜线(\)又是正则表达式中最重要的字符——因此我们做了一下变化:在为自定义标签的属性指定正则表达式属性值时,使用了斜线(/)代替反斜线(\),因此程序中需要用斜线(/)替换回来。
开发自定义标签库除了需要提供自定义标签处理类之外,还需要使用TLD文件来定义自定义标签。下面是本应用中的自定义标签库定义文件。
程序清单:codes\03\3.6\RegexValidator\WEB-INF\crazyit.tld
<?xml version="1.0" encoding="GBK"?> <taglib xmlns="http://java.sun.com/xml/ns/javaee" version="2.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee web-jsptaglibrary_2_1.xsd"> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>crazyitTaglib</short-name> <!-- 指定标签库的URI --> <uri>http://www.crazyit.org/taglib</uri> <tag> <!-- 指定该标签的名称 --> <name>regexValidator</name> <!-- 指定该标签的实现类 --> <tag-class>org.crazyit.taglib.RegexValidatorTag</tag-class> <body-content>JSP</body-content> <!-- 配置该标签的pattern属性 --> <attribute> <name>pattern</name> <required>true</required> </attribute> </tag> </taglib>
上面配置片段指定了该标签库的URI和校验器标签的名称,这样即可在页面中使用该校验器标签来执行输入校验了。页面代码如下所示。
程序清单:codes\03\3.6\RegexValidator\add.jsp
<%@taglib prefix="crazyit" uri="http://www.crazyit.org/taglib"%> … <h1>添加用户</h1> <h:form> 用户名:<h:inputText value="#{userBean.name}"/><br/> 电子邮件:<h:inputText value="#{userBean.email}" id="email"> <!-- 使用专门的标签来引用指定输入校验器 --> <crazyit:regexValidator pattern="/w+([-+.]/w+)*@/w+([-.]/w+)*/./w+([-.]/w+)*"/> </h:inputText> <h:message for="email" style="color:red;font-weight:bold"/><br/> <h:commandButton value="添加" action="#{userBean.add}"/> </h:form>
上面页面代码中两行粗体字代码就是使用自定义标签来执行输入校验的关键代码,如果用户输入的电子邮件不符合规则,则可以看到如图3.31所示的页面。
图3.31 正则表达式校验器的效果
从图3.31中看到,该正则表达式校验器功能非常强大。实际上,开发完全可以将这个校验器和校验器标签复制到实际项目中使用,每次要对任何字符串进行输入校验时,开发者只要提供相应的正则表达式即可——如果开发者不喜欢这个默认的错误提示消息,完全可通过自定义错误消息来覆盖它,或者通过validatorMessage来临时指定错误消息。
提示
实际上,笔者此处示范开发的这个正则表达式校验器和Struts 1中的mask校验器、Struts 2中的regex校验器的功能相似,它们都是功能强大、非常实用的校验器。
3.6.5 使用托管Bean的方法执行校验
对于JSF内置校验器不支持的校验需求,除了可以通过自定义输入校验器来进行校验之外,还可以直接使用托管Bean的校验方法来执行校验。
托管Bean中定义的校验方法必须满足如下方法签名:
public void xxx(FacesContext fc , UIComponent toValidate , Object value)
上面方法的方法签名形式与Validator接口中定义的validate方法的形参完全一样,只是该方法的方法名可以任意改变。
假如我们需要在托管Bean中定义一个校验电子邮件的方法,则可以在托管Bean中增加如下方法:
程序清单:codes\03\3.6\BeanValidator\WEB-INF\src\org\crazyit\jsf\UserBean.java
//定义一个输入校验的方法 public void validateEmail(FacesContext context, UIComponent component, Object toValidate) { if (context == null || component == null) { throw new NullPointerException(); } //设置只对输入组件起作用 if (!(component instanceof UIInput)) { return; } //要求被验证的值必须存在 if (null == toValidate) { return; } //如果被校验的值不匹配正则表达式 if (!toValidate.toString().matches( "\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*")) { throw new ValidatorException( new FacesMessage("Email校验失败")); } }
细心的读者不难发现,这个validateEmail方法与前面EmailValidator校验器中的validate方法的方法体完全一样,只是方法名不同而已。实际上,使用托管Bean中的校验方法与自定义校验器中的校验方法完全相同,只有方法名不同、放置位置不同而已。
在托管Bean中定义了如上所示的校验方法之后,接下来就可以在JSF页面的UI组件中通过validator属性指定该校验方法来执行输入校验了。例如,如下页面代码所示。
程序清单:codes\03\3.6\BeanValidator\add.jsp
<h1>添加用户</h1>
<h:form>
用户名:<h:inputText value="#{userBean.name}"/><br/>
电子邮件:<h:inputText value="#{userBean.email}" id="email"
validator="#{userBean.validateEmail}"/>
<h:message for="email" style="color:red;font-weight:bold"/><br/>
<h:commandButton value="添加" action="#{userBean.add}"/>
</h:form>
上面页面代码中粗体字代码指定使用userBean托管Bean的validateEmail方法来校验该输入组件,如果用户输入的电子邮件不符合要求格式,用户将可以看到如图3.30所示的页面。
经过上面介绍不难发现,自定义校验和托管Bean中校验方法的本质是相同的。那么到底选择自定义输入校验器进行自定义校验,还是选择使用托管Bean的方法进行自定义校验呢?两种方法的优缺点如表3.3所示。
表3.3 两种自定义校验机制对比
3.6.6 绑定到Bean属性的校验器
前面介绍过使用<f:validator…/>标签时可通过binding属性将校验器本身绑定到托管Bean的属性,通过把校验器本身绑定的托管Bean属性,即可让托管Bean获得对校验器的全部控制。
为了将校验器绑定到托管Bean的属性,该属性的类型必须是Validator。例如,下面托管Bean中的validator属性就可绑定到托管Bean的属性。
程序清单:codes\03\3.6\bindingValidator\WEB-INF\src\org\crazyit\jsf\UserBean.java
public class UserBean { private String name; private String email; //定义绑定到托管Bean属性的校验器 private Validator validator; //无参数的构造器 public UserBean() { } //初始化全部属性的构造器 public UserBean(String name , String email) { this.name = name; this.email = email; } //省略name属性的setter和getter方法 … //省略email属性的setter和getter方法 … //validaor属性的setter和getter方法 public void setValidator(Validator validaor) { this.validator = validator; } public Validator getValidator() { //以匿名内部类的形式创建一个Validator对象 return new Validator() { //定义一个输入校验的方法 public void validate(FacesContext context, UIComponent component, Object toValidate) { if (context == null || component == null) { throw new NullPointerException(); } //设置只对输入组件起作用 if (!(component instanceof UIInput)) { return; } //要求被验证的值必须存在 if (null == toValidate) { return; } //如果被校验的值不匹配正则表达式 if (!toValidate.toString().matches( "\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*")) { throw new ValidatorException( new FacesMessage("Email校验失败")); } } }; } //编写处理导航的方法 public String add() { return "success"; } }
上面托管Bean代码中粗体字代码以匿名内部类的形式返回一个Validator对象。由于该Validator本身由该托管Bean创建,因此该托管Bean获得了对该校验器的全部控制,从而允许更灵活地操作校验器本身。
接下来我们可使用binding属性将validator属性绑定到页面上一个校验器,如下页面代码所示。
程序清单:codes\03\3.6\bindingValidator\add.jsp
<h1>添加用户</h1>
<h:form>
用户名:<h:inputText value="#{userBean.name}"/><br/>
电子邮件:<h:inputText value="#{userBean.email}" id="email">
<!-- 将校验器绑定到托管Bean的属性 -->
<f:validator binding="#{userBean.validator}"/>
</h:inputText>
<h:message for="email" style="color:red;font-weight:bold"/><br/>
<h:commandButton value="添加" action="#{userBean.add}"/>
</h:form>
通常来说,将校验器本身绑定到托管Bean属性的情形并不常见,只在一些个别场景下才可能需要这么做。
到目前为止,一共介绍了将托管Bean的属性可以绑定到如下组件本身:
托管Bean的属性可绑定UI组件本身。
托管Bean的属性可绑定转换器本身。
托管Bean的属性可绑定校验器本身。
上面3种绑定方式都是通过组件标签的binding属性来执行绑定的,执行这种绑定之后可让托管Bean获得对UI组件、转换器、校验器的全部控制,从而可以更灵活地操作这些组件。