通用验证系统

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

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());   
}

  

通用验证系统


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论