( 五 )、 SpringBoot 中优雅的参数效验 hibernate-validator
( 五 )、 SpringBoot 中优雅的参数效验 hibernate-validator
官网: http://hibernate.org/validator/documentation/
1、Maven依赖:
如果是Springboot1.x spring-boot-starter-web
包里面有 hibernate-validator
包,不需要引用hibernate validator依赖。在 pom.xml
中添加上 spring-boot-starter-web
的依赖即可
如果是Springboot2.x 需要单独引入maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
JSR 303 是Bean验证的规范 ,Hibernate Validator 是该规范的参考实现,它除了实现规范要求的注解外,还额外实现了一些注解。
@Null | 被注释的元素必须为 null |
@NotNull | 限制必须不为null |
@NotEmpty | 验证注解的元素值不为 null 且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在 min 到 max 之间(也可以用在集合上) |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 | |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@AssertFalse | 限制必须为false (很少用) |
@AssertTrue | 限制必须为true (很少用) |
@Past | 限制必须是一个过去的日期 |
@Future | 限制必须是一个将来的日期 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过 integer,小数部分的位数不能超过 fraction (很少用) |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@Size | 被注释的list、map在指定的大小范围 |
@Range | 被注释的元素必须在合适的范围内 |
@URL | 被注释的元素必须是有效URL |
@Negative | 负数 |
@NegativeOrZero | 0或者负数 |
@Positive | 整数 |
@PositiveOrZero | 0或者整数 |
@Valid | 递归的对关联的对象进行校验(对象内部包含另一个对象作为属性,属性上加@Valid,可以验证作为属性的对象内部的验证) |
2、参数校验模式:
Hibernate Validator有以下两种验证模式:
- 普通模式(默认是这个模式), 效验完所有的字段然后返回失败。
- 快速失败返回模式。
快速失败返回模式配置:
@Configuration
public class ValidatorConfiguration {
/**
* failFast(true)快速返回失败
*
* @return
*/
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
// .addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
3、参数验证异常时全局异常处理
验证不通过时,抛出了ConstraintViolationException、BindException、MethodArgumentNotValidException异常,使用全局统一捕获异常处理:
/**
* @Author dw
* @ClassName GlobalExceptionHandler
* @Description 全局异常处理
* @Date 2022/4/14 22:49
* @Version 1.0
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* post请求参数校验抛出的异常
*
* @param ex
* @return
*/
@ExceptionHandler({BindException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class})
public String resolveMethodArgumentNotValidException(Exception ex) {
return handlerNotValidException(ex);
}
/**
* 封装参数校验异常
*
* @param e 异常
* @return 封装参数校验异常
*/
private String handlerNotValidException(Exception e) {
BindingResult bindingResult = null;
// 存储字段校验异常内容
Set<ConstraintViolation<?>> constraintViolations = new HashSet<>();
if (e instanceof BindException) {
BindException exception = (BindException) e;
bindingResult = exception.getBindingResult();
} else if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
bindingResult = exception.getBindingResult();
} else if (e instanceof ConstraintViolationException) {
ConstraintViolationException exception = (ConstraintViolationException) e;
constraintViolations = exception.getConstraintViolations();
}
//通过字段校验后存储校验结果
Map<String, Object> maps;
if (Objects.nonNull(bindingResult) && bindingResult.hasErrors()) {
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
maps = new HashMap<>(fieldErrors.size());
fieldErrors.forEach(error -> maps.put(error.getField(), error.getDefaultMessage()));
} else if (!CollectionUtils.isEmpty(constraintViolations)) {
maps = new HashMap<>(constraintViolations.size());
constraintViolations.forEach(error -> {
if (error instanceof ConstraintViolationImpl) {
ConstraintViolationImpl<?> constraintViolation = (ConstraintViolationImpl<?>) error;
maps.put(constraintViolation.getPropertyPath().toString(), constraintViolation.getMessage());
}
});
} else {
maps = Collections.emptyMap();
}
//返回结果
return maps.toString();
}
}
4、参数验证示例
4.1、验证简单对象
@Data
public class Product {
@NotNull(message = "产品名称不能为空")
private String productName;
@NotBlank(message = "产品描述不能为空")
private String productDes;
@Range(min = 1, max = 100)
@NotNull
private Integer price;
}
Controller
@RestController
public class TestValidateController {
@PostMapping("validaObject")
public String validaObject(@RequestBody @Valid Product product) {
return "验证成功!";
}
}
测试结果:
4.2、验证单个参数get请求
@RestController
@Validated
public class TestValidateGetController {
@GetMapping("getTest")
public String validaObject(@NotNull(message = "产品名字不能为空!") String productName) {
return "验证成功!";
}
}
测试:
4.3、@PathVariable 方式的参数验证
@RestController
@Validated
public class TestValidateGetController {
@GetMapping("getTest/{price}")
public String validaObject(@PathVariable @Range(min = 1, max = 100) Integer price) {
return "验证成功!";
}
}
测试:
4.4、验证嵌套对象
@Data
public class Product {
@NotNull(message = "产品名称不能为空")
private String productName;
@NotBlank(message = "产品描述不能为空")
private String productDes;
@Range(min = 1, max = 100)
@NotNull
private Integer price;
@NotNull(message = "产品类别不能为空!")
@Valid
private Category category;
}
Category:
public class Category {
@NotNull
private String categoryName;
@NotBlank
private String categoryCode;
}
Controller
@RestController
public class TestValidatePostController {
@PostMapping("validaObject")
public String validaObject(@RequestBody @Valid Product product) {
return "验证成功!";
}
}
测试1:
测试2:
4.5、验证集合
注意验证集合类上必须添加 @Validated 才行。
@RestController
@Validated
public class TestValidatePostController {
@PostMapping("validaList")
public String validaList(@RequestBody @Valid List<Product> products) {
return "验证成功!";
}
}
测试:
5、分组校验:
比如:有这样一种场景,新增用户信息的时候,不需要验证userId(因为系统生成);修改的时候需要验证userId,这时候可用用户到validator的分组验证功能。
定义两个接口groupA、groupB
public interface GroupA {
}
public interface GroupB {
}
验证model:Person
@Data
public class Person {
@NotBlank
@Range(min = 1,max = Integer.MAX_VALUE,message = "必须大于0",groups = {GroupA.class})
/**用户id*/
private Integer userId;
@NotBlank
@Length(min = 4,max = 20,message = "必须在[4,20]",groups = {GroupB.class})
/**用户名*/
private String userName;
@NotBlank
@Range(min = 0,max = 100,message = "年龄必须在[0,100]",groups={Default.class})
/**年龄*/
private Integer age;
@Range(min = 0,max = 2,message = "性别必须在[0,2]",groups = {GroupB.class})
/**性别 0:未知;1:男;2:女*/
private Integer sex;
}
如上Person所示,3个分组分别验证字段如下:
- GroupA验证字段userId;
- GroupB验证字段userName、sex;
- Default验证字段age(Default是Validator自带的默认分组)
controller验证:
@RequestMapping("/demo6")
public void demo6(@Validated({GroupA.class, GroupB.class}) Person p, BindingResult result){
if(result.hasErrors()){
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError error : allErrors) {
System.out.println(error);
}
}
}
6、自定义校验注解
- 注解上必须有 @Constraint(validatedBy = {******.class}) 注解标注,validateBy 的值就是校验逻辑的实现类,实现类必须实现接口 ConstraintValidator
- 自定义注解 必须包含 message ,groups,payload 属性。
1、创建约束注解类
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { MyValidator.class})
public @interface MyVolidate {
String message() default "默认的错误信息";
String name();
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default {};
}
@Constraint(validatedBy = { MyValidator.class})用来指定处理这个注解逻辑的类
2.创建验证器类
public class MyValidator implements ConstraintValidator<MyVolidate, User> {
private String name;
/**
* 用于初始化注解上的值到这个validator
* @param constraintAnnotation
*/
@Override
public void initialize(MyVolidate constraintAnnotation) {
name =constraintAnnotation.name();
}
/**
* 具体的校验逻辑
* @param value
* @param context
* @return
*/
@Override
public boolean isValid(User value, ConstraintValidatorContext context) {
return name ==null || name.equals(value.getName());
}
}
3. 测试
@PostMapping("/validateTest")
@ResponseBody
public User validateTest(@Valid @MyVolidate(name = "成都",message = "如果效验不通过,显示我") @RequestBody User user){
return user;
}
7、校验工具类
/**
* @Author dw
* @ClassName ValidationUtil
* @Description 验证工具类
* @Date 2022/4/14 22:31
* @Version 1.0
*/
public class ValidationUtil {
/**
* 开启快速结束模式 failFast (true)
*/
private static Validator validator = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory()
.getValidator();
/**
* 校验对象
*
* @param t bean
* @param groups 校验组
* @return ValidResult
*/
public static <T> ValidResult validateBean(T t, Class<?>... groups) {
ValidResult result = new ValidationUtil().new ValidResult();
Set<ConstraintViolation<T>> violationSet = validator.validate(t, groups);
boolean hasError = violationSet != null && violationSet.size() > 0;
result.setHasErrors(hasError);
if (hasError) {
for (ConstraintViolation<T> violation : violationSet) {
result.addError(violation.getPropertyPath().toString(), violation.getMessage());
}
}
return result;
}
/**
* 校验bean的某一个属性
*
* @param obj bean
* @param propertyName 属性名称
* @return ValidResult
*/
public static <T> ValidResult validateProperty(T obj, String propertyName) {
ValidResult result = new ValidationUtil().new ValidResult();
Set<ConstraintViolation<T>> violationSet = validator.validateProperty(obj, propertyName);
boolean hasError = violationSet != null && violationSet.size() > 0;
result.setHasErrors(hasError);
if (hasError) {
for (ConstraintViolation<T> violation : violationSet) {
result.addError(propertyName, violation.getMessage());
}
}
return result;
}
/**
* 校验结果类
*/
@Data
public class ValidResult {
/**
* 是否有错误
*/
private boolean hasErrors;
/**
* 错误信息
*/
private List<ErrorMessage> errors;
public ValidResult() {
this.errors = new ArrayList<>();
}
public boolean hasErrors() {
return hasErrors;
}
public void setHasErrors(boolean hasErrors) {
this.hasErrors = hasErrors;
}
/**
* 获取所有验证信息
*
* @return 集合形式
*/
public List<ErrorMessage> getAllErrors() {
return errors;
}
/**
* 获取所有验证信息
*
* @return 字符串形式
*/
public String getErrors() {
StringBuilder sb = new StringBuilder();
for (ErrorMessage error : errors) {
sb.append(error.getPropertyPath()).append(":").append(error.getMessage()).append(" ");
}
return sb.toString();
}
public void addError(String propertyName, String message) {
this.errors.add(new ErrorMessage(propertyName, message));
}
}
@Data
public class ErrorMessage {
private String propertyPath;
private String message;
public ErrorMessage() {
}
public ErrorMessage(String propertyPath, String message) {
this.propertyPath = propertyPath;
this.message = message;
}
}
public static void main(String[] args) {
Product product = new Product();
product.setProductName("测试!!!");
ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(product);
if(validResult.hasErrors()){
String errors = validResult.getErrors();
System.out.println(errors);
}
}
}
/**
* @Author dw
* @ClassName EnumValueValidator
* @Description 自定义枚举值效验
* @Date 2022/4/20 23:40
* @Version 1.0
*/
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValue.Validator.class)
public @interface EnumValue {
String message() default "{EnumValueValidator's value is invalid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> enumClass();
class Validator implements ConstraintValidator<EnumValue, Object> {
private Class<? extends Enum<?>> enumClass;
/**
* 枚举中效验是否合法的方法名字
*/
private static final String ENUM_METHOD_NAME="isValidValue";
@Override
public void initialize(EnumValue enumValue) {
enumClass=enumValue.enumClass();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) {
return Boolean.FALSE;
}
Class<?> valueClass = value.getClass();
try {
Method method = enumClass.getMethod(ENUM_METHOD_NAME, valueClass);
// 返回结果必须是boolean
if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) {
return Boolean.FALSE;
}
// 必须是静态方法
if(!Modifier.isStatic(method.getModifiers())) {
return Boolean.FALSE;
}
// 通过反射调度枚举中的方法进行验证
Boolean result = (Boolean)method.invoke(null, value);
return result == null ? false : result;
} catch (Exception e) {
e.printStackTrace();
return Boolean.FALSE;
}
}
}
}
8.2、新建一个枚举测试
@Getter
@AllArgsConstructor
public enum ProductTypeEnum {
BOOK(1, "书籍"),
CAR(2, "汽车"),
FOOD(3, "食品");
private Integer key;
private String value;
public static boolean isValidValue(Integer val) {
for (ProductTypeEnum value : values()) {
if (value.getKey().equals(val)) {
return true;
}
}
return false;
}
}
8.3、实体类中使用自定义注解
@Data
public class Product {
@NotNull(message = "产品名称不能为空")
private String productName;
@NotBlank(message = "产品描述不能为空")
private String productDes;
@Range(min = 1, max = 100)
@NotNull
private Integer price;
@NotNull(message = "产品类别不能为空!")
@Valid
private Category category;
@EnumValue(enumClass = ProductTypeEnum.class, message = "产品类型枚举值不合法!")
private Integer projectType;
}
8.4 、新建Controller测试
@RestController
@Validated
public class TestValidateEnumController {
@PostMapping("validaEnum")
public String validaObject(@RequestBody @Valid Product product) {
return "验证成功!";
}
}
8.5、测试结果
9、@ScriptAssert 注解实现参数个性化校验
@ScriptAssert() 主要用于类注释
9.1、以用户注册为例。在用户注册时我们要校验输入密码与确认密码是否一致,和用户名中是否包含特殊字符:
@ScriptAssert(script = "_this.password.equals(_this.rePassword)",lang = "javascript",message = "确认密码与输入密码不一致")
上面是校验对象中password属性与rePassword属性是否相等,equals返回true表示校验通过,返回false表示校验不通过(会返回错误信息)。_this是默认的对象别名,lang="javascript" 表示用java 执行script中的脚本。
9.2、script指定特定的静态方法校验用户名是否包含特殊字符:
@ScriptAssert(script = "com.dw.study.utils.UserValidate.checkUserName(_this.userName)",lang = "javascript",
message = "用户名不能包含! @ # $ % & * \\ / ? ?特殊符号")
public class UserValidate {public static boolean checkUserName(String userName){
String[] chars = {"!","@","#","$","%","&","*","\\","/","?","?"};
for(String c : chars){
if(userName.contains(c)){
return false;
}
}
return true;
}
}
注意script中方法是一个静态的方法,且包含完整的调用路径。校验参数时,会调用CheckUserName这个静态方法,方法参数为这个对象的userName属性。
多个 @ScriptAssert 需要使用 @ScriptAssert.List :
@ScriptAssert.List({
@ScriptAssert(script = "com.wl.account.dto.RegisterUserDto.checkUserName(_this.userName)",lang = "javascript",
message = "用户名不能包含! @ # $ % & * \\ / ? ?特殊符号"),
@ScriptAssert(script = "_this.password.equals(_this.rePassword)",lang = "javascript",message = "确认密码与输入密码不一致")
})
@Data
public class RegisterUserDto {
@NotBlank(message = "用户名不能为空")
@Length(message = "用户名长度应在3-12位",max = 12,min = 3)
private String userName;
@NotBlank(message = "密码不能为空")
@Length(message = "密码长度应在6-15位",max = 15,min = 6)
private String password;
@NotBlank(message = "确认密码不能为空")
private String rePassword;
private String mobile;
@Email
private String email;
public static boolean checkUserName(String userName){
String[] chars = {"!","@","#","$","%","&","*","\\","/","?","?"};
for(String c : chars){
if(userName.contains(c)){
return false;
}
}
return true;
}
}