内容


通用验证系统

Comments

1. 前言

本文较详尽地介绍了 jakarta 开源项目的子项目之一 commons-validator(通用验证系统),版本是 1.0.2。它使用了一个 xml 文件来定义针对用户输入的数据验证功能,整个验证体系提供了很强的扩展性,使得开发者可以开发自己的验证函数加入到这个验证体系中来。它对 web 应用程序提供了客户端 javascript 验证和服务端验证的两种选择,但是它只是一个验证体系,有些东西还需要自己开发特别是 validatoraction 的开发,不过有了项目源代码及其例子,还有 struts 这个优秀的开源项目的示范,使用好 commons-validator 验证体系应该是挺容易的。本文就这个验证体系作了些探讨,希望对大家有用!

2. 用户问题

我们在开发信息系统时,用户界面往往是一个很容易忽视的但是确是相当重要的地方。我们有好多关于编写后端代码的设计模式,现在我们还拥有 commons-validator 这样的优秀验证体系对付用户界面的用户千变万化的输入可能。输入验证关乎到整个信息系统的强壮性,因为恶意的输入数据可能导致信息系统崩溃;输入验证还关乎到信息系统的友好性,因为不能给用户提供正确的输入导引经常搞得使用者手足无措,最后只有悲愤而去。

3. 简单分析

通过对上面用户问题的描述,我们可以简单分析一下验证体系的基本特性:

  • 验证体系应该具有良好的可扩展性,可以让信息系统开发者开发自己的验证功能,以满足特殊系统的验证要求。
  • 验证体系应该能显示准确的验证错误信息,用以帮助使用者纠正错误,而且错误信息应该是外在可配置的,改变相应的错误信息不需要修改源代码。
  • 对于 web 信息系统来说,应该能支持客户端验证和服务端验证两种方式。

4. 使用界面

4.1. 配置文件

下面是验证规则 xml 文件的元素关系图,我将挑选一些重要而又相对复杂的元素进行讲解。

图 1. 验证规则 xml 文件的元素关系图

1. 元素 constant

"constant" 元素定义了"field"元素所使用的替换型参数的静态值。 "constant-name" 和 "constant-value" 元素分别表示这个静态值的引用标识和值

2. 元素 validator

这个"validator"元素定义了 formset 元素字段所能使用的 validatoraction 对象。

子元素javascript
属性
属性名可选性注释与缺省值
namerequired验证对象的标识
classnamerequired验证对象的完全类名
methodrequired用来实现这个验证的方法名
methodParamsrequired验证方法的逗号隔开的参数类型列表
msgrequired验证失败时使用的消息键
depends.逗号隔开的这个验证所依赖的其他验证列表
jsFunctionname

3. 元素 formset

"formset" 定义了一个针对 locale 的 form 集 . "form"元素定义了有待验证的"field" 集,名字属性是应用程序分配给这个"form"的引用标识。

子元素constant form
属性
属性名可选性注释与缺省值
language.locale 对象的语言部分
country.locale 对象的国家部分
variant.locale 对象的语言变种部分

4. 元素 field

"field" 元素定义了需要验证的属性,在 web 应用中,一个"field"对应于一个 HTML 表单控件。验证系统通过验证一个 JavaBean 来验证这个"field" 元素,这个元素可以接受 4 个属性:

子元素msg arg0 arg1 arg2 arg3 var
属性
属性名可选性注释与缺省值
propertyrequired这个"field" 元素对应的 JavaBean 属性。
depends.逗号隔开的 validatoraction 对象列表,所有的 validatoraction 对象验证通过,这个"field"才验证有效。
page.JavaBean 可能有一个 page 属性,只有"page"属性小于或等于 JavaBean page 属性的"field" 元素才会被处理。这个机制对"向导"性的应用非常有用。
缺省值 [0]
indexedListProperty."indexedListProperty"是一个返回数组或集合的方法。

5. 元素 msg

"msg" 元素定义了一个定制消息键,用来为验证失败的"field"提供消息文本。 当"field"没有子元素"msg" 元素时,每个 validatoraction 对象则使用自己的消息属性。

