hibernate.validator校验

为什么要使用hibernate.validator?

我们在编写接口的时候,会有各种各样形式的入参。我们不可能不做任何校验就直接进入业务代码进行程序运行,因为我们都会有个基础数据验证机制。比如入参是否为空,是否为数字,是否为正确的日期格式等等。这种基础的数据验证,代码并不复杂,但是要每个业务类都去写,工作量就翻倍加了,而且每个接口都写也是不符合常理的编码。而数据验证又分为两种,一种是无业务关联的规则性验证,一种是根据现有数据进行联动性数据验证(就是参数的合理性需要去查验数据库数据)。而hibernate.validator则适合做无业务关联的规则性验证,而这类验证的代码大多是可复用的。

如果项目框架是spring boot的话,在spring-boot-starter-web 中已经包含了Hibernate-validator的依赖。Hibernate-Validator的主要使用的方式就是注解的形式,并且是“零配置”的,无需配置也可以使用。

1、Hibernate-Validator 最基本的使用

(1)添加一个普通的接口信息,参数是@RequestParam类型的,传入的参数是id,且id不能小于10。

    @RestController
    @RequestMapping("/example")
    @Validated
    public class ExampleController {
    
        /**
         *  用于测试
         * @param id id数不能小于10 @RequestParam类型的参数需要在Controller上增加@Validated
         * @return
         */
        @RequestMapping(value = "/info",method = RequestMethod.GET)
        public String test(@Min(value = 10, message = "id最小只能是10") @RequestParam("id")
                                       Integer id){
            return "恭喜你拿到参数了";
        }
    }

(2)在全局异常拦截中添加验证异常的处理

