javax.validation 参数验证

为什么要用validator

1. javax.validation的一系列注解可以帮我们完成参数校验,免去繁琐的串行校验

2. 什么是javax.validation

JSR303 是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面(面向注解编程的时代),就可以在需要校验的时候进行校验了,在SpringBoot中已经包含在starter-web中,再其他项目中可以引用依赖,并自行调整版本:
     <!--jsr 303-->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>1.1.0.Final</version>
        </dependency>
        <!-- hibernate validator-->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>5.2.0.Final</version>
        </dependency>

3. 注解说明

@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,且trim后长度必须大于0    
@Email  被注释的元素必须是电子邮箱地址    
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内    
@NotEmpty   被注释的字符串的必须非空    
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内

@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实体类进行校验 
验证注解验证的数据类型说明
@AssertFalse Boolean,boolean 验证注解的元素值是false
@AssertTrue Boolean,boolean 验证注解的元素值是true
@NotNull 任意类型 验证注解的元素值不是null
@Null 任意类型 验证注解的元素值是null
@Min(value=值) BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 验证注解的元素值大于等于@Min指定的value值
@Max(value=值) 和@Min要求一样 验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值) 和@Min要求一样 验证注解的元素值大于等于@ DecimalMin指定的value值
@DecimalMax(value=值) 和@Min要求一样 验证注解的元素值小于等于@ DecimalMax指定的value值
@Digits(integer=整数位数, fraction=小数位数) 和@Min要求一样 验证注解的元素值的整数位数和小数位数上限
@Size(min=下限, max=上限) 字符串、Collection、Map、数组等 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Past java.util.Date,java.util.Calendar;Joda Time类库的日期类型 验证注解的元素值(日期类型)比当前时间早
@Future 与@Past要求一样 验证注解的元素值(日期类型)比当前时间晚
@NotBlank CharSequence子类型 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限) CharSequence子类型 验证注解的元素值长度在min和max区间内
@NotEmpty CharSequence子类型、Collection、Map、数组 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Range(min=最小值, max=最大值) BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式) CharSequence子类型(如String) 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式) String,任何CharSequence的子类型 验证注解的元素值与指定的正则表达式匹配
@Valid 任何非原子类型 指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证
此处只列出Hibernate Validator提供的大部分验证约束注解,请参考hibernate validator官方文档了解其他验证约束注解和进行自定义的验证约束注解定义。
 
 

实战演练

1. @Validated 声明要检查的参数

  添加@Valid或者@Validated都可以。

 /**
     * 走参数校验注解
     *
     * @param userDTO
     * @return
     */
    @PostMapping("/save/valid")
    public RspDTO save(@RequestBody @Validated UserDTO userDTO) {
        userService.save(userDTO);
        return RspDTO.success();
    }

2. 对参数的字段进行注解标注

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;

@Data
public class UserDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    /*** 用户ID*/
    @NotNull(message = "用户id不能为空")
    private Long userId;
    
    /** 用户名*/
    @NotBlank(message = "用户名不能为空")
    @Length(max = 20, message = "用户名不能超过20个字符")
    @Pattern(regexp = "^[\u4E00-\u9FA5A-Za-z0-9\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字")
    private String username;
    
    /** 手机号*/
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
    private String mobile;

    /**性别*/
    private String sex;

    /** 邮箱*/
    @NotBlank(message = "联系邮箱不能为空")
    @Email(message = "邮箱格式不对")
    private String email;

    /** 密码*/
    private String password;

    /*** 创建时间 */
    @Future(message = "时间必须是将来时间")
    private Date createTime;

}

3. 在全局校验中增加校验异常

import cn.hutool.core.util.StrUtil;
import com.eyxyt.basement.bean.Result;
import com.eyxyt.basement.enums.ResultEnum;
import com.eyxyt.basement.exception.CustomException;
import com.eyxyt.basement.exception.UnauthorizedException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.ValidationException;
import java.util.StringJoiner;

/**
 * 异常控制处理器
 * @author cd.wang
 * @create 2020-07-15 14:39
 * @since 1.0.0
 */
@Slf4j
@RestControllerAdvice
public class CustomExceptionHandler {

