對于任何一個應用而言在客戶端做的數(shù)據有效性驗證都不是安全有效的,這時候就要求我們在開發(fā)的時候在服務端也對數(shù)據的有效性進行驗證节芥。SpringMVC自身對數(shù)據在服務端的校驗有一個比較好的支持呼伸,它能將我們提交到服務端的數(shù)據按照我們事先的約定進行數(shù)據有效性驗證柒莉,對于不合格的數(shù)據信息SpringMVC會把它保存在錯誤對象中餐曼,這些錯誤信息我們也可以通過SpringMVC提供的標簽在前端JSP頁面上進行展示货岭。
使用Validator接口進行驗證
在SpringMVC中提供了一個Validator接口,我們可以通過該接口來定義我們自己對實體對象的驗證徘禁。接下來看一個示例诅诱。
假設我們現(xiàn)在有一個需要進行驗證的實體類User,其代碼如下所示:
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String toString() {
return username + ", " + password;
}
}
那么當我們需要使用SpringMVC提供的Validator接口來對該實體類進行校驗的時候該如何做呢送朱?這個時候我們應該提供一個Validator的實現(xiàn)類娘荡,并實現(xiàn)Validator接口的supports方法和validate方法干旁。
Supports方法用于判斷當前的Validator實現(xiàn)類是否支持校驗當前需要校驗的實體類,只有當supports方法的返回結果為true的時候炮沐,該Validator接口實現(xiàn)類的validate方法才會被調用來對當前需要校驗的實體類進行校驗争群。這里假設我們需要驗證User類的username和password都不能為空,先給出其代碼大年,稍后再進行解釋换薄。這里我們定義一個UserValidator,其代碼如下:
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
public class UserValidator implements Validator {
public boolean supports(Class<?> clazz) {
// TODO Auto-generated method stub
return User.class.equals(clazz);
}
public void validate(Object obj, Errors errors) {
// TODO Auto-generated method stub
ValidationUtils.rejectIfEmpty(errors, "username", null, "Username is empty.");
User user = (User) obj;
if (null == user.getPassword() || "".equals(user.getPassword()))
errors.rejectValue("password", null, "Password is empty.");
}
}
在上述代碼中我們在supports方法中定義了該UserValidator只支持對User對象進行校驗翔试。在validate方法中我們校驗了User對象的username和password不為empty的情況轻要,這里的empty包括null和空字符串兩種情況。ValidationUtils類是Spring中提供的一個工具類垦缅。Errors就是Spring用來存放錯誤信息的對象冲泥。
我們已經定義了一個對User類進行校驗的UserValidator了,但是這個時候UserValidator還不能對User對象進行校驗失都,因為我們還沒有告訴Spring應該使用UserValidator來校驗User對象柏蘑。在SpringMVC中我們可以使用DataBinder來設定當前Controller需要使用的Validator。先來看下面一段代碼:
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class UserController {
@InitBinder
public void initBinder(DataBinder binder) {
binder.setValidator(new UserValidator());
}
@RequestMapping("login")
public String login(@Valid User user, BindingResult result) {
if (result.hasErrors())
return "redirect:user/login";
return "redirect:/";
}
}
在上面這段代碼中我們可以看到我們定義了一個UserController粹庞,該Controller有一個處理login操作的處理器方法login咳焚,它需要接收客戶端發(fā)送的一個User對象,我們就是要利用前面的UserValidator對該User對象進行校驗庞溜。首先我們可以看到我們login方法接收的參數(shù)user是用@Valid進行標注的革半,這里的@Valid是定義在JSR-303標準中的,我這里使用的是Hibernate Validation對它的實現(xiàn)流码。這里我們必須使用@Valid標注我們需要校驗的參數(shù)user又官,否則Spring不會對它進行校驗。另外我們的處理器方法必須給定包含Errors的參數(shù)漫试,這可以是Errors本身六敬,也可以是它的子類BindingResult,使用了Errors參數(shù)就是告訴Spring關于表單對象數(shù)據校驗的錯誤將由我們自己來處理驾荣,否則Spring會直接拋出異常外构,而且這個參數(shù)是必須緊挨著@Valid參數(shù)的,即必須緊挨著需要校驗的參數(shù)播掷,這就意味著我們有多少個@Valid參數(shù)就需要有多少個對應的Errors參數(shù)审编,它們是一一對應的。前面有提到我們可以通過DataBinder來指定需要使用的Validator歧匈,我們可以看到在上面代碼中我們通過@InitBinder標記的方法initBinder設置了當前Controller需要使用的Validator是UserValidator垒酬。這樣當我們請求處理器方法login時就會使用DataBinder設定的UserValidator來校驗當前的表單對象User,首先會通過UserValidator的supports方法判斷其是否支持User對象的校驗,若支持則調用UserValidator的validate方法勘究,并把相關的校驗信息存放到當前的Errors對象中矮湘。接著我們就可以在我們的處理器方法中根據是否有校驗異常信息來做不同的操作。在上面代碼中我們定義了在有異常信息的時候就跳轉到登陸頁面乱顾。這樣我們就可以在登陸頁面上通過errors標簽來展示這些錯誤信息了板祝。
我們知道在Controller類中通過@InitBinder標記的方法只有在請求當前Controller的時候才會被執(zhí)行宫静,所以其中定義的Validator也只能在當前Controller中使用走净,如果我們希望一個Validator對所有的Controller都起作用的話,我們可以通過WebBindingInitializer的initBinder方法來設定了孤里。另外伏伯,在SpringMVC的配置文件中通過mvc:annotation-driven的validator屬性也可以指定全局的Validator。代碼如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<mvc:annotation-driven validator="userValidator"/>
<bean id="userValidator" class="com.xxx.xxx.UserValidator"/>
</beans>
使用JSR-303 Validation進行驗證
JSR-303是一個數(shù)據驗證的規(guī)范捌袜,這里我不會講這個規(guī)范是怎么回事说搅,只會講一下JSR-303在SpringMVC中的應用。JSR-303只是一個規(guī)范虏等,而Spring也沒有對這一規(guī)范進行實現(xiàn)弄唧,那么當我們在SpringMVC中需要使用到JSR-303的時候就需要我們提供一個對JSR-303規(guī)范的實現(xiàn),Hibernate Validator是實現(xiàn)了這一規(guī)范的霍衫,這里我將以它作為JSR-303的實現(xiàn)來講解SpringMVC對JSR-303的支持候引。
JSR-303的校驗是基于注解的,它內部已經定義好了一系列的限制注解敦跌,我們只需要把這些注解標記在需要驗證的實體類的屬性上或是其對應的get方法上澄干。來看以下一個需要驗證的實體類User的代碼:
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotBlank;
public class User {
private String username;
private String password;
private int age;
@NotBlank(message="用戶名不能為空")
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@NotNull(message="密碼不能為null")
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Min(value=10, message="年齡的最小值為10")
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
我們可以看到我們在username、password和age對應的get方法上都加上了一個注解柠傍,這些注解就是JSR-303里面定義的限制麸俘,其中@NotBlank是Hibernate Validator的擴展。不難發(fā)現(xiàn)惧笛,使用JSR-303來進行校驗比使用Spring提供的Validator接口要簡單的多从媚。我們知道注解只是起到一個標記性的作用,它是不會直接影響到代碼的運行的患整,它需要被某些類識別到才能起到限制作用拜效。
使用SpringMVC的時候我們只需要把JSR-303的實現(xiàn)者對應的jar包放到classpath中,然后在SpringMVC的配置文件中引入MVC Namespace并级,并加上mvn:annotation-driven/就可以非常方便的使用JSR-303來進行實體對象的驗證拂檩。加上了mvn:annotation-driven/之后Spring會自動檢測classpath下的JSR-303提供者并自動啟用對JSR-303的支持,把對應的校驗錯誤信息放到Spring的Errors對象中嘲碧。這時候SpringMVC的配置文件如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<mvc:annotation-driven/>
</beans>
接著我們來定義一個使用User對象作為參數(shù)接收者的Controller挟鸠,其代碼如下所示:
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class UserController {
@RequestMapping("login")
public String login(@Valid User user, BindingResult result) {
if (result.hasErrors())
return "user/login";
return "redirect:/";
}
}
這樣當我們不帶任何參數(shù)請求login.do的時候就不能通過實體對象User的屬性數(shù)據有效性限制娃弓,然后會把對應的錯誤信息放置在當前的Errors對象中爽丹。
JSR-303原生支持的限制有如下幾種:
限制 | 說明 |
---|---|
@Null | 限制只能為null |
@NotNull | 限制必須不為null |
@AssertFalse | 限制必須為false |
@AssertTrue | 限制必須為true |
@DecimalMax(value) | 限制必須為一個不大于指定值的數(shù)字 |
@DecimalMin(value) | 限制必須為一個不小于指定值的數(shù)字 |
@Digits(integer,fraction) | 限制必須為一個小數(shù)糕韧,且整數(shù)部分的位數(shù)不能超過integer,小數(shù)部分的位數(shù)不能超過fraction |
@Future | 限制必須是一個將來的日期 |
@Max(value) | 限制必須為一個不大于指定值的數(shù)字 |
@Min(value) | 限制必須為一個不小于指定值的數(shù)字 |
@Past | 限制必須是一個過去的日期 |
@Pattern(value) | 限制必須符合指定的正則表達式 |
@Size(max,min) | 限制字符長度必須在min到max之間 |
除了JSR-303原生支持的限制類型之外我們還可以定義自己的限制類型冬耿。定義自己的限制類型首先我們得定義一個該種限制類型的注解,而且該注解需要使用@Constraint標注。現(xiàn)在假設我們需要定義一個表示金額的限制類型斟览,那么我們可以這樣定義:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import com.xxx.xxx.constraint.impl.MoneyValidator;
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MoneyValidator.class)
public @interface Money {
String message() default"不是金額形式";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
我們可以看到在上面代碼中我們定義了一個Money注解,而且該注解上標注了@Constraint注解辑奈,使用@Constraint注解標注表明我們定義了一個用于限制的注解苛茂。@Constraint注解的validatedBy屬性用于指定我們定義的當前限制類型需要被哪個ConstraintValidator進行校驗。在上面代碼中我們指定了Money限制類型的校驗類是MoneyValidator鸠窗。
另外需要注意的是我們在定義自己的限制類型的注解時有三個屬性是必須定義的妓羊,如上面代碼所示的message、groups和payload屬性稍计。
在定義了限制類型Money之后躁绸,接下來就是定義我們的限制類型校驗類MoneyValidator了。限制類型校驗類必須實現(xiàn)接口javax.validation.ConstraintValidator臣嚣,并實現(xiàn)它的initialize和isValid方法净刮。我們先來看一下MoneyValidator的代碼示例:
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import com.xxx.xxx.constraint.Money;
public class MoneyValidator implements ConstraintValidator<Money, Double> {
private String moneyReg = "^\\d+(\\.\\d{1,2})?$";//表示金額的正則表達式
private Pattern moneyPattern = Pattern.compile(moneyReg);
public void initialize(Money money) {
// TODO Auto-generated method stub
}
public boolean isValid(Double value, ConstraintValidatorContext arg1) {
// TODO Auto-generated method stub
if (value == null)
return true;
return moneyPattern.matcher(value.toString()).matches();
}
}
從上面代碼中我們可以看到ConstraintValidator是使用了泛型的。
它一共需要指定兩種類型:
- 第一個類型是對應的initialize方法的參數(shù)類型硅则,
- 第二個類型是對應的isValid方法的第一個參數(shù)類型淹父。
從上面的兩個方法我們可以看出isValid方法是用于進行校驗的,有時候我們在校驗的過程中是需要取當前的限制類型的屬性來進行校驗的抢埋,比如我們在對@Min限制類型進行校驗的時候我們是需要通過其value屬性獲取到當前校驗類型定義的最小值的弹灭,我們可以看到isValid方法無法獲取到當前的限制類型Money。
這個時候initialize方法的作用就出來了揪垄。我們知道initialize方法是可以獲取到當前的限制類型的穷吮,所以當我們在校驗某種限制類型時需要獲取當前限制類型的某種屬性的時候,我們可以給當前的ConstraintValidator定義對應的屬性饥努,然后在initialize方法中給該屬性賦值捡鱼,接下來我們就可以在isValid方法中使用其對應的屬性了。
針對于這種情況我們來看一個代碼示例酷愧,現(xiàn)在假設我要定義自己的@Min限制類型和對應的MinValidator校驗器驾诈,那么我可以如下定義:
Min限制類型
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MinValidator.class)
public @interface Min {
int value() default 0;
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
MinValidator校驗器
public class MinValidator implements ConstraintValidator<Min, Integer> {
private int minValue;
public void initialize(Min min) {
// TODO Auto-generated method stub
//把Min限制類型的屬性value賦值給當前ConstraintValidator的成員變量minValue
minValue = min.value();
}
public boolean isValid(Integer value, ConstraintValidatorContext arg1) {
// TODO Auto-generated method stub
//在這里我們就可以通過當前ConstraintValidator的成員變量minValue訪問到當前限制類型Min的value屬性了
return value >= minValue;
}
}
繼續(xù)來說一下ConstraintValidator泛型的第二個類型,我們已經知道它的第二個類型是對應的isValid的方法的第一個參數(shù)溶浴,從我給的參數(shù)名稱value來看也可以知道isValid方法的第一個參數(shù)正是對應的當前需要校驗的數(shù)據的值乍迄,而它的類型也正是對應的我們需要校驗的數(shù)據的數(shù)據類型。
這兩者的數(shù)據類型必須保持一致士败,否則Spring會提示找不到對應數(shù)據類型的ConstraintValidator闯两。建立了自己的限制類型及其對應的ConstraintValidator后褥伴,其用法跟標準的JSR-303限制類型是一樣的。
以下就是使用了上述自己定義的JSR-303限制類型——Money限制和Min限制的一個實體類:
public class User {
private int age;
private Double salary;
@Min(value=8, message="年齡不能小于8歲")
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Money(message="標準的金額形式為xxx.xx")
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
}
另外再講一點Spring對自定義JSR-303限制類型支持的新特性漾狼,那就是Spring支持往ConstraintValidator里面注入bean對象≈芈現(xiàn)在假設我們在MoneyValidator里面需要用到Spring ApplicationContext容器中的一個UserController bean對象,那么我們可以給ConstraintValidator定義一個UserController屬性逊躁,并給定其set方法似踱,在set方法上加注解@Resource或@Autowired通過set方式來注入當前的ApplicationContext中擁有的UserController bean對象。關于@Resource和@AutoWired的區(qū)別可以參考這篇博客 稽煤。
所以我們可以這樣來定義我們的MoneyValidator:
public class MoneyValidator implements ConstraintValidator<Money, Double> {
private String moneyReg = "^\\d+(\\.\\d{1,2})?$";//表示金額的正則表達式
private Pattern moneyPattern = Pattern.compile(moneyReg);
private UserController controller;
public void initialize(Money money) {
// TODO Auto-generated method stub
}
public boolean isValid(Double value, ConstraintValidatorContext arg1) {
// TODO Auto-generated method stub
System.out.println("UserController: .............." + controller);
if (value == null)
returntrue;
return moneyPattern.matcher(value.toString()).matches();
}
public UserController getController() {
return controller;
}
@Resource
public void setController(UserController controller) {
this.controller = controller;
}
}