javax.validation 包里@NotNull等注解的使用

在做项目的时候,对pojo和传入的参数进行校验,如果是代码编写,需要很多if来判断

其实可根据一些校验的注解来实现我们的参数校验,主要介绍一下常用的 javax.validation 这个仓库的使用,这里总结一下

1、导包

在项目的pom.xml 文件夹中导入包

<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
    <dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>1.1.0.Final</version>
    </dependency>

这个验证根据JSR 380规范,validation-api 依赖包含标准验证注解

2、使用

这个包的注解主要有以下这么多,先简单说明(以版本 号2.0.2为标准),下面再分别介绍

注解名称

验证得类型

描述

AssertFalse

Boolean,boolean

被注解的元素属性值必须为false

AssertTrue

Boolean,boolean

被注解的元素属性值必须为true

DecimalMax

BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型

验证注解的元素值小于等于指定的value值

DecimalMin

BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型

验证注解的元素值大于等于指定的value值

Digits

BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型

验证注解的元素值的整数位数和小数位数上限

Max

BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型

被注解的元素值的类型必须为数字,其值必须小于等于指定的最大值

Min

BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型

被注解的元素值的类型必须为数字,其值必须大于等于指定的最小值

Email

CharSequence子类型(如String)

Future

java.util.Date,java.util.Calendar;Joda Time类库的日期类型

被注解的元素值得范围必须为未来的一个时间

FutureOrPresent

java.util.Date,java.util.Calendar;Joda Time类库的日期类型

被注解的元素值得范围必须为未来的一个时间或者现在得时间

Past

java.util.Date,java.util.Calendar;Joda Time类库的日期类型

被注解的元素的值范围必须为过去的一个时间

PastOrPresent

java.util.Date,java.util.Calendar;Joda Time类库的日期类型

被注解的元素的值范围必须为过去或者现在的一个时间

Negative

数值

适用于数值并验证它们是严格负数,不包括0

NegativeOrZero

数值

适用于数值并验证它们是严格负数,包括0

NotBlank

CharSequence子类型

验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格

NotEmpty

CharSequence子类型、Collection、Map、数组

验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)

NotNull

任意类型

被注解的元素属性值必须非null

Null

任意类型

被注解的元素属性值必须为null

Pattern

String,任何CharSequence的子类型

被注解的元素值必须符合指定的正则表达式

Positive

数值

适用于数值并验证它们是严格正数,不包括0

PositiveOrZero

数值

适用于数值并验证它们是严格正数,包括0

Size

String, Collection, Map和数组属性

被注解的元素属性值的大小必须在指定范围内

2.0 注解的介绍

// 表明注解可以使用在哪里
// 这里表示注解可以使用在 方法 属性  注解  构造方法  参数  type 
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
//描述保留注解的各种策略,它们与元注解(@Retention)一起指定注释要保留多长时间
@Retention(RetentionPolicy.RUNTIME)
//表明这个注解是由 javadoc记录的
@Documented
public @interface MyOwnAnnotation {
    
}

2.1 注解详细属性的介绍

查看上面所有注解的源码,关于注解中的字段,主要分为以下两种

2.1.1 不能自定义值的

以下面的这些注解为代表

AssertFalse AssertTrue

PositiveOrZero Positive Negative NegativeOrZero

NotNull Null NotEmpty NotBlank

FutureOrPresent Future PastOrPresent Past

@AssertFalse为例,该注解的详细内容(源码)

@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
// 这个表明可以重复使用,
//例如在一个field使用这个注解
// 一般注解只能使用一次
//@Max(value = 11)
//Long num;
// 如果使用了这个@Repeatable标注,那就可以使用两次
//@Max(value = 11)
//@Max(value = 22)
//Long num;
@Repeatable(AssertFalse.List.class)
// 这个Constraint 指定验证类,如果不自定义指定,实现其默认的验证类 ,如果指定
// @Constraint(validatedBy = { MyValidator.class })
@Constraint(validatedBy = {})
public @interface AssertFalse {
    // 校验失败的信息,可以自定义
    String message() default "{javax.validation.constraints.AssertFalse.message}";
    // 这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作
    Class<?>[] groups() default {};
	// 这个主要是指定bean,一般很少使用
    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        AssertFalse[] value();
    }
}
2.1.2 能自定义值的

其他的注解都是可以自定义一些值,作为校验的参照值

@Max为例

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Max.List.class)
@Documented
@Constraint(validatedBy = {})
public @interface Max {
    // 校验失败的信息,可以自定义
    String message() default "{javax.validation.constraints.Max.message}";

