springboot hibernate validator 参数校验详解

 

数据校验是任何一个应用程序都会用到的功能,无论是显示层还是持久层. 通常,相同的校验逻辑会分散在各个层中, 这样,不仅浪费了时间还会导致错误的发生(译注: 重复代码). 为了避免重复, 开发人员经常会把这些校验逻辑直接写在领域模型里面, 但是这样又把领域模型代码和校验代码混杂在了一起, 而这些校验逻辑更应该是描述领域模型的元数据.

JSR 303 - Bean Validation - 为实体验证定义了元数据模型和API. 默认的元数据模型是通过Annotations来描述的,但是也可以使用XML来重载或者扩展. Bean Validation API 并不局限于应用程序的某一层或者哪种编程模型, 例如,如图所示, Bean Validation 可以被用在任何一层, 或者是像类似Swing的富客户端程序中.

 

Hibernate Validator is the reference implementation of

this JSR. The implementation itself as well as the Bean Validation API and TCK are all provided and distributed under the Apache Software License 2.0

 

JSR提供的校验注解:         
@Null   被注释的元素必须为 null    
@NotNull    被注释的元素必须不为 null    
@AssertTrue     被注释的元素必须为 true    
@AssertFalse    被注释的元素必须为 false    
@Min(value)     被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@Max(value)     被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@Size(max=, min=)   被注释的元素的大小必须在指定的范围内    
@Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内    
@Past   被注释的元素必须是一个过去的日期    
@Future     被注释的元素必须是一个将来的日期    
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式    
 
 
Hibernate Validator提供的校验注解:  
@NotBlank(message =)   验证字符串非null,且长度必须大于0    
@Email  被注释的元素必须是电子邮箱地址    
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内    
@NotEmpty   被注释的字符串的必须非空    
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内
 

1.配置

spring-boot-starter-web包自动依赖hibernate-validator,不用再重复引入,直接开搞

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.3.1.Final</version>
</dependency>

声明一个bean注册到spring容器,这个bean是一个容器后处理器,会把校验的逻辑通过AOP织入有@Validated注解的class,具体可以看这个类的源码

这一步在springboot其实也不用做,ValidationAutoConfiguration这个配置类自动帮我们做了

 @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor(){
     return new MethodValidationPostProcessor();
    }

验证不通过会产生异常,因为我们项目提供rest接口,所以通过全局捕获异常,然后转换为json给前台


@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 用来处理bean validation异常
     * @param ex
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public  WebResult resolveConstraintViolationException(ConstraintViolationException ex){
        WebResult errorWebResult = new WebResult(WebResult.FAILED);
        Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
        if(!CollectionUtils.isEmpty(constraintViolations)){
            StringBuilder msgBuilder = new StringBuilder();
            for(ConstraintViolation constraintViolation :constraintViolations){
                msgBuilder.append(constraintViolation.getMessage()).append(",");
            }
            String errorMessage = msgBuilder.toString();
            if(errorMessage.length()>1){
                errorMessage = errorMessage.substring(0,errorMessage.length()-1);
            }
            errorWebResult.setInfo(errorMessage);
            return errorWebResult;
        }
        errorWebResult.setInfo(ex.getMessage());
        return errorWebResult;
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public WebResult resolveMethodArgumentNotValidException(MethodArgumentNotValidException ex){
        WebResult errorWebResult = new WebResult(WebResult.FAILED);
        List<ObjectError>  objectErrors = ex.getBindingResult().getAllErrors();
        if(!CollectionUtils.isEmpty(objectErrors)) {
            StringBuilder msgBuilder = new StringBuilder();
            for (ObjectError objectError : objectErrors) {
                msgBuilder.append(objectError.getDefaultMessage()).append(",");
            }
            String errorMessage = msgBuilder.toString();
            if (errorMessage.length() > 1) {
                errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
            }
            errorWebResult.setInfo(errorMessage);
            return errorWebResult;
        }
        errorWebResult.setInfo(ex.getMessage());
        return errorWebResult;
    }
}

这两个异常分别对应校验的两种使用方式

  1. 在方法里面校验
  2. 在bean对象里面校验
    经过测试,以上两种形式的数据验证不仅仅对controller层有用,在service层也行,只要这个类在spring ioc容器里面

 