/**
 * 统一异常处理类
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
        StringBuilder errorInfo = new StringBuilder();
        BindingResult bindingResult = exception.getBindingResult();
        for(int i = 0; i < bindingResult.getFieldErrors().size(); i++){
            if(i > 0){
                errorInfo.append(",");
            }
            FieldError fieldError = bindingResult.getFieldErrors().get(i);
            errorInfo.append(fieldError.getField()).append(" :").append(fieldError.getDefaultMessage());
        }

        //返回BaseResponse
        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg(errorInfo.toString());
        response.setCode(DefaultErrorCode.error);
        return response;
    }


    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> handleConstraintViolationException(ConstraintViolationException exception) {
        StringBuilder errorInfo = new StringBuilder();
        String errorMessage ;

        Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
        for (ConstraintViolation<?> item : violations) {
            errorInfo.append(item.getMessage()).append(",");
        }
        errorMessage = errorInfo.toString().substring(0, errorInfo.toString().length()-1);

        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg(errorMessage);
        response.setCode(DefaultErrorCode.error);
        return response;
    }



    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> handleDefaultException(Exception exception) {

        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg("其他错误");
        response.setCode(DefaultErrorCode.error);
        return response;
    }
}


2、验证复杂参数的案例

(1)添加一个vo的实体信息。

@Data
@ApiModel("用户添加对象")
public class UserAddDto {

    @NotBlank(message = "用户id不能为空")
    @ApiModelProperty(name = "id", notes = "用户id",example = "2441634686")
    private String id;


    @NotBlank(message = "用户名称不能为空")
    @Size(min = 1,max = 20,message = "名称必须在1-20个字符内") //如果只有@Size,name可为空
    @ApiModelProperty(name = "name", notes = "用户名称",example = "张飞")
    private String name;

    @NotNull(message = "开始时间不能为空")
    @ApiModelProperty(name = "startDate", notes = "开始时间",example = "2018-08-08")
    private String startDate;

    @Size(max = 12,message = "不能超过12个")
    @ApiModelProperty(name = "list", notes = "包含的方向列表")
    @Valid //级联校验
    private List<DirectionDto> list;

    @EnumCheck(enumClass = SexEnum.class,message = "用户类型不合法")
    @ApiModelProperty(name = "id", notes = "用户类型",example = "2")
    private Integer type;

}
@Data
@ApiModel("方向对象")
public class DirectionDto {

    @NotBlank(message = "方向id不能为空")
    @ApiModelProperty(name = "list", notes = "方向唯一标识",example = "45587845465")
    private String id;

    @NotBlank(message = "开始日期不能为空")
    @ApiModelProperty(name = "startDate", notes = "开始时间",example = "2019-01-08")
    @Length(max = 8,message = "开始日期不合法")
    private String startDate;

    @ApiModelProperty(name = "endDate", notes = "结束时间",example = "2019-01-14")
    private String endDate;
}

(2)添加一个POST请求的接口。   

@PostMapping(value = "/add")
    @ApiOperation(value = "添加用户",notes = "添加用户,方向可以为空")
    public BaseResponse add(@Valid @RequestBody UserAddDto addDto) {

        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg("添加成功");
        return response;
    }

 以下是自行封装异常信息的写法:

	@PutMapping
    public User addUser(@RequestBody @Valid UserAddDto userAddDto, BindingResult errors, HttpServletResponse response) {
        if (errors.hasErrors()) {
            errors.getAllErrors().forEach(error -> System.out.println(((FieldError) error).getField() + "" + error.getDefaultMessage()));
            response.setStatus(400);
            return userAddDto;
        }
        return iUserService.addUser(userAddDto);
    }

此时如果调用此接口的客户端、没有传id或name的值,那么请求会被hibernate validator拦截,并且将异常信息封装到BindingResult 对象中,我们在这里可以将异常信息封装到Exception类中返回至客户端。

(3)校验日期必须在今天之前。方式同非空校验一样

实体类中添加@Pass注解

	@Past
    private Date birthday;

接口中添加@Valid注解

	@PutMapping
    public User addUser(@RequestBody @Valid User user, BindingResult errors, HttpServletResponse response) {
        if (errors.hasErrors()) {
            errors.getAllErrors().forEach(error -> System.out.println(((FieldError) error).getField() + "" + error.getDefaultMessage()));
            response.setStatus(400);
            return user;
        }
        return iUserService.addUser(user);
    }

3、常用的校验注解

注解使用
@NotNull值不能为空;被注释的元素(任何元素)必须不为 null, 集合为空也是可以的。
@Null值必须为空
@Pattern(regex=)字符串必须匹配正则表达式;即正则表达式匹配,可用来校验年月日格式,是否包含特殊字符(regexp = "^[a-zA-Z0-9\u4e00-\u9fa5
@Size(min=, max=)集合的元素数量必须在min和max之间;指定的字符串、集合、map、数组长度必须在指定的max和min内允许元素为null,字符串允许为空格
@CreditCardNumber(ignoreNonDigitCharacters=)字符串必须是信用卡号(按照美国的标准校验!!)
@Email字符串必须是Email地址
@Length(min=, max=)检查字符串的长度;长度必须在指定的max和min内 允许元素为null
@NotBlank字符串必须有字符;只用来校验字符串不能为null,空格也是被允许的 。校验字符串推荐使用@NotEmpty
@NotEmpty字符串不为null,集合有元素;用来校验字符串、集合、map、数组不能为null或空(字符串传入空格也不可以)(集合需至少包含一个元素)
@Range(min=, max=)数字必须大于等于min, 小于等于max;用来校验数字或字符串的大小必须在指定的min和max内字符串会转成数字进行比较,如果不是数字校验不通过允许元素为null
@SafeHtml字符串是安全的html
@URL字符串是合法的URL
@AssertFalse值必须是false
@AssertTrue值必须是true
@DecimalMax(value=, inclusive=)值必须小于等于(inclusive=true)/小于(inclusive=false)value属性指定的值。可以注解在字符串类型的属性上
@DecimalMin(value=, inclusive=)值必须大于等于(inclusive=true)/大于(inclusive=false)value属性指定的值。可以注解在字符串类型的属性上
@Digits(integer=, fraction=)数字格式检查。Integer指定整数部分的最大长度,fraction指定小数部分的最大长度
@Future值必须是未来的日期
@Past值必须是过去的日期
@Max(value=)值必须小于等于value指定的值。不能注解在字符串类型的属性上;不支持小数即double和float,允许元素为null
@Min(value=)值必须大于等于value指定的值。不能注解在字符串类型的属性上;不支持小数即double和float,允许元素为null
@Email被注释的元素必须是电子邮箱地址

除了@Empty要求字符串不能全是空格,其他的字符串校验都是允许空格的。
message是可以引用常量的,但是如@Size里max不允许引用对象常量,基本类型常量是可以的。
注意大部分规则校验都是允许参数为null,即当不存在这个值时,就不进行校验了
4、自定义注解

这些注解能适应我们绝大多数的验证场景,但是为了应对更多的可能性,我们需要增加注解功能配合Hibernate-Validator的其他的特性,来满足验证的需求。

在新增用户的时候、我们可能需要校验用户名是否重复,如果重复则提示更换用户名。这个也可以使用自定义validator注解实现。

  • 创建注解类
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotNull;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@NotNull
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UserConstraintValidator.class)
public @interface UsernameNotRepeat {

    String message();

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

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

@NotNull:代表被注解的字段不能为空
@Target({ElementType.METHOD, ElementType.FIELD}):此注解适用于方法和字段
@Retention(RetentionPolicy.RUNTIME):注解的生命周期,注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Constraint(validatedBy = UserConstraintValidator.class):实际业务执行类,在这里需要校验用户名是否重复

  • 创建业务执行类
import com.mright.security.demo.platform.entity.User;
import com.mright.security.demo.platform.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class UserConstraintValidator implements ConstraintValidator<UsernameNotRepeat, String> {

    @Autowired
    private IUserService iUserService;

    @Override
    public void initialize(UsernameNotRepeat userConstraint) {

    }

    @Override
    public boolean isValid(String username, ConstraintValidatorContext constraintValidatorContext) {
        User user = iUserService.getUserByUsername(username);
        return user == null;
    }

}

此处必须实现ConstraintValidator接口,代表属于validator的子类,可用于注解校验。可以直接使用@Autowired注入我们的service执行查询等业务操作,最终返回用户名是否已经存在。

  • 创建完注解之后、实际使用和hibernate validator默认注解使用并无二致
        @UsernameNotRepeat(message = "用户名已经存在")
        private String username;
    
    	@PutMapping
        public User addUser(@RequestBody @Valid User user, BindingResult errors, HttpServletResponse response) {
            if (errors.hasErrors()) {
                errors.getAllErrors().forEach(error -> System.out.println(((FieldError) error).getField() + "" + error.getDefaultMessage()));
                response.setStatus(400);
                return user;
            }
            return iUserService.addUser(user);
        }
    

    如此、用户名不可重复的校验就完成了。

5、分组验证

同一个校验规则,不可能适用于所有的业务场景,对此,对每一个业务场景去编写一个校验规则,又显得特别冗余。这里我们刚好可以用到Hibernate-Validator的分组功能。

添加一个名为ValidGroupA的接口(接口内容可以是空的,所以就不列举代码)

添加一个需要分组校验的字段

  	@Data
    public class ExampleVo {
    
        @NotNull(message = "主键不允许为空",groups = ValidGroupA.class)
        private Integer id;
    
        @NotBlank(message = "用户名不能为空",groups = Default.class)
        private String userName;
        
        @Range(min = 18,max = 60,message = "只能填报年龄在18~60岁的",groups = Default.class)
        private String age;
    
        @EnumCheck(message = "只能选男:1或女:2",enumClass = Sex.class,groups = Default.class)
        private Integer sex;
    }

改动接口的内容

		@RequestMapping(value = "/info1",method = RequestMethod.POST)
        public String test1(@Validated({ValidGroupA.class,Default.class})  @RequestBody ExampleVo vo){
            return "恭喜你拿到参数了";
        }

 这里我们可以注意一下,校验的注解由 @Valid 改成了 @Validated。进行测试,保留ValidGroupA.class和去掉ValidGroupA.class的测试。

       使用分组能极大的复用需要验证的类信息。而不是按业务重复编写冗余的类。然而Hibernate-Validator还提供组序列的形式进行顺序式校验,此处就不重复列举了。我认为顺序化的校验,场景更多的是在业务处理类,例如联动的属性验证,值的有效性很大程度上不能从代码的枚举或常量类中来校验。

参考文章:https://blog.csdn.net/maniacer/article/details/90713669

                  https://blog.csdn.net/java_collect/article/details/85534054

                  https://www.cnblogs.com/mr-yang-localhost/p/7812038.html#_label3_3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值