Bean validation和Hibernate Validator
我们经常要验证数据的输入和输出是否合规,例如不允许为null,不允许为空,对于email地址,其正则表达式如下:
/* 正则表达式:
* ^[a-z0-9`!#$%^&*'{}?/+=|_~-] : ^ 匹配输入字符串的开始位置 []表示匹配内部的任何一个
* + 表示前面的表达式出现1次或者多次
* (\\.[a-z0-9`!#$%^&*'{}?/+=|_~-]+) : () 匹配这一pattern并获取这一pattern
* \\. .在正则表达式里面的\.,因为放在string里面,所以是\\.
* * 表示前面的出现0次或者多次
* @
* ? 表示前面的子表达式零次或一次。
* ([a-z0-9]([a-z0-9-]*[a-z0-9])?) :? 匹配前面的子表达式零次或一次
* $ 匹配输入字符串的结束位置
* */
String regexp = "^[a-z0-9`!#$%^&*'{}?/+=|_~-]+(\\.[a-z0-9`!#$%^&*'{}?/+=|_~-]+)*"
+ "@"
+ "([a-z0-9]([a-z0-9-]*[a-z0-9])?)+(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$";
J2EE提供Bean validation为验证提供方便。JSR-303是Bean validation的v1.0规范,在Java EE 6中使用,它定义了annotation和API,JSR-349是Bean validation的v1.1规范,在Java EE 7中用。我们在http://beanvalidation.org/网站上看到最新版本的是v2.0,即JSR380,计划在Java EE 8中提供。在V2.0中,对email的的验证,可以直接通过annotation了。标记方式简化代码,下面是v2.0的范例。
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
public class User {
private String email;
@NotNull @Email
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
JSR-349只是规范,需要具体的实现,一般使用
Hibernate Validator。Hibernate Validator是Bean validation的灵感来源,所以走在Hibernate Validator之前,v5.0支持JSR-349,现在到6.0.2.Final版本,支持Bean Validation 2.0,即JSR-380。我们需要在pom.xml中引入:
<!-- Bean Validation API: 使用v2.0 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.0.Final</version>
<scope>compile</scope>
</dependency>
<!-- 对Bean Validation API v2.0的具体实现 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.2.Final</version>
<scope>runtime</scope>
</dependency>
手动验证的小例子
前面给出User类,要求email非null,并符合email的格式,下面给出一个小例子,在代码中验证User对象的合法性。public void test(){
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
User user = new User();
Set<ConstraintViolation<User>> violations = validator.validate(user);
if(violations.size() > 0){
violations.forEach(v -> logger.error(v));
throw new ConstraintViolationException(violations);
}
return "abc";
}
这里显然违反了email不能为null的要求,执行的时候会抛出异常:
javax.validation.ConstraintViolationException: email: 不能为null
我们将具体的ConstraintViolation打印出来:
ConstraintViolationImpl{interpolatedMessage='不能为null', propertyPath=email, rootBeanClass=class cn.wei.chapter16.site.hr_portal.User, messageTemplate='{javax.validation.constraints.NotNull.message}'}
在Spring框架中配置Validation
手动验证的方式,在代码中会很繁复,给出手动了例子,主要是了解一下Bean validation是如何工作的,这些都应该能够自动进行。在Spring框架中配置Validation包括以下3个部分
- 定义Validator,也就是Spring validator bean,为Validator进行消息本地化
- 方法验证的处理器
- 在Spring MVC使用同一验证bean
步骤1:在root上下文中设置Spring validator bean
在RootContextConfiguration中:
@Bean
public MessageSource messageSource(){
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setCacheSeconds(-1); //Cache forever
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
messageSource.setBasenames("/WEB-INF/i18n/messages", "/WEB-INF/i18n/titles","/WEB-INF/i18n/validation");
return messageSource;
}
/* 1)定义Spring validator bean。
Spring提供了两个Bean,LocalValidatorFactoryBean和CustomValidatorBean。
LocalValidatorFactoryBean :This is the central class for javax.validation (JSR-303) setup in a Spring
application context: It bootstraps a javax.validation.ValidationFactory and exposes it through the Spring
org.springframework.validation.Validator interface as well as through the JSR-303 javax.validation.Validator
interface and the javax.validation.ValidatorFactory interface itself.
LocalValidatorFactoryBean支持javax.validation.ValidationFactory接口,也支持javax.validation.Validator接口,由于它extends SpringValidatorAdapter,因此也支持org.springframework.validation.Validator接口,属于N合一的Bean。
org.springframework.validation.Validator提供validate(Object target, org.springframework.validation.Errors errors)接口,将错误输出到Errors。下面是一个controller方法的例子,对页面的form的输入数据进行验证,如果错误,存放到Errors中:
public ModelAndView createEmployee(Map<String, Object> model,@Valid EmployeeForm form, Errors errors)
*/
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean(){
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
/* 1.1)自动寻找Validator实现。
LocalValidatorFactoryBean自动检查在classpath中的Bean Validation的实现,将javax.validation.ValidatorFactory作为其缺省备选,本例将自动找到Hibernate Validator。但是如果在classpath下面有超过一个实现(例如运行在完全的J2EE web应用服务器,如GlassFish或WebSphere),这时通过下面方式指定采用哪个,以避免不可测性。
validator.setProviderClass(HibernateValidator.class);
但这样的缺点在于是complie的而不是runtime的。要runtime,可以采用
validator.setProviderClass(Class.forName("org.hibernate.validator.HibernateValidator"));
但如果类写错了,无法在compile的时候查出 */
// validator.setProviderClass(Class.forName("org.hibernate.validator.HibernateValidator"));
/* 1.2)为Validator进行消息本地化
缺省的使用classpath路径下的ValidationMessages.properties, ValidationMessages_[language].properties, ValidationMessages_[language]_[region].properties),但在Bean validation1.1开始,可以自行提供国际化方式。*/
validator.setValidationMessageSource(this.messageSource());
return validator;
}
本地化验证的小例子
我们将前面手动验证的代码做一点小修改,测试一下本地化的情况。public class User {
//... ...
@NotNull(message = "{validate.user.notnull}") //在WEB-INF/i18n/中进行相关的设置
@Email(message = "{validate.user.email}")
public String getEmail() {
return email;
}
}
已经在Root上下文中配置了localValidatorFactoryBean,可以直接注入使用。
@Inject LocalValidatorFactoryBean validator;
public void test2(){
User user = new User();
user.setEmail("abc");
Set<ConstraintViolation<User>> violations = validator.validate(user);
if(violations.size() > 0){
violations.forEach(v -> logger.error(v));
throw new ConstraintViolationException(violations);
}
}
我们看到输出log为
ConstraintViolationImpl{interpolatedMessage='要求电子邮件的格式', propertyPath=email, rootBeanClass=class cn.wei.chapter16.site.hr_portal.User, messageTemplate='{validate.user.email}'}
interpolatedMessage已经根据messageTemplate,根据locale进行了本地化,抛出的异常为:
javax.validation.ConstraintViolationException: email: 要求电子邮件的格式
步骤2:在root上下文中设置方法验证
Bean Validation使用@javax.validation.Constraint标记或者自定义标记在允许在field、方法和方法的参数。- field:当调用对象的一个受验方法时,验证器对该field时进行检验。
- method:将在方法执行后检查该方法的返回值。如果我们加在一个getter上,和加载field上的效果一样。
- method parameter:在方法前检查输出的参数。
对于method的输入和输出的限制,一般应在interface上注明,确保实现着和使用者清晰,这就是所谓的PbC(programming by contract)。使用PbC,需要创建一个proxy来验证具体实现的类,相关注入要调用proxy。一个完整的Java EE 7 web应用服务器提供的DI proxied,如果使用简单servlet容器,如tomcat,需要提供其他的DI解决方案。Spring framwork就是其中的解决方案,它的DI(依赖注入,dependency injecttion)解决这个问题。
Spring framework使用bean post-processor的概念在完成startup之前来配置,个性化,甚至替换bean。,设置 BeanPostProcessor的实现将在一个bean注入到其他需要它的bean之前完成。我们接触过的实现有:
- AutowiredAnnotationBeanPostProcessor:这是framework自动生成的bean,用来扫描@Autowired,@Inject的
- InitDestroyAnnotationBeanPostProcessor是寻找InitializingBean的实现(@PostConstruct)和DisposableBean的实现(@PreDestroy)
- AsyncAnnotationBeanPostProcessor是用来替换bean的,寻找带有@Async的方法,替换为proxy,是这些方法可以异步运行
对于方法的输入输出的验证,通过MethodValidationPostProcessor来调用proxy。MethodValidationPostProcessor是一个BeanPostProcessor的实现,和AsyncAnnotationBeanPostProcessor一样,都实现了org.springframework.aop.framework.ProxyConfig,也就是采用proxy来进行替代,但不同的是,这个方法验证后处理器不能由spring自动生产,需要通过代码。
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor(){
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setValidator(this.localValidatorFactoryBean());
return processor;
}
笔者曾经犯了个低级错误,将类的标记@Configuration写成了@Configurable,一旦执行processor.setValidator(),就会报错。但是比较奇怪的是如果不进行验证validator的配置,似乎也运行流畅。
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'methodValidationPostProcessor' defined in cn.wei.flowingflying.customer_support.config.RootContextConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.validation.beanvalidation.MethodValidationPostProcessor]: Factory method 'methodValidationPostProcessor' threw exception; nested exception is java.lang.IllegalArgumentException: No target ValidatorFactory set
步骤3:在Spring MVC使用同一验证bean
Spring的MVC controller对form对象和参数的验证使用Spring validator对象。LocalValidatorFactoryBean实现了spring validator的api,但缺省地,Spring MVC会创建另一个独立的Spring validator对象。要使用统一bean,我们需要手动进行设置。因为是在MVC中使用,因此必须要加上@EnableWebMvc
,否则不能在controller中检查方法的参数输入,即Errors errors
都是no error的。
在SerlvetContextConfiguration中重写WebMvcConfigurerAdapter的方法getValidator():
@Inject SpringValidatorAdapter validator;
@Override
public Validator getValidator() {
return this.validator;
}
相关文章相关链接: 我的Professional Java for Web Applications相关文章