通用验证系统
 

2009-05-14 来源:ibm

 

1. 前言

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

2. 用户问题

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

3. 简单分析

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

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

4. 使用界面

4.1. 配置文件

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

1. 元素constant

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

2. 元素validator

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

子元素 javascript
属性
属性名 可选性 注释与缺省值
name required 验证对象的标识
classname required 验证对象的完全类名
method required 用来实现这个验证的方法名
methodParams required 验证方法的逗号隔开的参数类型列表
msg required 验证失败时使用的消息键
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
属性
属性名 可选性 注释与缺省值
property required 这个"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());   
}

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织