Java for Web学习笔记(七七):Validation(1)启动验证

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个部分

  1. 定义Validator,也就是Spring validator bean,为Validator进行消息本地化
  2. 方法验证的处理器
  3. 在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相关文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值