在Struts 2.0中實現表單數據校驗(Validation)

All Input Is Evil!
-Writing secure code

文章的開頭所引用的《Writing Secure Code》的名言:“所有的輸入都是罪惡的”,所以我們應該對所有的外部輸入進行校驗。而表單是應用程序最簡單的入口,對其傳進來的數據,我們必須進行校驗。

轉換與校驗(Conversion & Validation)

我本來是打算寫表單數據校驗的內容,但是經過再三思考後,還是決定先寫Struts 2.0轉換器的內容。原因是我認爲轉換是校驗的基礎,只有在數據被正確地轉換成其對應的類型後,我們纔可以對其取值範圍進行校驗。看個例子相信大家可以更清楚。現在我們就來改造一下《轉換器(Converter)——Struts 2.0中的魔術師》的第一個例子。

首先,從Action開始,修改後的代碼如下:

package tutorial;

import java.util.Locale;

import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.util.LocalizedTextUtil;

public class HelloWorld extends ActionSupport {
   
private String msg;
   
private Locale loc = Locale.US;
   
   
public String getMsg() {
       
return msg;        
   }

   
   
public Locale getLoc() {
       
return loc;
   }

   
   
public void setLoc(Locale loc) {
       
this .loc = loc;
   }

   
   @Override
   
public void validate() {
       System.out.println(
" Calling validate() " );
       
if ( ! (loc.equals(Locale.US) || loc.equals(Locale.CHINA))) {
                   addFieldError(
" loc " , getText( " validation.loc " ));
       }

   }

       
   
public void validateExecute() {
       System.out.println(
" Calling validateExecute() by reflection " );
   }

   
   @Override
   
public String execute() {
       System.out.println(
" Calling execute() " );
       
// LocalizedTextUtil是Struts 2.0中國際化的工具類,<s:text>標誌就是通過調用它實現國際化的
           msg = LocalizedTextUtil.findDefaultText( " HelloWorld " , loc);
       
return SUCCESS;
   }

}

然後,修改Struts.xml中Action的定義指明輸入地址:

< action name ="HelloWorld" class ="tutorial.HelloWorld" >
   
< result > /HelloWorld.jsp </ result >
   
< result name ="input" > /HelloWorld.jsp </ result >
</ action >

接着,在HelloWorld.jsp中加入錯誤提示:

<% @ page  contentType = " text/html; charset=UTF-8 " %>
<% @taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
   
< title > Hello World </ title >
</ head >
< body >
   
< div style ="color:red;" >
       
< s:fielderror />
   
</ div >
   
< s:form action ="HelloWorld" theme ="simple" >            
        Locale:
< s:textfield name ="loc" /> &nbsp; < s:submit />
   
</ s:form >    
   
< h2 >< s:property value ="msg" /></ h2 >
</ body >
</ html >

再修改LocaleConverter.java文件,將內容改爲:

package tutorial;

import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

public class LocaleConverter extends ognl.DefaultTypeConverter {
   @Override
   
public Object convertValue(Map context, Object value, Class toType) {
       
if (toType == Locale. class ) {            
           System.out.println(
" Converting String to Locale " );
           String locale
= ((String[]) value)[ 0 ];
           
return new Locale(locale.substring( 0 , 2 ), locale.substring( 3 ));
       }
else if (toType == String. class ) {
           System.out.println(
" Converting Locale to String " );
           Locale locale
= (Locale) value;
           
return locale.toString();
       }

       
return null ;
   }

}

之後,修改國際化資源文件,內容爲:

HelloWorld = 你好,世界!
invalid.fieldvalue.loc
= Locale必須爲/ " xx_XX/ " 的格式
validation.loc
= 區域必須爲中國或美國
globalMessages_zh_CN.properties

HelloWorld = Hello World!
invalid.fieldvalue.loc
= Locale must like / " xx_XX/ "
validation.loc
= Locale must be China or USA
globalMessages_en_US.properties

發佈運行應用程序,在瀏覽器中鍵入http://localhost:8080/Struts2_Validation/HelloWorld.action,在Locale中輸入zh_CN,按“Submit”提交,效果如上篇文章所示。而在服務器控制檯有如下輸出:

Converting String to Locale...
Calling validateExecute() by reflection...
Calling validate()...
Calling execute()...
Converting Locale to String...

上述的輸出說明了Struts 2.0的數據校驗工作方式,它需要經過下面幾個步驟:

  1. 通過轉換器將請求參數轉換成相應的Bean屬性;
  2. 判斷轉換過程是否出現異常。如果有,則將其保存到ActionContext中,conversionError攔截器再封裝爲fieldError;如果沒有,進行下一步;
  3. 通過反射(Reflection)來調用validateXxx()方法(其中,Xxx表示Action的方法名);
  4. 調用validate()方法;
  5. 如果經過上述步驟沒有出現fieldError,則調用Action方法;如果有,則會跳過Action方法,通過國際化將fieldError輸出到頁面。

