通用驗證系統

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());   
}
發佈了0 篇原創文章 · 獲贊 0 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章