    /**
     * 捕捉其他所有自定义异常
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public Result handle(CustomException e) {
        if(StrUtil.isBlank(e.getText())){
            e.setText(ResultEnum.ERROR.getText());
        }
        return Result.error(e);
    }

    /**
     * 捕捉401 未授权异常
     * @return
     */
    @ExceptionHandler(UnauthorizedException.class)
    public Result handle(UnauthorizedException e) {
        return Result.error(ResultEnum.UNAUTHORIZED.getCode(), e.getMessage(), ResultEnum.UNAUTHORIZED.getText());
    }

    /**
     * 捕捉所有Shiro异常
     * @param e
     * @return
     */
    @ExceptionHandler(ShiroException.class)
    public Result handle401(ShiroException e) {
        log.error("Shiro 异常 (" + e.getMessage() + ")");
        return Result.error(ResultEnum.UNAUTHORIZED);
    }

    @ExceptionHandler(AuthorizationException.class)
    public Result handleAuthorizationException(AuthorizationException e){
        log.error("Token 异常 (" + e.getMessage() + ")");
        return Result.error(ResultEnum.UNAUTHORIZED);
    }

    /**
     * 单独捕捉Shiro(UnauthorizedException)异常
     * 该异常为访问有权限管控的请求而该用户没有所需权限所抛出的异常
     * @param e
     * @return
     */
    @ExceptionHandler(org.apache.shiro.authz.UnauthorizedException.class)
    public Result handle401(org.apache.shiro.authz.UnauthorizedException e) {
        log.error("无权访问(Unauthorized):当前Subject没有此请求所需权限(" + e.getMessage() + ")");
        return Result.error(ResultEnum.UNAUTHORIZED);
    }

    /**
     * 单独捕捉Shiro(UnauthenticatedException)异常
     * 该异常为以游客身份访问有权限管控的请求无法对匿名主体进行授权,而授权失败所抛出的异常
     * @param e
     * @return
     */
    @ExceptionHandler(UnauthenticatedException.class)
    public Result handle401(UnauthenticatedException e) {
        log.error("无权访问(Unauthorized):当前Subject是匿名Subject,请先登录(This subject is anonymous.)");
        return Result.error(ResultEnum.UNAUTHORIZED);
    }

    /**
     * 方法参数校验
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error(e.getMessage(), e);
        // 按需重新封装需要返回的错误信息 解析原错误信息,
        // 封装后返回,此处返回非法的字段名称error.getField(),
        // 原始值error.getRejectedValue(),错误信息
        StringJoiner sj = new StringJoiner(";");
        e.getBindingResult().getFieldErrors().forEach(x -> sj.add(x.getDefaultMessage()));
        return Result.error(ResultEnum.PARAM_ERROR, sj.toString());
    }


    /**
     * ValidationException
     */
    @ExceptionHandler(ValidationException.class)
    public Result handleValidationException(ValidationException e) {
        log.error(e.getMessage(), e);
        return Result.error(ResultEnum.PARAM_ERROR, e.getCause().getMessage());
    }

    /**
     * ConstraintViolationException
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Result handleConstraintViolationException(ConstraintViolationException e) {
        log.error(e.getMessage(), e);
        StringJoiner sj = new StringJoiner(";");
        e.getConstraintViolations().forEach(x -> sj.add(x.getMessageTemplate()));
        return Result.error(ResultEnum.PARAM_ERROR, sj.toString());
    }

    /**
     * 方法请求异常
     * @param e
     * @return
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Result handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        log.error(e.getMessage(), e);
        return Result.error(Constant.METHOD_NOT_SUPPORTED, "不支持'" + e.getMethod() + "'请求方法");
    }
    /**
     * 捕捉404异常
     * @return
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public Result handle(NoHandlerFoundException e) {
        return Result.error(ResultEnum.NOT_FOUND);
    }

    /**
     * 捕捉数据库异常
     * @return
     */
    @ExceptionHandler(DuplicateKeyException.class)
    public Result handle(DuplicateKeyException e) {
        return Result.error(ResultEnum.DATA_EXISTS);
    }