属性名可选性注释与缺省值
name.对应于这个消息的 validatoraction 对象。
key.消息资源文件中的消息键。
resource.如果这个值为 "false","key"属性将是直接的消息文本。缺省值 [true]

6. 元素 arg0|arg1|arg2|arg3

这是 4 个参数元素,定义了 validator 或 field 消息模版中的 4 个替换值。比如 validator 的 msg 对应的消息资源是"必须提供 {0} 字段,而且字段的长度不能小于 {1} 字符! ",在显示错误的时候,其中 {0} 将被 arg0 的消息文本替换,而 {1} 将被 arg1 的消息文本替换。

属性名可选性注释与缺省值
name.对应于这个消息的 validatoraction 对象。
key.消息资源文件中的消息键。
resource.如果这个值为 "false","key"属性将是直接的消息文本。缺省值 [true]

7. 元素 var

"field"能通过这个元素向某个 validatoraction 对象传递参数,这些参数也能被 arg? 元素通过语法 ${var:var-name} 引用。它的子元素 var-name 和 var-value 分别为变量标识和变量的值。

4.2. 应用编程接口

如图《 Commons-validator 的 API 》所示,commons-validator 的类明显的分成三种,第一种为代表验证规则文件中各个元素的类,本文称元素类,第二种是程序准备验证资料和验证的类,本文称 fa?ade 类,第三种是实现了通用功能的类,本文称工具类。元素类代表了验证规则文件中的各个元素,对于编程者来说主要作用是用他们来得到消息文本;fa?ade 类用来使 Commons-validator 验证系统融入到应用系统中;而工具类有助于编程者写实现各种 validatorAction 的类。具体的使用参见下面的代码样例。

4.3. 代码样例

虽然 common-validation 是为 web 应用写的验证体系,它同时也能用在 java 应用程序中,为了把注意力放在验证系统的介绍上,下面的验证样例使用 java 应用程序来表演。

4.3.1. 定义验证规则

验证规则是一个 xml 文件,定义了需要验证的表单,及其表单的各个字段以及字段的验证要求,另外 validator 元素是用来完成各个字段的验证要求的。本例定义了一个输入表单 nameForm 及其两个字段,两个字段都必须提供,而且 age 字段还必须是整数;还定义了两个验证动作 int 和 required,分别满足整数要求和必须提供的要求:

 <form-validation> 
     <global>      
         <validator name="int" 
             classname="org.i505.validator.MyTypeValidator" 
             method="validateInt"    
             methodParams="java.lang.Object,org.apache.commons.validator.Field"
             msg="errors.int"/>    
         <validator name="required"        
             classname="org.i505.validator.MyValidator"  
             method="validateRequired"           
             methodParams="java.lang.Object,org.apache.commons.validator.Field"
             msg="errors.required"/> 
     </global> 
 <formset> 
 <form name="nameForm"> 
     <field property="username"  depends="required">    
         <arg0 key="nameForm.username.displayname"/>    
     </field>    
     <field  property="age" depends="required,int">       
         <arg0 key="nameForm.age.displayname"/>      
     </field>  
 </form> 
 </formset>  
 </form-validation>

4.3.2. 编写消息资源文件

commons-validator 的消息资源包括两大部份,第一部分是包括了参数占位符的 validatoraction 对象的消息,第二部分是各个输入表单输入数据的显示信息,用作验证失败时的信息显示。本例中值包括了一个输入表单的显示信息:

 # validatoraction 对象的消息
 errors.required= 必须提供 {0} 字段!
 errors.int= {0} 字段必须是整数!
 # nameForm 输入表单的各个输入数据的显示信息
 nameForm.username.displayname= 姓名
 nameForm.age.displayname= 年龄

4.3.3. 编写 validatorAction