    // 这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作
    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
	// 与上面不同的就是多了个value,这个需要自定义参数传入的值与这个value值对比,也就是校验的参考值
    long value();

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        Max[] value();
    }
}
2.1.3 groups属性的使用

这个属性的作用就是举个例子:例如我们在传参的时候,加入我们创建一个用户,那么我们再传入的参数是不会包含用户的id,而如果更新这个用户,我们就需要传入用户id,那么根据多种校验规则的时候,这个属性就起作用了。

如果我们在一个属性使用注解的时候,如果不指定groups的时候,其实默认是在Default 组别中

@NotNull(message = "姓名不能为空")
private String name;

等同于
import javax.validation.groups.Default;
@NotNull(message = "姓名不能为空" , groups = {Default.class})
private String name;

那么根据这个情况,如果我们想自定义组别的时候,我们就可以分为以下几个步骤:

  • 定义组别
  • 使用组别

(1)定义组别

还是这个例子:如果创建一个用户,不用传入用户的id,如果更新这个用户,我们就需要传入用户id

那么我们就定义两个组别,创建用户组别、更新用户组别(其实就是两个接口,里面不用有方法,但是要注意要继承Default接口,理由下面会说

import javax.validation.groups.Default;

public interface CreateUser extends Default {
}

import javax.validation.groups.Default;

public interface UpdateUser extends Default{
}

(2)使用组别

1)首先在pojo中,我们需要使用组别,若属性在不同的组别有不同的校验方式,那么就特殊指定其需要校验的规则,如果不指定,就还是默认按照Default

这里需要注意:定义的组别最好要继承Default接口,不然当我们在指定规则的校验时候,那么不标注groups的属性就不再校验了,因为默认按照Default

@Data
class User{
    	
    	@NotNull(message = "用户id不能为空", groups = UpdateUser.class)
    	private Integer id;
   
        @NotNull(message = "姓名不能为空" ,groups = {CreateUser.class, UpdateUser.class})
        private String name;
    
        @NotNull(message = "性别不能为空")
        private String sex;
    
        @Max(value = 20 ,message = "最大长度为20")
        private String address;
    
        @Email(message = "不满足邮箱格式")
        private String email;
    
        @AssertTrue(message = "字段为true才能通过")
        private boolean isAuth;
    
        @NotBlank(message = "手机号不能为空")
        @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
        private String mobile;
    
        @Future(message = "时间在当前时间之后才可以通过")
        private Date date;
}

2)在接口中使用特定的组别。

两个接:

  • 创建user的时候,指定验证的组别是CreateUser
  • 更新user的时候,指定验证的组别是UpdateUser

(当自定义的组别继承了Default,这里指定验证组别的,属性会根据指定的groups来进行校验,那么没有指定groups的都会校验,如果不继承Default,那没有自定义groups为这个组别的属性就不生效了)

/**
     * 走参数校验注解的 groups 组合校验
     *
     * @param user
     * @return
     */
@PostMapping("/users/update")
public ResponseDTO updateUser(@RequestBody @Validated(UpdateUser.class) User user) {
     userService.updateById(userDTO);
     return ResponseDTO.success();
}


 /**
     * 走参数校验注解的 groups 组合校验
     *
     * @param user
     * @return
     */
@PostMapping("/users/save")
public ResponseDTO saveUser(@RequestBody @Validated(CreateUser.class) User user) {
     userService.saveUser(userDTO);
     return ResponseDTO.success();
}

3、使用

3.1 第一种方式,pojo作为传参的形式

  • Pojo 定义验证规则
  • 以pojo作为参数传参

(1)User pojo类 定义校验规则

@Data
class User{
   
        @NotNull(message = "姓名不能为空")
        private String name;
    
        @NotNull(message = "性别不能为空")
        private String sex;
    
        @Max(value = 20 ,message = "最大长度为20")
        private String address;
    
        @Email(message = "不满足邮箱格式")
        private String email;
    
        @AssertTrue(message = "字段为true才能通过")
        private boolean isAuth;
    
        @NotBlank(message = "手机号不能为空")
        @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
        private String mobile;
    
        @Future(message = "时间在当前时间之后才可以通过")
        private Date date;
}

(2)让校验规则生效。在Controller类的时候,我们只需要利用 @Validated 注解来实现pojo的校验

@RequestMapping("users")
public ResponseDTO saveUser( @RequestBody @Validated User user){
    
}

(3)捕捉验证异常。如果参数校验通过,那么就直接执行接口方法,但是如果失败了,我们如何进行处理