可以自定义异常格式

package com.start.base.common.validator;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;


@Component
public class CommonValidator implements InitializingBean {

   private Validator validator;

   //实现校验方法并返回结果
   public ValidationResult validate(Object bean) {
       ValidationResult validationResult = new ValidationResult();
       Set<ConstraintViolation<Object>> constraintViolations = validator.validate(bean);
       if (constraintViolations.size() > 0) {
           //有错误
           validationResult.setHasError(true);
           Map<String,String> errMsgMap = new HashMap<>();
           constraintViolations.forEach(constraintViolation -> {
               String errMsg = constraintViolation.getMessage();
               String propertyName = constraintViolation.getPropertyPath().toString();
               errMsgMap.put(propertyName,errMsg);
               
           });
           validationResult.setErrMsgMap(errMsgMap);
       }
       return validationResult;
   }

   @Override
   public void afterPropertiesSet() throws Exception {
       //使hibernate validator通过工厂初始化进行实例化
       ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
       this.validator = factory.getValidator();
      
   }
}

package com.start.base.common.validator;

import java.util.HashMap;
import java.util.Map;

import org.springframework.util.StringUtils;

import com.alibaba.fastjson.JSONObject;

//import org.springframework.util.StringUtils;

public class ValidationResult {

    //判断校验是否有错
    private boolean hasError = false;

    //存放错误信息的map
    private Map<String,String> errMsgMap = new HashMap<>();

    //格式化字符串信息获得错误结果的msg方法
    public String getErrMsg(){
       
        return JSONObject.toJSONString(errMsgMap);
    }

    public void setHasError(boolean b) {
        // TODO Auto-generated method stub
        this.hasError=b;
    }

    public Object getErrMsgMap() {
        // TODO Auto-generated method stub
        return errMsgMap;
    }

    public void setErrMsgMap(Map<String, String> errMsgMap) {
         this.errMsgMap = errMsgMap;
    }
}

 

2.使用

2.1常用校验注解

@AssertFalse 校验false
@AssertTrue 校验true
@DecimalMax(value=,inclusive=) 小于等于value,
inclusive=true,是小于等于
@DecimalMin(value=,inclusive=) 与上类似
@Max(value=) 小于等于value
@Min(value=) 大于等于value
@NotNull  检查Null
@Past  检查日期
@Pattern(regex=,flag=)  正则
@Size(min=, max=)  字符串,集合,map限制大小
@Valid 对po实体类进行校验

这篇文章介绍的注解更全一点

2.2在方法参数上使用

@Controller
@Validated
public class ValidationController {

    @GetMapping("/validate1")
    @ResponseBody
    public String validate1(
            @Size(min = 1,max = 10,message = "姓名长度必须为1到10")@RequestParam("name") String name,
            @Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100") @RequestParam("age") Integer age,
            @Future @RequestParam("birth")@DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss") Date birth){
        return "validate1";
    }
}

注意类名需要加注解@Validated
校验失败会抛出ConstraintViolationException异常
然后我们在全局异常捕获类捕获这个异常,返回给前台对应的错误json

2.3在bean内属性上使用

给model类增加校验注解

public class User {

    @Size(min = 1,max = 10,message = "姓名长度必须为1到10")
    private String name;

    @NotEmpty
    private String firstName;

    @Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100")
    private Integer age;

    @Future
    @JSONField(format="yyyy-MM-dd HH:mm:ss")
    private Date birth;

    ...getter setter
}

在controller对应User实体前增加@Valid注解

@PostMapping("/validate2")
@ResponseBody
public User validate2(@Valid @RequestBody User user){
    return user;
}

message支持表达式和EL表达式 ,比如message = "姓名长度限制为{min}到{max} ${1+2}")

想把错误描述统一写到properties的话,在classpath下面新建ValidationMessages_zh_CN.properties文件(注意value需要转换为unicode编码),然后用{}格式的占位符

3.扩展

除了默认提供的校验注解外,我们可以定义自己的校验注解