我们从验证定义规则文件中可以看出 validator 元素定义的 int 和 required validatorAction 分别使用了 org.i505.validator.MyTypeValidator 和 org.i505.validator.MyValidator 两个类,这个元素还定义了它们使用的验证方法 validateInt 和 validateRequired 以及方法的参数类型列表。下面是这两个类的代码:

 package org.i505.validator;import 
 org.apache.commons.validator.Field; 
 import org.apache.commons.validator.GenericTypeValidator; 
 import org.apache.commons.validator.ValidatorUtil; 
 public class MyTypeValidator { 
	 public static Integer validateInt(Object bean, Field field) {   
		 String value = ValidatorUtil.getValueAsString(bean, field.getProperty()); 
		 Integer x= GenericTypeValidator.formatInt(value);   
		 return x;   
	 } 
 }
 package org.i505.validator; 
 import org.apache.commons.validator.Field; 
 import org.apache.commons.validator.GenericValidator; 
 import org.apache.commons.validator.ValidatorUtil;  
 public class MyValidator {            
	 public static boolean validateRequired(Object bean, Field field) {  
		 String value = ValidatorUtil.getValueAsString(bean, field.getProperty()); 
		 return !GenericValidator.isBlankOrNull(value);  
	 } 
 }

4.3.4. 编写 javabean

commons-validator 是一个针对 web 应用的输入验证体系,验证规则中的 form 定义是针对 html form 表单的,但是 common-validator 在内部验证时需要 javabean。这个 javabean 的各个属性就代表了 html form 表单的输入控制。所以针对前面的验证规则,我们实现的 javabean 需要定义两个属性:age 和 username,代码如下 :

 public class ValidateBean extends Object { 
	 String username;String age;  
	 public void setUsername (String username) { 
		 this. username = username;   
	 }   
	 public String getUsername () { 
		 return this.username;  
	 }   
	 public void setAge (String age) { 
		 this.age = age; 
	 } 
	 public String getAge () { 	
		 return this.age;  
	 } 
	 public String toString() { 
		 return "{ username =" + this.username + ", age=" + this.age + "}";   
	 } 
 }

注意,这个验证 BEAN 的 age 属性的类型是字符串型的,因为它只是代表了 html form 表单的输入控制的值,原始的用户输入数据基本上都可以用 String 来表示,如果我们申明 age 属性的类型时整数型,则我们在 html form 表单的值到 BEAN 的 age 属性就经过了一次类型转换,这个早于我们的整型验证,所以可能有产生类型转换错误的危险。

4.3.5. 编写验证主程序

编写验证主程序主要有下面五步:

  1. 创建和处理 ValidatorResources 对象,这要借助于 ValidatorResourcesInitializer 类利用验证规则定义文件初始化这个对象。
  2. 创建要验证的 bean 对象
  3. 用验证规则定义文件中定义的某个 form 创建 validator 对象,并且告诉这个对象要验证的 bean 对象。
  4. 运行 validator 对象的 validate() 方法实际验证 bean 对象
  5. 打印验证结果

下面是依据上面所述步骤编写的实例代码,代码中进行了三次验证,第一次是验证两个属性都是空的 bean 对象,第二次是 age 属性不合法的 bean 对象,第三次是两个属性都合法的 bean 对象:

 public static void main(String[] args) throws IOException, ValidatorException {   
     InputStream in = null;  
     try {        
         ValidatorResources resources = new ValidatorResources();       
         in = ValidateExample.class.getResourceAsStream("myvalidator-example.xml");
         ValidatorResourcesInitializer.initialize(resources, in);      
         ValidateBean bean = new ValidateBean();           
         Validator validator = new Validator(resources, "nameForm");   
         validator.addResource(Validator.BEAN_KEY, bean);       
         ValidatorResults results = null;          
         results = validator.validate();       
         printResults(bean, results, resources);   
         bean.setUsername("龚永生");           
         bean.setAge("很年轻");          
         results = validator.validate();  
         printResults(bean, results, resources);   
         bean.setAge("28");    
         results = validator.validate();   
         printResults(bean, results, resources);    
     } 
     finally {     
         if (in != null) { 
             in.close();     
         } 
     } 
 }

4.3.6. 打印验证结果

