Hibernate-validator
是对validation-api
的再次封装,在开发过程中是经常使用到(spring boot里面的spring-boot-starter-web是默认应用了Hibernate-validator包的),特别是参数校验,刚开始做开发工作的时候,都是在Service层用if…else…来判断参数的合法性,这个会使代码显得很臃肿,后来接触Hibernate-validator
,真的是很好用的。这里分享出来,也是做个笔记,以后用到可以作为参考资料。
1. validation常用的注解和核心类
validation-api
中的注解比较全,Hibernate-validator
也有自己的一些注解,都是一些很好用的。
1.1 validation-api原生注解
@AssertTrue、@AssertFalse
:作用在布尔类型的属性上,校验其值是true或者false@DecimalMax、@DecimalMin
:支持BigDecimal、BigInteger等类型,限定其最大值和最小值,包含界限值@Min、@Max
:支持int、BigDecimal、BigInteger等类型,限定最大值和最小值,包含界限值@Digit
:作用在数字上,有两个参数,分别是integer和fraction,integer用来限定小数点前的位数,fraction用来限定小数点后的位数@Future
:作用在日期类型上,根据官网的说法是Date和Calendar,要求传入的时间是未来的时间@Null
:用来表示当前字段是否为null@Past
:此注解和Future是相反的,要求传入的时间是过去的时间@Size
:作用在Collection集合、Map集合以及CharSequence类型(String是CharSequence子类),注解内有两个参数,分别是min、max,作为长度的上限和下限,包含上下限值@Pattern
:通过正则表达式表达式校验传入参数是否符合要求
1.2 Hibernate-validator封装注解
@NotBlank
:这个注解其实不是validation包里面的,如果需要用它,需另外引入hibernate-validator包,用来限定属性不能为null和空串,一般都是用在String类型属性上,此注解会将传入的字符串的前面和后面的空清除,相当于执行了trim操作@NotEmpty
:限定属性不能为空,需要和@NotBlank
注解区分开,两者有所不同,此注解不会执行trim操作@Email
:用来校验传入的数据是否符合邮箱格式的@Range
:范围值,包含两个主要的两个属性min和max,这个相当于对validator-api
中@Min
和@Max
的再次封装,将两个注解合并为一个注解,包含边界值
1.3 其他说明
Hibernate-validator
的注解不止这么多,还有如@SafeHtml
、@ScriptAssert
等,这些在日常很少用,就不多去赘述了,感兴趣的可以研究一下,其实从字面上也能理解一二。
1.4 核心类
上面的注解校验结束后,会将结果封装到这个核心类里面,通过这个类提供的API获取是否有校验不通过的字段信息。核心类的名称是BindingResult
,这个类是Spring封装的,在spring-context
包内。
主要使用的两个API:
hasErrors()
:返回值为boolean类型,true
表示由校验未通过的属性getFieldError().getDefaultMessage()
:获取错误信息,也就是校验上面的注解中message
属性对应的值
2. 对象属性的校验
2.1 简单属性校验
对一个类中的几个非集合,非子对象的属性校验,这些校验是最常见的,也是最简单的。
- 校验对象
@Data
public class ValidDemo {
@Size(min = 2, max = 5, message = "长度不符合要求")
private String name;
@Min(value = 10,message = "不合法年龄")
private Integer age;
}
- 控制层方法的书写方式
@PostMapping("/post")
@ResponseBody
public String valid1(@RequestBody @Valid ValidDemo validDemo, BindingResult bindingResult) {
return bindingResult.hasErrors() ? bindingResult.getFieldError().getDefaultMessage() : "success";
}
需要注意点
- 控制层方法中,要在需要校验的对象上加上
@Valid
注解,否则被校验对象里面的相关校验注解都是不能生效的 - 控制层方法中,需要添加一个参数
BindingResult
,用来接收校验的结果,给出前端友好的响应 - 这里的简单类型主要指的是基本类型、基本类型包装类和String
- 另外最需要注意的点就是被校验对象,它不是基本类型、基本类型包装类和String,具体原因后面会继续说到
2.2 复杂属性校验
所谓的复杂类型,就是对象里面的属性不是简单的基本类型、基本类型包装类和String,而是包含Collection集合、嵌套对象等。
- 校验对象
@Data
public class ValidDemo {
@Size(min = 2, max = 5, message = "长度不符合要求")
private String name;
@Min(value = 10, message = "不合法年龄")
private Integer age;
@NotNull(message = "用户列表不能为空")
@Valid
private List<User> users;
@NotNull(message = "订单信息不能为空")
@Valid
private Order order;
}
//内嵌的用户对象
@Data
public class User {
@NotBlank(message = "地址不能为空")
private String addr;
@Email(message = "邮箱格式不正确")
private String email;
}
//内嵌的订单对象
@Data
public class Order {
@DecimalMin(value = "1.00", message = "价格不正确")
private BigDecimal price;
}
- 控制层方法
@GetMapping("/get")
@ResponseBody
public String valid1(@ModelAttribute @Valid ValidDemo validDemo, BindingResult bindingResult) {
return bindingResult.hasErrors() ? bindingResult.getFieldError().getDefaultMessage() : "success";
}
需要注意点
- 复杂对象,里面嵌套Collection集合(List集合),必须在集合上加
@Valid
注解,否则集合内的对象(User)中属性加的注解不会生效 - 同上理里面嵌套的对象(Order对象)上也是需要加上
@Valid
注解 - 控制层方法,这里使用了GET请求,接收参数没有使用字段直接接收,而是使用对象,需要在对象上加
@ModelAttribute
注解,将GET请求的字段封装到此对象中,为什么这么做而不用单子段接收?下面说具体原因
2.3 GET和POST请求的区别
上面的两个例子说完了,在注意点上都留下了一个问题。为什么控制层都使用对象来接收参数,POST请求用对象接收是可以理解的,而GET请求更常用的使用字段来接收,但是为什么不用。
看个例子:
@GetMapping("/error")
@ResponseBody
public String error(@RequestParam("name") @Size(min = 2, max = 5, message = "名字长度不合法") String name) {
return "success";
}
控制层这么写,调试可以得到,这样写@Size
是不会生效的。
但是又有人会觉得应该加上@Valid
注解,但是事实依然不会生效。(想验证的可以尝试一下)
如果你必须用字段来接收GET请求的参数,且需要使用validation
的校验,接下来给你一个不建议的校验方式。
2.4 不建议的校验方式
描述不多做赘述,直接上代码啦!
- 第一步:在Spring容器中添加一个Bean:
MethodValidationPostProcessor
。
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
- 第二步:在需要校验的方法所在类上添加
@Validated
注解,注意是类上面,不是方法上。
@Validated
public class ValidatorController {}
- 第三步:在方法上加上校验注解
@GetMapping("/error")
@ResponseBody
public String error(@RequestParam("name") @Size(min = 2, max = 5, message = "名字长度不合法") String name) {
return "success";
}
OK,到此就完成了,当校验不通过的时候,会给出以下信息:
{
"timestamp": 1556769001783,
"status": 500,
"error": "Internal Server Error",
"exception": "javax.validation.ConstraintViolationException",
"message": "No message available",
"path": "/valid/error"
}
到这里多少能看出来一点问题了,下面来罗列一下:
- 响应信息不是给出注解里面已经预设好的”名字长度不合法”,而是一串异常信息,这个有点尴尬,这个直接响应给前端,会不会被骂死,你看着办吧。(也许你可以用切面去处理,但是不是有点太麻烦)
- 正常一个控制层的Controller里面都会有很多方法,其他方法(如POST)是用对象来接收的参数的,用不到这个注解,直接使用
@Valid
就可以。但是不好意思,@Validated
注解是写在方法上的,作用于全局,会导致@Valid
不起作用,也就是说当前这个Controller所有的方法返回的都是上面的异常信息。为了兼容一个GET接收字段参数,把整个方法的校验规则都打乱了,是不是有点得不偿失。因此这种方式的校验是不推荐的,在上面复杂校验介绍里面其实已经给出解决方案了,不管是GET、POST还是其他类型的请求,都统一用对象接收。另外用对象接收是有好处的,比如一个GET查询有很多查询条件,如果都写在方法上会让整个方法很长,如果在传参的过程中,不小心把参数顺序弄错了,那就尴尬啦。用对象封装接收更科学更方便,个人觉得。
3. 自定义校验规则
上面的校验规则都达不业务校验的要求,我需要有一个自定义的校验规则,那也是可以的,接下来说一下自定义校验规则的写法。
- 自定义注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = MapValidConstraint.class)
public @interface MapValidNull {
int value() default -1;
String message() default "Map集合不能为空";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List {
MapValidNull[] value();
}
}
- 注解处理类
// 泛型的第一个是对应的注解,第二个是字段属性类型
@Component
public class MapValidConstraint implements ConstraintValidator<MapValidNull, Map> {
private int value;
@Override
public void initialize(MapValidNull constraintAnnotation) {
this.value = constraintAnnotation.value();
}
@Override
public boolean isValid(Map map, ConstraintValidatorContext context) {
//TODO 具体判断逻辑……
return true;
}
}
- 然后及时使用注解,和预定义的注解使用方式是一样的。
@MapValidNull
private Map<String,String> map;
4. 业务校验工具
直接在Controller中写检查注解+BindingResult
基本就能满足日常开发需求,但是还有一种情况下是满足不了的,就是非HTTP接口,在使用Dubbo或者其他RPC调用的服务就不能使用这种方式,需要另外的在业务代码中进行校验。这个时候就无法使用BindingResult
来直接获取校验结果。怎么办呢?
在Hibernate-validator
里面可以手动的执行校验,使用到的工具类是Validator
。简单看一下这个类的源码如下:
public interface Validator {
//直接检查整个类中的所有字段
<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);
//指定检查类中某个字段
<T> Set<ConstraintViolation<T>> validateProperty(T object,
String propertyName,
Class<?>... groups);
<T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType,
String propertyName,
Object value,
Class<?>... groups);
BeanDescriptor getConstraintsForClass(Class<?> clazz);
<T> T unwrap(Class<T> type);
ExecutableValidator forExecutables();
}
使用到的是前两个方法,一般使用到第一个就足够啦。下面看一下检查的具体逻辑。
首先需要检查的字段上需要加上注解,可以是
Hibernate-validator
里面自带的注解,也可以是自定义注解在业务代码中获取
Validator
实例,调用validate
方法,将类作为参数传入进去,如下代码://构建Validator实例 ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); //调用校验方法 Article article = new Article(); Set<ConstraintViolation<T>> validates = validator.validate(artilce); //调用校验方法 if(CollectionUtils.isEmpty(validates)) System.out.println("全部检查通过"); return validates.iterator().next().getMessage(); //取第一个错误信息返回
如果只是校验类中的某个字段,调用
validateProperty
方法,传入需要检查的对象和需要检查的字段名称,如下代码://构建Validator实例 ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); //调用校验方法 Article article = new Article(); Set<ConstraintViolation<T>> validates = validator.validate(artilce,"title"); //调用校验方法,只检查title字段 if(CollectionUtils.isEmpty(validates)) System.out.println("检查通过"); return validates.iterator().next().getMessage(); //取第一个错误信息返回
这种检查方式还是会经常用到的,每次都去获取Validator
实例以及判断检查结果,代码会比较臃肿,所以这里可以封装一个工具类。(可以根据实际的项目对这个工具类进行修改)
public class ValidatorUtil {
public final static String VALIDATE_PASS = "VALIDATE_PASS";
private static Validator validator;
static {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
}
/**
* 校验对象内所有的字段
*
* @param validateBean 被检查的对象
* @param <T> 对象类型
* @return 成功返回固定信息:VALIDATE_PASS,失败默认返回第一个异常信息
*/
public static <T> String validate(T validateBean) {
Set<ConstraintViolation<T>> validates = validator.validate(validateBean);
if (CollectionUtils.isEmpty(validates)) return VALIDATE_PASS;
return validates.iterator().next().getMessage();
}
/**
* 校验对象内的指定字段
*
* @param validateBean 被检查的对象
* @param propertyName 指定字段名称
* @param <T> 对象类型
* @return 成功返回固定信息:VALIDATE_PASS,失败默认返回第一个异常信息
*/
public static <T> String validateProperty(T validateBean, String propertyName) {
Set<ConstraintViolation<T>> validates = validator.validateProperty(validateBean, propertyName);
if (CollectionUtils.isEmpty(validates)) return VALIDATE_PASS;
return validates.iterator().next().getMessage();
}
}
5. 总结
到这里关于Hibernate-validator
的使用就写完啦,主要说的是下面几点内容。
- 介绍
Hibernate-validator
和validation-api
中已经定义好的检查注解,以及这些注解的基本使用规则 - 基于HTTP接口的参数校验,GET、POST请求都有涉及到,并且特殊说明接收参数是对象还是单个字段存在的区别
- 说明为什么不用单个字段接收参数的问题
- 自定义检查注解,实现个性化的检查规则
- 基于非HTTP接口服务,实现用工具类在业务代码中完成参数校验