3.1.创建约束注解类

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { HandsomeBoyValidator.class})
public @interface HandsomeBoy {

    String message() default "123";

    String name();

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default {};
}

注意:message用于显示错误信息这个字段是必须的,groups和payload也是必须的
@Constraint(validatedBy = { HandsomeBoyValidator.class})用来指定处理这个注解逻辑的类

一开始写了这个自定义注解和验证类,发现没有生效,最后发现是@Constraint这个注解里的类没有配置,还跟了很多源码,蛋疼,总的来讲,这个配置还是挺方便的

外国人写的一篇博客,介绍自定义验证配置,挺全的

https://www.baeldung.com/spring-mvc-custom-validator

3.2.创建验证器类

public class HandsomeBoyValidator implements ConstraintValidator<HandsomeBoy, User> {

    private String name;

    /**
     * 用于初始化注解上的值到这个validator
     * @param constraintAnnotation
     */
    @Override
    public void initialize(HandsomeBoy constraintAnnotation) {
        name =constraintAnnotation.name();
    }

    /**
     * 具体的校验逻辑
     * @param value
     * @param context
     * @return
     */
    @Override
    public boolean isValid(User value, ConstraintValidatorContext context) {
        return name ==null || name.equals(value.getName());
    }
}

这边的功能是user类里面的name字段必须和配置的一样,否则输出一个事实

3.3. 测试

@PostMapping("/validate3")
@ResponseBody
public User validate3(@Valid @HandsomeBoy(name = "scj",message = "666") @RequestBody  User user){
    return user;
}

如果验证不通过,会输出盛超杰第二帅,全局异常处理器不要忘记配置

3.4.创建自定义大小验证器类

public enum CaseMode {
    UPPER,
    LOWER;
}


@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {
    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    CaseMode value();
}


public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
    private CaseMode caseMode;
    public void initialize(CheckCase checkCase) {
        this.caseMode = checkCase.value();
    }

    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (s == null) {
            return true;
        }

        if (caseMode == CaseMode.UPPER) {
            return s.equals(s.toUpperCase());
        } else {
            return s.equals(s.toLowerCase());
        }
    }
}

 

3.5.创建自定义日期格式(yyyy-MM-dd HH:mm:ss)验证器类

package com.start.base.common.validator;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
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;

@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckDateValidator.class)
@Documented
public @interface CheckDate {
    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String date()  default "";
}

 

 

package com.start.base.common.validator;

import java.text.ParseException;
import java.text.SimpleDateFormat;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CheckDateValidator implements ConstraintValidator<CheckDate, String> {
    private String date;
    public void initialize(CheckDate checkDate) {
        this.date= checkDate.date();
    }

    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (s == null) {
            return false;
        }

       SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            sdf.parse(s);
            this.date = sdf.parse(s).toString();
            return true;
        } catch (ParseException e) {
            //e.printStackTrace();
            this.date = null;
            
        }
        return false;

    }
}

 

4.demo源码下载

https://github.com/hsn999/start-cloud/tree/master/base-common

 

参考

Hibernate validator官方文档 

https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#preface

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Spring Boot 中,可以使用 Hibernate Validator 来进行请求参数校验。具体步骤如下: 1. 引入 Hibernate Validator 依赖: ```xml <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.1.5.Final</version> </dependency> ``` 2. 在需要校验的请求参数实体类中添加校验注解,例如: ```java public class User { @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; @Email(message = "邮箱格式不正确") private String email; // 省略 getter 和 setter 方法 } ``` 3. 在请求处理方法中添加 @Validated 注解,并在参数上添加 @Valid 注解,例如: ```java @RestController @RequestMapping("/user") @Validated public class UserController { @PostMapping("/login") public Result login(@RequestBody @Valid User user) { // 处理登录逻辑 } } ``` 4. 当请求参数不符合校验规则时,会抛出 ConstraintViolationException 异常。可以在全局异常处理器中对该异常进行处理,例如: ```java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) public Result handleConstraintViolationException(ConstraintViolationException e) { List<String> errorMessages = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.toList()); return Result.error(String.join(",", errorMessages)); } } ``` 这样就可以自定义请求参数校验了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值