MethodArgumentNotValidException是springBoot中进行绑定参数校验时的异常,需要在springBoot中处理

其他需要处理 ConstraintViolationException异常进行处理

一般像异常捕捉的,可以自定义一个异常捕捉Handler,实现异常捕捉后的数据返回

import com.dto.ResponseDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;

@RestControllerAdvice
public class GlobalExceptionHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private static int DUPLICATE_KEY_CODE = 1001;
    private static int PARAM_FAIL_CODE = 1002;
    private static int VALIDATION_CODE = 1003;

    /**
     * 处理自定义异常
     */
    @ExceptionHandler(BizException.class)
    public ResponseDTO handleRRException(BizException e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(e.getCode(), e.getMessage());
    }

    /**
     * 方法参数校验
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseDTO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(PARAM_FAIL_CODE, e.getBindingResult().getFieldError().getDefaultMessage());
    }

    /**
     * ValidationException
     */
    @ExceptionHandler(ValidationException.class)
    public ResponseDTO handleValidationException(ValidationException e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(VALIDATION_CODE, e.getCause().getMessage());
    }

    /**
     * ConstraintViolationException
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseDTO handleConstraintViolationException(ConstraintViolationException e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(PARAM_FAIL_CODE, e.getMessage());
    }
	/**
	* 路径异常
	*/
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseDTO handlerNoFoundException(Exception e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(404, "不好意思,路径不存在,请检查路径是否正确");
    }
	/**
	* 所有其他异常捕捉
	*/
    @ExceptionHandler(Exception.class)
    public ResponseDTO handleException(Exception e) {
        logger.error(e.getMessage(), e);
        return new ResponseDTO(500, "不好意思,系统繁忙,请稍后再试");
    }
}

3.2 第二种方式,restful风格

  • Controller 上加上@Validated
  • 参数上加注解
@RestController
@RequestMapping("user/")
@Validated
public class UserController{
    @RequestMapping("users)
    public ResponseDTO getUser(@RequestParam("userId") @NotNull(message = "用户id不能为空") Long userId){

    }
}

4、自定义使用

如果我们想自定义一个验证的注解,那么需要怎么做呢?

  • 定义一个注解
  • 编写一个验证类
  • 使用

**(1)**我们首先定义一个像上面的@Null 这样的注解

@Documented
// 这里标注该注解是使用在filed和参数上的
@Target({ElementType.PARAMETER, ElementType.FIELD})
// 运行时生效
@Retention(RetentionPolicy.RUNTIME)
// 指定验证的类是哪个   MyValidator 就是第二步做的事情
@Constraint(validatedBy = MyValidator.class)
public @interface MyValid {
	// 提供自定义异常的信息,可以指定默认值
    String message() default "参数不合法";
    // 分组,详细看 上面的介绍 
    Class<?>[] groups() default {};
	// 针对bean的,使用不多
    Class<? extends Payload>[] payload() default {};
}

(2)编写一个验证类

我们需要编写一个 实现 ConstraintValidator 类实现类,来指定我们的校验规则

如果不指定特定注解的情况下,直接使用

// 这个是Max的指定的验证规则源码
public abstract class AbstractMaxValidator<T> implements ConstraintValidator<Max, T> {
    protected long maxValue;

    public AbstractMaxValidator() {
    }
    public void initialize(Max maxValue) {
        this.maxValue = maxValue.value();
    }
    public boolean isValid(T value, ConstraintValidatorContext constraintValidatorContext) {
        if (value == null) {
            return true;
        } else {
            return this.compare(value) <= 0;
        }
    }
    protected abstract int compare(T var1);
}

// 自定义的验证类
public class MyValidator implements ConstraintValidator {
    @Override
    public void initialize(Annotation constraintAnnotation) {
		// 可以获取注解的值 ,一般写在该方法中
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        // 编写自己属性验证的规则,o 则是待验证的值
        return false;
    }
}

如果在指定特定注解的情况下,那么我们就可特定 注解

// 自定义的验证类
public class MyValidator implements ConstraintValidator<MyValid , Object> {
    @Override
    public void initialize(MyValid myValid) {
		// 可以获取注解的值 ,一般写在该方法中
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        // 编写自己属性验证的规则,o 则是待验证的值
        return false;
    }
}

(3)使用。就跟正常的那些注解一样使用即可。

@Data
public Class Example{
    @NotBlank(message = "姓名不能为空")
    @MyValid(message = "姓名有误,请核对后提交")
    private String name;
}