不喜歡看文字的朋友,可以參考下面的圖1。

圖1 校驗順序圖
圖1 校驗順序圖

看到這裏可能大家會疑問:“這麼多地方可以校驗表單數據,到底我應該在那裏做呢?”有選擇是好事,但抉擇的過程往往是痛苦的,往往讓人不知所措。如果大家參照以下幾點建議,相信會比較容易地做出正確的抉擇。

  1. 如果需要轉換的數據,通常做法在轉換的時候做格式的校驗,在Action中的校驗方法中校驗取值。假如用戶填錯了格式,我們可以通過在資源文件配置invalid.fieldvalue.xxx(xxx爲屬性名)來提示用戶正確的格式,不同的階段出錯顯示不同的信息。具體做法請參考上面的例子;
  2. 至於用validate()還是validateXxx(),我推薦使用validate()。原因是validateXxx()使用了反射,相對來說性能稍差,而validate()則是通過接口com.opensymphony.xwork2.Validateable調用。當然如果你的表單數據取值是取決於特定Action方法,則應該使用validateXxx()。

在運行上面的例子時,在Locale中輸入zh並提交時出現圖2所示頁面。

圖2 錯誤格式
圖2 錯誤格式

在Locale中輸入de_DE時,出現如圖3所示頁面。

圖3 取值錯誤
圖3 取值錯誤

使用Struts 2.0的校驗框架

上一節的內容都是關於如何編程實現校驗,這部分工作大都是單調的重複。更多情況下,我們使用Struts 2.0的校驗框架,通過配置實現一些常見的校驗。

我學習編程有個習慣——喜歡先看輸出結果,再看代碼實現。這樣學的好處是先看結果可以刺激學習的激情,也可以在看代碼前自已思考一下如何實現,然後帶着問題去看代碼,那就清晰多了。因此下面我們先來做演示。

首先,在tutorial包下新建ValidationAction.java,代碼如下:

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

public class ValidationAction extends ActionSupport {
   
private String reqiuredString;

   
public String getReqiuredString() {
       
return reqiuredString;
   }


   
public void setReqiuredString(String reqiuredString) {
       
this .reqiuredString = reqiuredString;
   }

   
   @Override
   
public String execute() {
       
return SUCCESS;
   }
   
}

然後,配置上述所建的Ation,代碼片段如下:

< action name ="ValidationAction" class ="tutorial.ValidationAction" >
   
< result > /Output.jsp </ result >
   
< result name ="input" > /Input.jsp </ result >
</ action >

接着,創建Input.jsp和Output.jsp,內容分別如下:

<% @ page  contentType = " text/html; charset=UTF-8 " %>
<% @taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
   
< title > Hello World </ title >
   
<!-- 此標誌的作用是引入Struts 2.0的常用的Javascript和CSS -->
   
< s:head />
</ head >
< body >
   
< s:form action ="ValidationAction" >            
       
< s:textfield name ="reqiuredString" label ="Required String" />
       
< s:submit />
   
</ s:form >    
</ body >
</ html >
Input.jsp

<% @ page  contentType = " text/html; charset=UTF-8 " %>
<% @taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
   
< title > Hello World </ title >
</ head >
< body >
    Required String:
< s:property value ="reqiuredString" />    
</ body >
</ html >
Output.jsp

再接下來,在tutorial包下創建ValidationAction的校驗配置文件Xxx-validation.xml(Xxx爲Action的類名),在本例中該文件名ValidationAction-validation.xml,內容如下:

<? xml version="1.0" encoding="UTF-8" ?>
<! DOCTYPE validators PUBLIC 
          "-//OpenSymphony Group//XWork Validator 1.0//EN" 
          "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd"
>          
< validators >
   
< field name ="reqiuredString" >
       
< field-validator type ="requiredstring" >
           
< message > This string is required </ message >
       
</ field-validator >
   
</ field >
</ validators >

發佈運行應用程序,在地址欄中鍵入http://localhost:8080/Struts2_Validation/Input.jsp,出現如圖4所示頁面。

圖4 Input.jsp
圖4 Input.jsp

直接點擊“Submit”提交表單,出現圖5所示的頁面。

圖5 錯誤提示
圖5 錯誤提示

在Required String中隨便填點東西,轉到Output.jsp頁面,如圖6所示。

圖6 Output.jsp
圖6 Output.jsp

通過上面的例子,大家可以看到使用該校驗框架十分簡單方便。不過,上例還有兩點不足:

  1. 還沒有國際化錯誤消息;
  2. 沒有實現客戶端的校驗。

當然,要完善以上不足,對於Struts 2.0來說,只是小菜一碟。

  1. 在Xxx-validation.xml文件中的<message>元素中加入key屬性;
  2. 在Input.jsp中的<s:form>標誌中加入validate="true"屬性,就可以在用Javascript在客戶端校驗數據。