打印验证结果可能是验证体系中最复杂的一部分,因为它涉及到验证文件和消息资源文件,涉及到好多对象以及它们复杂的关系。特别需要指出的是错误消息文本的显示。下面的代码包括三个部分:第一部分是使用资源文件生成 ResourceBundle 对象,注意你的资源文件必须在 classloader 能找到的地方;第二部分是实际打印验证结果;第三部分是个显示中文消息的函数。

validator 对象的 validate() 方法会把验证结果保存到其返回的 ValidatorResults 对象中,它保存了 bean 对象被验证的每个属性的各种验证要求的验证结果对象 ValidatorResult,首先我们可以获取 bean 对象对应的验证文件定义的 form,从而得到相应的消息键和其它信息,而且通过这些信息从 ValidatorResults 对象中获取相应的 ValidatorResult 对象,利用 ValidatorResult 对象 isValid 函数可以判断验证的成功与否,如果验证没通过,可以使用 form 的信息显示错误消息文本。

private static ResourceBundle apps =    
     ResourceBundle.getBundle(      
         "org.i505.validator.myapplicationResources");  
         public static void printResults(     
             ValidateBean bean,     
             ValidatorResults results,    
             ValidatorResources resources) {   
                 boolean success = true;  
                 Form form = resources.get(Locale.getDefault(), "nameForm");  
                 System.out.println("\n\n 验证 :");   
                 System.out.println(bean);  
                 Iterator propertyNames = results.get();    
                 while (propertyNames.hasNext()) {    
                     String propertyName = (String) propertyNames.next();      
                     Field field = (Field) form.getFieldMap().get(propertyName);  
                     String prettyFieldName = getGBKMsg(apps.getString(
                         field.getArg0().getKey()));
                     ValidatorResult result = results.getValidatorResult(propertyName);
                     Map actionMap = result.getActionMap();     
                     Iterator keys = actionMap.keySet().iterator();   
                     while (keys.hasNext()) {      
                         String actName = (String) keys.next();      
                         ValidatorAction action=resources.getValidatorAction(actName);
                         System.out.println(         
                         propertyName                 
                         + "["                   
                         + actName               
                         + "] ("               
                         + (result.isValid(actName) ? "验证通过" : "验证失败")     
                         + ")");       
                     if (!result.isValid(actName)) {    
                         success = false;               
                         String message = getGBKMsg(apps.getString(action.getMsg()));
                         Object[] args = { prettyFieldName };             
                         System.out.println(                
                            "错误信息是 : "                   
                             + MessageFormat.format(message, args));     
                             }     
                         }       
                     }    
                     if (success) {   
                         System.out.println("表单验证通过"); 
                     } 
                     else { 
                         System.out.println("表单验证失败"); 
                     } 
                 }   
                 public static String getGBKMsg(String msg){   
                     String gbkStr="";    
                     try {         
                         gbkStr=new String(msg.getBytes("iso-8859-1"),"gbk");
                     } 
                     catch (UnsupportedEncodingException e) {
                         // TODO Auto-generated catch block
                         e.printStackTrace();     
                     }     
                     return gbkStr; 
                 }

验证结果如下:

验证 :{ username =null, age=null} 
 age[required] ( 验证失败 ) 错误信息是 : 必须提供年龄字段!
 username[required] ( 验证失败 ) 错误信息是 : 必须提供姓名字段!
表单验证失败
验证 :{ username = 龚永生 , age= 很年轻 } 
 age[required] ( 验证通过 ) 
 age[int] ( 验证失败 ) 
错误信息是 : 年龄字段必须是整数!
 username[required] ( 验证通过 ) 表单验证失败
验证 :{ username = 龚永生 , age=28} 
 age[required] ( 验证通过 ) 
 age[int] ( 验证通过 ) 
 username[required] ( 验证通过 ) 
表单验证通过

5. 内部剖析

5.1. 类之间的联系

ValidatorResults 对象有个 map,以 field 的 getKey() 为键,这个 field 的验证结果 ValidatorResult 对象为值。

ValidatorResult 对象也有个 map, 以 field 的各个 validator 元素的名字为键(在 field 元素的 depends 中定一个 field 的 validator 元素列表),以一个表示验证成功与否的对象为值。

