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