Spring 里的数据校验
相信大家都知道什么是数据校验吧,简单说就是对数据处理前进行验证,包括有效性验证,格式验证,完整性验证,等等。Spirng对此主要提供了两种验证支持:
1.使用spring validator 接口
2.使用JSR-303, Bean Validation API
下面让我们一个一个来看:
使用spring validator 接口:这种方式主要是通过实现Validator接口。假设有个Contact类,里面的first name 属性不能为空, 代码如下
package com.apress.prospring3.ch14.domain;
import java.net.URL;
import org.joda.time.DateTime;
public class Contact {
private String firstName;
private String lastName;
private DateTime birthDate;
private URL personalSite;
// Getter/setter methods omitted
public String toString() {
return "First name: " + getFirstName() + " - Last name: "
+ getLastName() + " - Birth date: " + getBirthDate()
+ " - Personal site: " + getPersonalSite();
}
}
对此我们来创建一个数据校验类,代码如下:
package com.apress.prospring3.ch14.validator;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.apress.prospring3.ch14.domain.Contact;
@Component("contactValidator")
public class ContactValidator implements Validator {
public boolean supports(Class<?> clazz) {
return Contact.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "firstName", "firstName.empty");
}
}
可以看到这个类实现了接口Validator的2个方法:support()代表这个validator类所支持的类型;validate()方法用来对传入的对象进行校验,返回的结果会存入org.springframework.validation.Errors。可以看到在上面我们使用了spring自带的校验方法ValidationUtils.rejectIfEmpty()来校验属性是否为空,最后的firstName.empty表示错误代码,可以被用于国际化。
接下来需要配置下spring,因为配置了标注,只需让spring自行扫描即可:
<?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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd"> <context:annotation-config /> <context:component-scan base-package="com.apress.prospring3.ch14.validator" /> </beans>
这样一个简单的validator就写完了,下面是测试代码:
package com.apress.prospring3.ch14.validator;
import java.util.List;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.apress.prospring3.ch14.domain.Contact;
public class SpringValidatorSample {
public static void main(String[] args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring-validator-app-context.xml");
ctx.refresh();
Contact contact = new Contact();
contact.setFirstName(null);
contact.setLastName("Ho");
Validator contactValidator = ctx.getBean("contactValidator",
Validator.class);
BeanPropertyBindingResult result = new BeanPropertyBindingResult(
contact, "Clarence");
ValidationUtils.invokeValidator(contactValidator, contact, result);
List<ObjectError> errors = result.getAllErrors();
System.out.println("No of validation errors: " + errors.size());
for (ObjectError error : errors) {
System.out.println(error.getCode());
}
}
}
可以看见测试代码里创建的Contact对象的firstName=null。为了捕获校验结果,我们创建了BeanPropertyBindingResult类,并且使用了ValidationUtils.invokeValidator()来手动调用validator。跑完程序结果应该如下:
No of validation errors: 1
firstName.empty
使用JSR-303, Bean Validation API
Spring 3对JSR-303的支持非常棒,Bean Validation API里定义了大量的校验标注,使用起来非常方便。比如@NotNull。
口说无凭,还是通过例子来学习,我们创建一个Customer类:
package com.apress.prospring3.ch14.domain;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class Customer {
@NotNull
@Size(min = 2, max = 60)
private String firstName;
private String lastName;
@NotNull
private CustomerType customerType;
private Gender gender;
// Getter/setter methods omitted
public boolean isIndividualCustomer() {
return this.customerType.equals(CustomerType.INDIVIDUAL);
}
}
可以看到,我们在属性上定义了@NotNull,用来表示这个属性不能为空。以及@Size表示这个属性的长度必须为大于2小于60.对了,里面还用到了CustomerType和Gender,代码如下:
package com.apress.prospring3.ch14.domain;
public enum CustomerType {
INDIVIDUAL("I"), CORPORATE("C");
private String code;
private CustomerType(String code) {
this.code = code;
}
public String toString() {
return this.code;
}
}
package com.apress.prospring3.ch14.domain;
public enum Gender {
MALE("M"), FEMALE("F");
private String code;
private Gender(String code) {
this.code = code;
}
public String toString() {
return this.code;
}
}
老样子,接下来该编写spring配置文件了:
<?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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd"> <context:annotation-config /> <context:component-scan base-package="com.apress.prospring3.ch14.jsr303.service" /> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" /> </beans>
唯一不同就是我们添加了一个LocalValidatorFactoryBean的validator bean。为了能够调用,我们最好再加个简单的校验service,如下:
package com.apress.prospring3.ch14.jsr303.service;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.apress.prospring3.ch14.domain.Customer;
@Service("myBeanValidationService")
public class MyBeanValidationService {
@Autowired
private Validator validator;
public Set<ConstraintViolation<Customer>> validateCustomer(Customer customer) {
return validator.validate(customer);
}
}
在sevice类里我们注入了一个validator,一旦LocalValidatorFactoryBean被定义了,你可以在任何地方注入这个bean。执行校验也很简单,调用validate方法即可,校验的结果将被封装为ConstraintViolation<T>对象集合返回。OK,让我们测试,代码如下:
package com.apress.prospring3.ch14.jsr303;
import java.util.HashSet;
import java.util.Set;
import javax.validation.ConstraintViolation;
import org.springframework.context.support.GenericXmlApplicationContext;
import com.apress.prospring3.ch14.domain.Customer;
import com.apress.prospring3.ch14.domain.CustomerType;
import com.apress.prospring3.ch14.jsr303.service.MyBeanValidationService;
public class Jsr303Sample {
public static void main(String[] args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:jsr303-app-context.xml");
ctx.refresh();
MyBeanValidationService myBeanValidationService = ctx.getBean(
"myBeanValidationService", MyBeanValidationService.class);
Customer customer = new Customer();
// Test basic constraints
customer.setFirstName("C");
customer.setLastName("Ho");
customer.setCustomerType(null);
customer.setGender(null);
validateCustomer(customer, myBeanValidationService);
}
private static void validateCustomer(Customer customer,
MyBeanValidationService myBeanValidationService) {
Set<ConstraintViolation<Customer>> violations = new HashSet<ConstraintViolation<Customer>>();
violations = myBeanValidationService.validateCustomer(customer);
listViolations(violations);
}
private static void listViolations(
Set<ConstraintViolation<Customer>> violations) {
System.out.println("No. of violations: " + violations.size());
for (ConstraintViolation<Customer> violation : violations) {
System.out.println("Validation error for property: "
+ violation.getPropertyPath() + " with value: "
+ violation.getInvalidValue() + " with error message: "
+ violation.getMessage());
}
}
}
测试代码没什么复杂的,可以看见直接调用了validateCustomer()方法,结果正如我们期望的,有2条校验失败记录:
No. of violations: 2
Validation error for property: firstName with value: C with error message: size must be
between 2 and 60
Validation error for property: customerType with value: null with error message: may not be
Null
创建自定义的Validator
虽然JSR-303很给力,但有些比较特别的需求还是得自己来定义,下面我们来展示下如何通过自定义的方法创建Validator。自定义的步骤主要分2步,首先自定义一个validate标注,然后编写这个标注的实现代码。例子来了,这次我们稍微做下变动,提个扭曲的需求。还是刚才的Customer对象,lastName和Gender不能为空。首先自定义标注:
package com.apress.prospring3.ch14.jsr303.validator;
import java.lang.annotation.Documented;
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;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Constraint(validatedBy = IndividualCustomerValidator.class)
@Documented
public @interface CheckIndividualCustomer {
String message() default "Individual customer should have gender and last name defined";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
里面有很多标注,我们来解释下。@Target(ElementType.TYPE)表示我们的标注只能用于class这一层级。@Constraint代表这是一个校验用标注,里面的validatedBy标识了哪个实现类来实现校验逻辑,这里是IndividualCustomerValidator。
然后是里面的三个属性:
Message定义了返回的错误信息,当然也可以通过标注的方式定义。
Groups用于定义校验用于哪些校验小组。
Payload用于添加一些额外的约束信息(比如校验顺序神马的)
OK,下面是第二步,创建实现:
package com.apress.prospring3.ch14.jsr303.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import com.apress.prospring3.ch14.domain.Customer;
public class IndividualCustomerValidator implements
ConstraintValidator<CheckIndividualCustomer, Customer> {
public void initialize(CheckIndividualCustomer constraintAnnotation) {
}
public boolean isValid(Customer customer, ConstraintValidatorContext context) {
boolean result = true;
if (customer.getCustomerType() != null
&& (customer.isIndividualCustomer() && (customer.getLastName() == null || customer
.getGender() == null))) {
result = false;
}
return result;
}
}
可以发现,IndividualCustomerValidator实现了ConstraintValidator<CheckIndividualCustomer, Customer>,也就是说这个校验类是用@CheckIndividualCustomer标注来校验Customer的。方法isValid()用来存放校验逻辑,校验customer对象,并通过返回boolean类型来代表通过还是失败。
好了,到此为止,自定义已经完成了。现在我们只需要在原来的Customer类添加上我们自定义的标注@CheckIndividualCustomer即可了:
package com.apress.prospring3.ch14.domain;
// Import statements omitted
@CheckIndividualCustomer
public class Customer {
// Other code omitted
}
测试代码片段:
customer.setFirstName("Clarence");
customer.setLastName("Ho");
customer.setCustomerType(CustomerType.INDIVIDUAL);
customer.setGender(null);
validateCustomer(customer, myBeanValidationService);
运行结果:
No. of violations: 1
Validation error for property: with value: com.apress.prospring3.ch14.domain.Customer@d3f136e
with error message: Individual customer should have gender and last name defined
用AssertTrue来自定义Validator
除了自定义标注的方式外,JSR-303还能通过@AssertTrue来自定义validator。
例子来了,还是刚才的Customer类,把@CheckIndividualCustomer删除,在isValidIndividualCustomer()方法上添加如下@AssertTrue,别加错地方了。如下代码片段:
// Codes omitted
@AssertTrue(message = "Individual customer should have gender and last name defined")
private boolean isValidIndividualCustomer() {
boolean result = true;
if (getCustomerType() != null
&& (isIndividualCustomer() && (gender == null || lastName == null)))
result = false;
return result;
}
当调用validate()时,系统会检查这个方法是否返回true,如果不是则抛出错误message。运行一下,结果应该是一样的。
最后,总结下这么多的自定义校验方式究竟该使用哪种。从易用性来说,@AssertTrue无疑是最简单的,如果你的逻辑够简单,那就用它。但是如果逻辑比较复杂,那就要用自定义标注或者实现spring validation 接口的方法了,仅此而已,希望对各位有用。
下一节: Spring Remoting (-) http://wsjjasper.iteye.com/blog/1574590