ValidatorResources 对象包含一个 map, 以 Locale 的某种字符串表示为键,FormSet 为值(所以 formset 有多种版本),还包含一个 map,保存了全局常量,以常量名为键,常量值为值;还包含一个 map,以 validator 元素的 name 属性为键 , validatorAction 对象为值。

Formset 对象包含一个 map, 以 form 的 name 属性为键,Form 对象为值;还包含一个 map,以 formset 元素的子元素 Constant 的 name 为键,子元素 Constant 的值为值。

Form 对象包含一个 map, 以 Field 元素对应的 Field 对象的 getKey() 为键,Field 对象为值;另外还拥有一个保存顺序的 field 对象数组。

field 对象拥有一个 map,以 var 的名字为键,var 对象为值。

Validator 对象包含一个 map,以各个 validator 元素的 methodParams 参数列表中的名字为键,相应的对象为值,这个 map 的键和值将会用作调用相应 validator 元素中的 methods 属性指定方法的参数。

通过这些 map,commons-validator 在验证系统各个类间铺了一张类关系表,见下图:

5.2. 如何调用 validatorAction

验证规则的 validator 元素定义了 validatorAction,而 field 元素则通过 depends 属性引用了这些 validatorAction。从上面代码样例中的验证主程序可以知道 validator.validate()方法是针对某个 form 元素的,它将对这个 form 元素的各个 field 进行验证,对 field 进行验证也就是调用 field 元素的 depends 属性引用的各个 validator 元素定义的验证方法。

validator 元素使用 classname、method 和 methodParams 三个属性定义了一个验证方法,比如下面的 xml 片断就定义了一个验证整数的验证方法 validateInt,这个方法带有两个参数,类型依次是 java.lang.Object,org.apache.commons.validator.Field。验证方法 validateInt 将在 org.i505.validator.MyTypeValidator 代码中实现。

 <validator name="int"     
	 classname="org.i505.validator.MyTypeValidator"  
	 method="validateInt"        
	 methodParams="java.lang.Object,org.apache.commons.validator.Field" 
	 msg="errors.int"/>

讲了这么多,现在的问题是 validator.validate()方法是如何调用各个验证方法(比如 validateInt)的?

我们用一个顺序图和一段代码剖析这个问题。

顺序图

上图是个简要的顺序图,这个顺序图的解释图下:

1. 向 validator 对象增加资源(向资源 map 增加项)

2. 实际验证

对 form 定义的每个 field,调用如下步骤:

#begin

3. 验证一个 field

对 field 的每个 validatoraction,执行如下步骤:

#begin

4. 验证一个 validatoraction

5. 合并验证结果

#end

#end

