Java Bean校验
1、@Valid 介绍
1.1 前言
在我们日常开发中,数据校验的实现是一件比较痛苦的事情,繁琐且无趣,对于一般的业务而言,极少出现一些过于复杂的校验,常常都是非空,长度,最大最小值,正则,数据关联,定值等等。
那么如何优雅的对参数进行校验呢?
JSR303就是为了解决这个问题出现的,本篇文章主要是介绍 JSR303,Hibernate Validator 等校验工具的使用,以及自定义校验注解的使用。我在开发过程中为了减少我写这些代码的coding成本,由此认识 javax.validation 包下的 @Valid 注解 和 Java 的 Bean Validation 规范。
1.2 Bean Validation
首先了解下 JSR(Java Specification Requests)
JSR:Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
Java 在2009年的 JAVAEE 6 中发布了 JSR303以及javax下的validation包内容。
这项工作的主要目标是为java应用程序开发人员提供 基于java对象的 约束(constraints)声明 和 对约束的验证工具(validator),以及约束元数据存储库和查询API。
但是该内容并没有具体的实现, Hibernate-Validator框架 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
注意:此处的Hibernate 不是 Hibernate ORM
后续关于Bean Validation 的规范提案 和 各个版本的实现 陆续得以迭代,各个版本的信息如下:
Bean Validation Version | release year | JSR Version | Hibernate impl version | apache BVal impl version |
---|---|---|---|---|
Bean Validation 1.0 | 2009年 | JSR 303 | Hibernate-Validator-4.3.1.Final | org.apache.bval 0.5 |
Bean Validation 1.1 | 2013年 | JSR 349 | Hibernate-Validator-5.1.1.Final | org.apache.bval 1.1.2 |
Bean Validation 2.0 | 2017年 | JSR 380 | Hibernate-Validator-6.0.1.Final | org.apache.bval 2.0.3(最新版本) |
本文主要基于 Hibernate-Validator 框架的实现为主。
1.3 关于validation包
Java-Api文档
其中 javax.validation.constraints包下是 bean validation 支持的约束,以注解的形式使用。
1.4 关于Spring Hibernate Validator支持的约束集信息(constraints)
JSR303 是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。注解如下:
Hibernate validator 在JSR303的基础上对校验注解进行了扩展,扩展注解如下:
2 、简单的使用
2.1配置项
2.1.1 maven – jar包引入
如果使用springMVC ,需要在 pom文件中引入对应的jar包。
<!-- hibernate.validator jar包-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
springboot项目则引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
这里不需要引入 javax.validation包,hibernate-validator框架中已经有引入了。
如果使用springboot,本文使用的是 2.1.7.RELEASE 版本,其中 spring-boot-starter-web 包中已经引入了 hibernate-validator 6.0.17.RELEASE了,不需要我们手动添加。
如果使用的非maven项目,需要下载对应的jar引入到项目中。
2.1.2 SpringMVC配置
<!--配置中需要注意的地方:
LocalValidatorFactoryBean :
validationMessageSource通过set方法注入,然后转换为MessageInterpolator对象,如果不配置validationMessageSource信息源,那么会使用默认的信息源HibernateValidations_xx_xx.properties,位于org.hibernate.validator包下,具体使用哪个文件由Locale.getDefaultLocale()决定。-->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
<property name="validationMessageSource" ref="hibernate_validator_messages"/>
</bean>
2.2 实现方式
2.2.1 @Valid + BindingResult + javax.validation.constraints
BindingResult 是 spring 对于 Hibernate-Validator的进一步封装,主要处理 约束违反信息 的处理。也就是当 判断 校验不通过时 所获取的 默认的或者自定义的 错误信息。
使用 @Valid 结合 BindingResult实例,Spring将对被注解的实体进行约束校验,原理是AOP。
2.2.2 validator.validate()
springboot 自动将 ValidatorFactory 加载到了IOC容器中,不需要进行配置(注意:SpringMVC 需要进行配置)。我们可以使用自动注入的方式获取 ValidatorFactory接口的实现的实例,进而获取 Validator接口的实现的实例,调用.validate(T var1, Class<?>… var2)方法对 java bean 进行约束检验。基于此,还可以用作AOP切面的开发。
validator接口源码
package javax.validation;
import java.util.Set;
import javax.validation.executable.ExecutableValidator;
import javax.validation.metadata.BeanDescriptor;
public interface Validator {
<T> Set<ConstraintViolation<T>> validate(T var1, Class<?>... var2);
<T> Set<ConstraintViolation<T>> validateProperty(T var1, String var2, Class<?>... var3);
<T> Set<ConstraintViolation<T>> validateValue(Class<T> var1, String var2, Object var3, Class<?>... var4);
BeanDescriptor getConstraintsForClass(Class<?> var1);
<T> T unwrap(Class<T> var1);
ExecutableValidator forExecutables();
}
ValidatorFactory 工厂接口源码
package javax.validation;
public interface ValidatorFactory extends AutoCloseable {
Validator getValidator();
ValidatorContext usingContext();
MessageInterpolator getMessageInterpolator();
TraversableResolver getTraversableResolver();
ConstraintValidatorFactory getConstraintValidatorFactory();
ParameterNameProvider getParameterNameProvider();
ClockProvider getClockProvider();
<T> T unwrap(Class<T> var1);
void close();
}
2.2.3 代码示例
下面我们用一个简单的示例来完成上诉提到的两种实现。
首先我们创建一个 简单的 po 类,也就是我们的业务实体类。
假设一个后台管理系统的职员信息管理模块,需要新增员工信息。
新建一个Employee类 用于 封装 职员的基础信息
//看包名就知道这个是hibernate附加的constraints
import org.hibernate.validator.constraints.Length;
//javax.validation.constraints包下所支持的一些约束类型
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import java.util.Date;
@Data
public class Employee {
@NotNull(message = "姓名必填!")
@Length(max = 20, message = "姓名过长!")
private String name;
@NotNull(message = "工牌必填!")
@Pattern(regexp = "^[0-9]\\d{10}",message = "请输入10位数字工牌!")//长度10,0-9
private String badgeCode;
@Pattern(regexp = "^[1-2]",message = "性别参数错误!")
@NotNull(message = "性别必填!")
private String gender;
@Past(message = "无效的出生日期!")
private Date birthDate;
}
无论是@Valid注解 + BindingResult 这种形式 或者是 直接调用Validator.validate() 方法,我们都可以直接使用这个 po 对象。
接下来我们简便点来,直接写出来两种实现方式的controller层方法。这里忽略了service层,如有其他的业务可以在service层写。代码如下:
import com.onesport.kern.beanValidationTest.dao.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.*;
import java.util.List;
import java.util.Set;
@RequestMapping(path = "/employee")
@RestController
public class EmployeeControl {
//换行符
private static String lineSeparator = System.lineSeparator();
/**
* 注解实现
* 使用@Valid 注解 实体, 并传入参数bindResult以获取校验结果信息
* @param employee
* @param bindingResult
* @return
*/
@PostMapping("/bindingResult")
public Object addEmployee(@RequestBody @Valid Employee employee, BindingResult bindingResult){
if (bindingResult.hasErrors()){
//校验结果以集合的形式返回,当然也可以获取单个。具体可以查看bindResult的API文档
List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
//StringBuilder组装异常信息
StringBuilder builder = new StringBuilder();
//遍历拼装
fieldErrorList.forEach(error -> {
builder.append(error.getDefaultMessage() + lineSeparator);
});
builder.insert(0,"use @Valid n BingdingResult :" +lineSeparator);
return builder.toString();
}
//TODO there can invoke service layer method to do someting
return "添加职员信息成功:" + employee.toString();
}
//Spring boot 已帮我们把 validation 的关键对象的实例装载如 IOC 容器中
@Autowired
private ValidatorFactory autowiredValidatorFactory;
@Autowired
private Validator autowiredValidator;
/**
* 调用validator实现
* @param employee
* @return
*/
@PostMapping("/validator")
public Object addEmployee(@RequestBody Employee employee){
System.out.println("这里将导入 由 Springboot 的 IOC 容器中获取的 校验器工厂和 校验器类");
System.out.println("validator工厂类:"+ autowiredValidatorFactory.toString());
System.out.println("validator类:"+ autowiredValidator.toString());
/**
* 下述的工厂类和校验器类也可以使用上述由IOC容器中获取的对象实例代替
*/
//实例化一个 validator工厂
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
//获取validator实例
Validator validator = validatorFactory.getValidator();
//调用调用,得到校验结果信息 Set
Set<ConstraintViolation<Employee>> constraintViolationSet = validator.validate(employee);
//StringBuilder组装异常信息
StringBuilder builder = new StringBuilder();
//遍历拼装
constraintViolationSet.forEach(violationInfo -> {
builder.append(violationInfo.getMessage() + lineSeparator);
});
if (builder.toString().length() > 0){
builder.insert(0,"use validator :" +lineSeparator);
return builder.toString();
}
//TODO there can invoke service layer method to do someting
return "添加职员信息成功:" + employee.toString();
}
}
上下两个方法,分别对应两种实现。我们测试一下结果。这里直接使用yapi接口管理工具来测试了。
首先调用一下第一个实现 @Valid + BingdingResult:
参数:
结果:
再调用一下第二个实现 直接调用 validator
参数: 这次我们只把姓名和工牌写错
结果:
控制台console:
返回信息:
2.2.4 对比不同实现的一些总结。
我们回过头来看一下上述两种实现,第一种实现 BindingResult是 Spring 提供的再封装工具,底层是调用 spring aop ,在我们业务开发中实现也相对简单,method body 也可以很方便的进行封装成一个通用的工具方法供调用,以免重复写这些代码。但是总是要带这个bindResult 参数还是比较烦。
而第二种实现方法就相对较为灵活,同样也是可编程的,在对一些Excel批量导入的数据校验等等的场景会较为灵活。
3、自定义校验注解
虽然jSR303和Hibernate Validtor 已经提供了很多校验注解,但是当面对复杂参数校验时,还是不能满足我们的要求,这时候我们就需要 自定义校验注解。
下面以List数组中不能含有null元素”为实例自定义校验注解
1、注解定义如下:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 自定义参数校验注解
* 校验 List 集合中是否有null 元素
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = ListNotHasNullValidatorImpl.class)此处指定了注解的实现类为ListNotHasNullValidatorImpl
public @interface ListNotHasNull {
/**
* 添加value属性,可以作为校验时的条件,若不需要,可去掉此处定义
*/
int value() default 0;
String message() default "List集合中不能含有null元素";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 定义List,为了让Bean的一个属性上可以添加多套规则
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List {
ListNotHasNull[] value();
}
}
2、注解实现类:
import org.springframework.stereotype.Service;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;
/**
* 自定义注解ListNotHasNull 的实现类
* 用于判断List集合中是否含有null元素
*/
@Service
public class ListNotHasNullValidatorImpl implements ConstraintValidator<ListNotHasNull, List> {
private int value;
@Override
public void initialize(ListNotHasNull constraintAnnotation) {
//传入value 值,可以在校验中使用
this.value = constraintAnnotation.value();
}
public boolean isValid(List list, ConstraintValidatorContext constraintValidatorContext) {
for (Object object : list) {
if (object == null) {
//如果List集合中含有Null元素,校验失败
return false;
}
}
return true;
}
}
3、实体类添加注解:
public class User {
//其他参数 .......
/**
* 所拥有的书籍列表
*/
@NotEmpty(message = "所拥有书籍不能为空")
@ListNotHasNull(message = "List 中不能含有null元素")
@Valid
private List<Book> books;
//getter setter 方法.......
}
使用方法同上,在在需要校验的实体属性上面加上@Valid 即可
4、分组校验
对同一个Model,我们在增加和修改时对参数的校验也是不一样的,这个时候我们就需要定义分组验证,步骤如下
1、定义两个空接口,分别代表Person对象的增加校验规则和修改校验规则
/**
* 可以在一个Model上面添加多套参数验证规则,此接口定义添加Person模型新增时的参数校验规则
*/
public interface PersonAddView {
}
/**
* 可以在一个Model上面添加多套参数验证规则,此接口定义添加Person模型修改时的参数校验规则
*/
public interface PersonModifyView {
}
2、Model上添加注解时使用指明所述的分组
public class Person {
private long id;
/**
* 添加groups 属性,说明只在特定的验证规则里面起作用,不加则表示在使用Deafault规则时起作用
*/
@NotNull(groups = {PersonAddView.class, PersonModifyView.class}, message = "添加、修改用户时名字不能为空", payload = ValidateErrorLevel.Info.class)
@ListNotHasNull.List({
@ListNotHasNull(groups = {PersonAddView.class}, message = "添加上Name不能为空"),
@ListNotHasNull(groups = {PersonModifyView.class}, message = "修改时Name不能为空")})
private String name;
@NotNull(groups = {PersonAddView.class}, message = "添加用户时地址不能为空")
private String address;
@Min(value = 18, groups = {PersonAddView.class}, message = "姓名不能低于18岁")
@Max(value = 30, groups = {PersonModifyView.class}, message = "姓名不能超过30岁")
private int age;
//getter setter 方法......
}
3、启用校验
此时启用校验和之前的不同,需要指明启用哪一组规则
/**
* 添加一个Person对象
* 此处启用PersonAddView 这个验证规则
* 备注:此处@Validated(PersonAddView.class) 表示使用PersonAndView这套校验规则,若使用@Valid 则表示使用默认校验规则,
* 若两个规则同时加上去,则只有第一套起作用
*/
@RequestMapping(value = "/person", method = RequestMethod.POST)
public void addPerson(@RequestBody @Validated({PersonAddView.class, Default.class}) Person person) {
System.out.println(person.toString());
}
/**
* 修改Person对象
* 此处启用PersonModifyView 这个验证规则
*/
@RequestMapping(value = "/person", method = RequestMethod.PUT)
public void modifyPerson(@RequestBody @Validated(value = {PersonModifyView.class}) Person person) {
System.out.println(person.toString());
}
5、Spring validator 方法级别的校验
JSR和Hibernate validator的校验只能对Object的属性进行校验,不能对单个的参数进行校验,spring 在此基础上进行了扩展,添加了MethodValidationPostProcessor拦截器,可以实现对方法参数的校验,实现如下:
1、实例化MethodValidationPostProcessor
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
2、在所要实现方法参数校验的类上面添加@Validated
@RestController
@Validated
public class ValidateController {
}
3、在方法上面添加校验规则
@RequestMapping(value = "/test", method = RequestMethod.GET)
public String paramCheck(@Length(min = 10) @RequestParam String name) {
System.out.println(name);
return null;
}
当方法上面的参数校验失败,spring 框架就回抛出异常
{
"timestamp": 1476108200558,
"status": 500,
"error": "Internal Server Error",
"exception": "javax.validation.ConstraintViolationException",
"message": "No message available",
"path": "/test"
}
从此可以优雅的对参数进行校验了
写在后面的话:
本篇文章只列举了常用的几种校验方法,其实关于校验的内容还有很多:
校验信息的国际化显示,
组合参数校验,
message中使用EL表达式,
将校验信息绑定到ModelAndView等,这里就不一一列出了,下面这几篇文章写的也不错,读者可以参考:
将校验信息绑定到ModelAndView http://www.voidcn.com/blog/983836259/article/p-5794496.html
集成Bean Validation 1.1(JSR-349)到SpringMVC https://my.oschina.net/qjx1208/blog/200946