(1)Spring validation的结构,api和实现类
(2)No Validator的两个场景
Spring的参数校验会用到两个库:validation-api,hibernate-validator
validation-api是一套标准,hibernate-validator实现了此标准
JSR-303 是Java EE 6 中的一项子规范,叫做BeanValidation,官方参考实现是hibernate-validator。
hibernate-validator实现了JSR-303规范
@Validated org.springframework.validation.annotation.Validated jar包:spring-context
@Valid javax.validation.Valid jar包:javax.validation
Spring Validation验证框架对参数的验证机制提供了@Validated【org.springframework.validation.annotation.Validated】(Spring's JSR-303规范,是标准JSR-303的一个变种),
javax提供了@Valid【javax.validation.Valid】(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。
其中对于字段的特定验证注解比如@NotNull等网上到处都有,这里不详述
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
注解地方
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。
嵌套验证
@Valid可以加在子类上,进行嵌套验证
@Valid的参数后必须紧挨着一个BindingResult 参数,否则spring会在校验不通过时直接抛出异常
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#isBindExceptionRequired
org.springframework.validation.BindingResult
org.springframework.validation.Errors
参考:
http://www.imooc.com/article/details/id/292766
【已解决】javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint
https://www.cnblogs.com/softidea/p/6044123.html
spring boot 1.4默认使用 hibernate validator
https://www.cnblogs.com/softidea/p/6043879.html
Spring boot将配置属性注入到bean 专题 【@ConfigurationProperties是支持JSR303数据校验的】
https://www.cnblogs.com/softidea/p/5863330.html
Error creating bean with name ‘org.springframework.validation.beanvalidation.LocalValidatorFactoryBean#0
https://www.cnblogs.com/softidea/p/5631143.html
@Valid springMVC bean校验不起作用及如何统一处理校验【@Valid的参数后必须紧挨着一个BindingResult 参数,否则spring会在校验不通过时直接抛出异常】
https://www.cnblogs.com/softidea/p/5839327.html
SpringMVC参数校验
https://www.cnblogs.com/softidea/p/10079965.html
控制层中使用dto接收参数并使用@Validated/@Valid注解开启对参数的校验
@Validated注解表示使用Spring的校验机制,支持分组校验,声明在入参上.
@Valid注解表示使用Hibernate的校验机制,不支持分组校验,声明在入参上.
JSR-303 原生支持的限制有如下几种 :
限制 | 说明 |
@Null | 限制只能为 null |
@NotNull | 限制必须不为 null |
@AssertFalse | 限制必须为 false |
@AssertTrue | 限制必须为 true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过 integer ,小数部分的位数不能超过 fraction |
@Future | 限制必须是一个将来的日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Past | 限制必须是一个过去的日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在 min 到 max 之间 |
@NotNull: The CharSequence, Collection, Map or Array object is not null, but can be empty. @NotEmpty: The CharSequence, Collection, Map or Array object is not null and size > 0. @NotBlank: The string is not null and the trimmed length is greater than zero.
1.@NotNull:不能为null,但可以为empty
(""," "," ")
2.@NotEmpty:不能为null,而且长度必须大于0
(" "," ")
3.@NotBlank:只能作用在String上,不能为null,而且调用trim()后,长度必须大于0
("test") 即:必须有实际字符
4.examples:
1.String name = null;
@NotNull: false
@NotEmpty:false
@NotBlank:false
2.String name = "";
@NotNull:true
@NotEmpty: false
@NotBlank: false
3.String name = " ";
@NotNull: true
@NotEmpty: true
@NotBlank: false
4.String name = "Great answer!";
@NotNull: true
@NotEmpty:true
@NotBlank:true
*参考链接:
http://stackoverflow.com/questions/17137307/in-hibernate-validator-4-1-what-is-the-difference-between-notnull-notempty
SpringMVC Validation 介绍
对于任何一个应用而言在客户端做的数据有效性验证都不是安全有效的,这时候就要求我们在开发的时候在服务端也对数据的有效性进行验证。
SpringMVC 自身对数据在服务端的校验有一个比较好的支持,它能将我们提交到服务端的数据按照我们事先的约定进行数据有效性验证,对于不合格的数据信息 SpringMVC 会把它保存在错误对象中,这些错误信息我们也可以通过 SpringMVC 提供的标签在前端 JSP 页面上进行展示。
使用 Validator 接口进行验证
在 SpringMVC 中提供了一个 Validator 接口,我们可以通过该接口来定义我们自己对实体对象的验证。接下来看一个示例。
假设我们现在有一个需要进行验证的实体类 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 的实现类,并实现 Validator接口的 supports 方法和 validate 方法。 Supports 方法用于判断当前的 Validator 实现类是否支持校验当前需要校验的实体类,只有当 supports 方法的返回结果为 true 的时候,该 Validator 接口实现类的 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 ,它需要接收客户端发送的一个 User 对象,我们就是要利用前面的 UserValidator 对该 User 对象进行校验。首先我们可以看到我们 login 方法接收的参数 user 是用 @Valid 进行标注的,这里的 @Valid 是定义在JSR-303 标准中的,我这里使用的是 Hibernate Validation 对它的实现。这里我们 必须使用 @Valid 标注我们需要校验的参数 user ,否则 Spring 不会对它进行校验。另外我们的 处理器方法必须给定包含 Errors 的参数 ,这可以是 Errors 本身,也可以是它的子类 BindingResult ,使用了 Errors 参数就是告诉 Spring 关于表单对象数据校验的错误将由我们自己来处理,否则 Spring 会直接抛出异常。前面有提到我们可以通过 DataBinder 来指定需要使用的 Validator ,我们可以看到在上面代码中我们通过 @InitBinder 标记的方法 initBinder 设置了当前 Controller 需要使用的 Validator 是 UserValidator 。这样当我们请求处理器方法 login 时就会使用 DataBinder 设定的 UserValidator 来校验当前的表单对象 User ,首先会通过 UserValidator 的 supports 方法判断其是否支持 User 对象的校验,若支持则调用 UserValidator 的 validate 方法,并把相关的校验信息存放到当前的 Errors 对象中。接着我们就可以在我们的处理器方法中根据是否有校验异常信息来做不同的操作。在上面代码中我们定义了在有异常信息的时候就跳转到登陆页面。这样我们就可以在登陆页面上通过 errors 标签来展示这些错误信息了。
我们知道在 Controller 类中通过 @InitBinder 标记的方法只有在请求当前 Controller 的时候才会被执行,所以其中定义的 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 是一个数据验证的规范,这里我不会讲这个规范是怎么回事,只会讲一下 JSR-303 在 SpringMVC 中的应用。
JSR-303 只是一个规范,而 Spring 也没有对这一规范进行实现,那么当我们在 SpringMVC 中需要使用到 JSR-303 的时候就需要我们提供一个对 JSR-303 规范的实现,
Hibernate Validator 是实现了这一规范的,这里我将以它作为 JSR-303 的实现来讲解 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 的扩展。
不难发现,使用 JSR-303 来进行校验比使用 Spring 提供的 Validator 接口要简单的多。
我们知道注解只是起到一个标记性的作用,它是不会直接影响到代码的运行的,它需要被某些类识别到才能起到限制作用。
使用 SpringMVC 的时候我们只需要把 JSR-303 的实现者对应的 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 对象作为参数接收者的 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:/";
} }
这样当我们不带任何参数请求 login.do 的时候就不能通过实体对象 User 的属性数据有效性限制,然后会把对应的错误信息放置在当前的 Errors 对象中。
除了 JSR-303 原生支持的限制类型之外我们还可以定义自己的限制类型。定义自己的限制类型首先我们得定义一个该种限制类型的注解,而且该注解需要使用 @Constraint 标注。
现在假设我们需要定义一个表示金额的限制类型,那么我们可以这样定义:
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 了。限制类型校验类必须实现接口 javax.validation.ConstraintValidator ,并实现它的 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) { } public boolean isValid(Double value, ConstraintValidatorContext arg1) {
if (value == null)
return true;
return moneyPattern.matcher(value.toString()).matches();
} }
从上面代码中我们可以看到 ConstraintValidator 是使用了泛型的。它一共需要指定两种类型,第一个类型是对应的 initialize 方法的参数类型,第二个类型是对应的 isValid 方法的第一个参数类型。从上面的两个方法我们可以看出 isValid 方法是用于进行校验的,有时候我们在校验的过程中是需要取当前的限制类型的属性来进行校验的,比如我们在对 @Min 限制类型进行校验的时候我们是需要通过其 value 属性获取到当前校验类型定义的最小值的,我们可以看到 isValid 方法无法获取到当前的限制类型 Money 。这个时候 initialize 方法的作用就出来了。我们知道 initialize 方法是可以获取到当前的限制类型的,所以当我们在校验某种限制类型时需要获取当前限制类型的某种属性的时候,我们可以给当前的 ConstraintValidator 定义对应的属性,然后在 initialize 方法中给该属性赋值,接下来我们就可以在 isValid 方法中使用其对应的属性了。针对于这种情况我们来看一个代码示例,现在假设我要定义自己的 @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) {
//把Min限制类型的属性value赋值给当前ConstraintValidator的成员变量minValue
minValue = min.value();
} public boolean isValid(Integer value, ConstraintValidatorContext arg1) {
//在这里我们就可以通过当前ConstraintValidator的成员变量minValue访问到当前限制类型Min的value属性了
return value >= minValue;
} }
继续来说一下 ConstraintValidator 泛型的第二个类型 ,我们已经知道它的第二个类型是 对应的 isValid 的方法的第一个参数 ,从我给的参数名称 value 来看也可以知道 isValid 方法的第一个参数正是对应的当前需要校验的数据的值,而它的类型也 正是对应的我们需要校验的数据的数据类型。 这两者的数据类型必须保持一致,否则 Spring 会提示找不到对应数据类型的 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 对象 。现在假设我们在 MoneyValidator 里面需要用到 Spring ApplicationContext 容器中的一个 UserController bean 对象,那么我们可以给 ConstraintValidator 定义一个 UserController 属性,并给定其 set方法,在 set 方法上加注解 @Resource 或 @Autowired 通过 set 方式来注入当前的 ApplicationContext 中拥有的 UserController bean 对象。关于 @Resource 和 @AutoWired 的区别可以参考 这篇博客。
所以我们可以这样来定义我们的 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) { } public boolean isValid(Double value, ConstraintValidatorContext arg1) {
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;
} }
http://www.tuicool.com/articles/mYrqMb