下面代码详细解释了上面的第四步:验证一个 validatoraction。

     // Add these two Objects to the resources since they reference  
     // the current validator action and field   
     hResources.put(VALIDATOR_ACTION_KEY, va);   
     hResources.put(FIELD_KEY, field);      
     Class c = getClassLoader().loadClass(va.getClassname());   
     List lParams = va.getMethodParamsList();       
     int size = lParams.size();      
     int beanIndexPos = -1;      
     int fieldIndexPos = -1;    
     Class[] paramClass = new Class[size];          
     Object[] paramValue = new Object[size];       
     for (int x = 0; x < size; x++) {            
         String paramKey = (String) lParams.get(x); 
         if (BEAN_KEY.equals(paramKey)) {         
             beanIndexPos = x;         
         }       
         if (FIELD_KEY.equals(paramKey)) {     
             fieldIndexPos = x;                
         } 
         // There were problems calling getClass on paramValue[]
         paramClass[x] = getClassLoader().loadClass(paramKey);
         paramValue[x] = hResources.get(paramKey);       
     }       
     Method m = c.getMethod(va.getMethod(), paramClass);     
     // If the method is static we don't need an instance of the class
     // to call the method.  If it isn't, we do.        
     if (!Modifier.isStatic(m.getModifiers())) {       
         try {              
             if (va.getClassnameInstance() == null) {    
                 va.setClassnameInstance(c.newInstance());
             } 
         } 
         catch (Exception ex) {           
             log.error(                 
                "Couldn't load instance "         
                 + "of class "                 
                 + va.getClassname()          
                 + ".  "                 
                 + ex.getMessage());     
             } 
         } 
         Object result = null;           
         if (field.isIndexed()) {        
             Object oIndexed =           
                 PropertyUtils.getProperty(   
                 hResources.get(BEAN_KEY),     
                 field.getIndexedListProperty());   
                 Object indexedList[] = new Object[0]; 
                 if (oIndexed instanceof Collection) {  
                     indexedList = ((Collection) oIndexed).toArray();   
                 } 
                 else if (oIndexed.getClass().isArray()) {  
                     indexedList = (Object[]) oIndexed;      
                 }   
                 // Set current iteration object to the parameter array
                 paramValue[beanIndexPos] = indexedList[pos];   
                 // Set field clone with the key modified to represent     
                 // the current field           
                 Field indexedField = (Field) field.clone();          
                 indexedField.setKey(                
                 ValidatorUtil.replace(          
                     indexedField.getKey(),      
                     Field.TOKEN_INDEXED,        
                    "[" + pos + "]"));  
                     paramValue[fieldIndexPos] = indexedField;     
                     result = m.invoke(va.getClassnameInstance(), paramValue);
                     results.add(field, va.getName(), isValid(result), result);
                     if (!isValid(result)) {           
                         return false;              
                     } 
                 } 
                 else {         
                     result = m.invoke(va.getClassnameInstance(), paramValue);
                     results.add(field, va.getName(), isValid(result), result);
                     if (!isValid(result)) {             
                         return false;                
                     } 
                 }

这段代码首先增加了两个资源:目前正在验证的 field 和 validatoraction,接着实例化验证方法所在类的一个对象,接着按照资源 map 的键 / 值和验证方法的参数类列表构造验证方法的参数列表,最后调用验证方法所在类的一个对象的验证方法。

6. 遗留问题

我们说 commons-validator 是个通用的验证系统,它确实是个不错的东西,但是要想在实际系统中使用它还需要一定的工作,特别是想利用它的客户端验证时尤为如此。所幸的是 struts 项目为我们使用这些这个验证系统作了很经典的示范,本人认为有必要把 struts 项目的这些工作移到 commons-validator 项目中来,这样它的可用性将大大提高。

7. 总结

作为一个验证的通用框架,有些功能不是立即可用的,它需要开发者再次包装。Struts 就重新包装了 commons-validator 的客户端验证机制,使得这种机制在开发 struts 程序来说是立即可用的。有了这些包装,剩下的任务就是开发 validatoraction 来满足不同的验证要求了。另外 struts 还提供了验证和某个正则表达式匹配的输入,它使用了 commons-validator 的 perl5 正则表达式匹配机制。

在开发 web 信息系统时,除了验证输入外,我们还需要注意数据的输出。Web 的界面是 html 代码,而且这个代码是由浏览器来解释的,如果我们的内部数据包括了 html 代码的保留字,轻一点危害是破坏浏览器对 html 的解释,搞坏了我们的最后界面;重一点的是引入安全隐患,瘫痪信息系统。下面这段代码可用于过滤 html 保留字,学着 URLEncoding 的样,我把它称为 HTMLEncoding:

 public static String HTMLEncoding (String value) {   
	 if (value == null)        
   return (null);     
   char content[] = new char[value.length()];    
   value.getChars(0, value.length(), content, 0); 
   StringBuffer result = new StringBuffer(content.length + 50);     
   for (int i = 0; i < content.length; i++) {        
	 switch (content[i]) {         
	 case '<':                
		 result.append("<");             
		 break;       
	 case '>':        
		 result.append(">");  
		 break;          
	 case '&':        
		 result.append("&");   
		 break;         
	 case '"':        
		 result.append(""");       
		 break;         
	 case '\'':      
		 result.append("'");  
		 break;          
	注释与缺省值 :        
	 result.append(content[i]); 
 }     
 }     
 return (result.toString());   
 }

相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=52996
ArticleTitle=通用验证系统
publish-date=10132003