    /**
     * 捕捉其他所有异常
     * @param request
     * @param ex
     * @return
     */
    @ExceptionHandler(Exception.class)
    public Result globalException(HttpServletRequest request, Throwable ex) {
        log.error("--"+ this.getClass().getSimpleName()+"--> " + ex.getMessage());
        ex.printStackTrace();
        return Result.error(this.getStatus(request).value(), ex.toString() + ": " + ex.getMessage(), ResultEnum.ERROR.getText());
    }


    /**
     * 获取状态码
     * @param request
     * @return
     */
    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }

自定义参数注解

1. 比如我们来个 自定义身份证校验 注解

@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentityCardNumberValidator.class)
public @interface IdentityCardNumber {

    String message() default "身份证号码不合法";

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

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

这个注解是作用在Field字段上,运行时生效,触发的是IdentityCardNumber这个验证类。

  • message 定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制
  • groups 这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作
  • payload 主要是针对bean的,使用不多。

2. 然后自定义Validator

public class IdentityCardNumberValidator implements ConstraintValidator<IdentityCardNumber, Object> {

    @Override
    public void initialize(IdentityCardNumber identityCardNumber) {
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        return IdCardValidatorUtils.isValidate18Idcard(o.toString());
    }
}

3. 使用自定义的注解

 @NotBlank(message = "身份证号不能为空")
    @IdentityCardNumber(message = "身份证信息有误,请核对后提交")
    private String clientCardNo;

4.使用groups的校验

1. 先定义groups的分组接口CreateUpdate

import javax.validation.groups.Default;

public interface Create extends Default {

}

import javax.validation.groups.Default;

public interface Update extends Default{

}

同一个对象要复用,比如UserDTO在更新时候要校验userId,在保存的时候不需要校验userId,在两种情况下都要校验username,那就用上groups

2. 再在需要校验的地方@Validated声明校验组

 /**
     * 走参数校验注解的 groups 组合校验
     *
     * @param userDTO
     * @return
     */
    @PostMapping("/update/groups")
    public RspDTO update(@RequestBody @Validated(Update.class) UserDTO userDTO) {
        userService.updateById(userDTO);
        return RspDTO.success();
    }

3. 在DTO中的字段上定义好groups = {}的分组类型

@Data
public class UserDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    /*** 用户ID*/
    @NotNull(message = "用户id不能为空", groups = Update.class)
    private Long userId;

    /**
     * 用户名
     */
    @NotBlank(message = "用户名不能为空")
    @Length(max = 20, message = "用户名不能超过20个字符", groups = {Create.class, Update.class})
    @Pattern(regexp = "^[\u4E00-\u9FA5A-Za-z0-9\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字")
    private String username;

    /**
     * 手机号
     */
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误", groups = {Create.class, Update.class})
    private String mobile;

    /**
     * 性别
     */
    private String sex;

    /**
     * 邮箱
     */
    @NotBlank(message = "联系邮箱不能为空")
    @Email(message = "邮箱格式不对")
    private String email;

    /**
     * 密码
     */
    private String password;

    /*** 创建时间 */
    @Future(message = "时间必须是将来时间", groups = {Create.class})
    private Date createTime;

}
注意: 在声明分组的时候尽量加上 extend javax.validation.groups.Default 否则,在你声明@Validated(Update.class) 的时候,就会出现你在默认没添加groups = {}的时候的校验组@Email(message = "邮箱格式不对"),会不去校验,因为默认的校验组是groups = {Default.class}.

5.restful风格用法

在多个参数校验,或者@RequestParam 形式时候,需要在controller上加注@Validated

 @GetMapping("/get")
    public RspDTO getUser(@RequestParam("userId") @NotNull(message = "用户id不能为空") Long userId) {
        User user = userService.selectById(userId);
        if (user == null) {
            return new RspDTO<User>().nonAbsent("用户不存在");
        }
        return new RspDTO<User>().success(user);
    }
@RestController
@RequestMapping("user/")
@Validated
public class UserController extends AbstractController {

}

参考链接:https://www.cnblogs.com/moues/p/11399421.html

 
原文地址:https://www.cnblogs.com/iathanasy/p/13409941.html