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. 編寫驗證主程序
編寫驗證主程序主要有下面五步:
- 創建和處理ValidatorResources對象,這要藉助於ValidatorResourcesInitializer類利用驗證規則定義文件初始化這個對象。
- 創建要驗證的bean對象
- 用驗證規則定義文件中定義的某個form創建validator對象,並且告訴這個對象要驗證的bean對象。
- 運行validator對象的validate()方法實際驗證bean對象
- 打印驗證結果
下面是依據上面所述步驟編寫的實例代碼,代碼中進行了三次驗證,第一次是驗證兩個屬性都是空的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());
}
|