下面是具體的實現,首先在國際化資源文件中加入錯誤消息,然後按照上面說明實現。因爲要使用Javascript在客戶端顯示出錯信息,所以在加載Input.jsp頁面時,Struts 2.0需要獲得國際化的字符串,故我們需要使用Action來訪問Input.jsp頁面,具體實現請參考《在Struts 2.0中國際化(i18n)您的應用程序》的最後部分。最後發佈運行應用程序,直接在頁面中點擊“Submit”,表單沒有被提交併出現錯誤提示,如圖7所示:

圖7 客戶端校驗
圖7 客戶端校驗

校驗框架是通過validation攔截器實現,該攔載被註冊到默認的攔截器鏈中。它在conversionError攔截器之後,在validateXxx()之前被調用。這裏又出現了一個選擇的問題:到底是應該在action中通過validateXxx()或validate()實現校驗,還是使用validation攔截器?絕大多數情況,我建議大家使用校驗框架,只有當框架滿足不了您的要求才自已編寫代碼實現。

配置文件查找順序

在上面的例子中,我們通過創建ValidationAction-validation.xml來配置表單校驗。Struts 2.0的校驗框架自動會讀取該文件,但這樣就會引出一個問題——如果我的Action繼承其它的Action類,而這兩個Action類都需要對錶單數據進行校驗,那我是否會在子類的配置文件(Xxx-validation.xml)中複製父類的配置嗎?

答案是不,因爲Struts 2.0的校驗框架跟《在Struts 2.0中國際化(i18n)您的應用程序》提到的“資源文件查找順序”相似,有特定的配置文件查找順序。不同的是校驗框架按照自上而下的順序在類層次查找配置文件。假設以下條件成立:

  1. 接口 Animal;
  2. 接口 Quadraped 擴展了 Animal;
  3. 類 AnimalImpl 實現了 Animal;
  4. 類 QuadrapedImpl 擴展了 AnimalImpl 實現了 Quadraped;
  5. 類 Dog 擴展了 QuadrapedImpl;

如果Dog要被校驗,框架方法會查找下面的配置文件(其中別名是Action在struts.xml中定義的別名):

  1. Animal-validation.xml
  2. Animal-別名-validation.xml
  3. AnimalImpl-validation.xml
  4. AnimalImpl-別名-validation.xml
  5. Quadraped-validation.xml
  6. Quadraped-別名-validation.xml
  7. QuadrapedImpl-validation.xml
  8. QuadrapedImpl-別名-validation.xml
  9. Dog-validation.xml
  10. Dog-別名-validation.xml

已有的校驗器

Struts 2.0已經爲您實現很多常用的校驗了,以下在jar的default.xml中的註冊的校驗器。

< validators >
   
< validator name ="required" class ="com.opensymphony.xwork2.validator.validators.RequiredFieldValidator" />
   
< validator name ="requiredstring" class ="com.opensymphony.xwork2.validator.validators.RequiredStringValidator" />
   
< validator name ="int" class ="com.opensymphony.xwork2.validator.validators.IntRangeFieldValidator" />
   
< validator name ="double" class ="com.opensymphony.xwork2.validator.validators.DoubleRangeFieldValidator" />
   
< validator name ="date" class ="com.opensymphony.xwork2.validator.validators.DateRangeFieldValidator" />
   
< validator name ="expression" class ="com.opensymphony.xwork2.validator.validators.ExpressionValidator" />
   
< validator name ="fieldexpression" class ="com.opensymphony.xwork2.validator.validators.FieldExpressionValidator" />
   
< validator name ="email" class ="com.opensymphony.xwork2.validator.validators.EmailValidator" />
   
< validator name ="url" class ="com.opensymphony.xwork2.validator.validators.URLValidator" />
   
< validator name ="visitor" class ="com.opensymphony.xwork2.validator.validators.VisitorFieldValidator" />
   
< validator name ="conversion" class ="com.opensymphony.xwork2.validator.validators.ConversionErrorFieldValidator" />
   
< validator name ="stringlength" class ="com.opensymphony.xwork2.validator.validators.StringLengthFieldValidator" />
   
< validator name ="regex" class ="com.opensymphony.xwork2.validator.validators.RegexFieldValidator" />
</ validators >
關於每個校驗器的具體用法,請參考以下鏈接:
http://wiki.javascud.org/display/ww2cndoc/Validation
該鏈接中還有一些很有的信息,請大家仔細閱讀。

總結

使用校驗框架既可以方便地實現表單數據校驗,又能夠將校驗與Action分離,故我們應該儘可能使用校驗框架。在使用校驗框架時,請不要忘記通過在資源文件加入invalid.fieldvalue.xxx字符串,顯示適合的類型轉換出錯信息;或者使用conversion校驗器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章