## 1.为何需要自定义?

虽然JSR303和springboot-validator 已经提供了很多校验注解,但是当面对复杂参数校验时,还是不能满足我们的要求,这时候我们就需要 自定义校验注解。

例如BrandEntity中的showStatus,用 0代表不显示 1代表显示,我们自定义一个校验注解@ListValue,指定取值只能0和1。

```
/**
     * 显示状态[0-不显示;1-显示]
     */
    private Integer showStatus;
```

## 2.自定义校验步骤:

-   1、编写一个自定义的校验注解
-   2、编写一个自定义的校验器
-   3、关联自定义的校验器和自定义的校验注解

### 2.1创建注解

创建文件 `com/lcl/common/valid/ListValue.java`

```
package com.lcl.common.valid;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 校验注解
 *
 * @description
 * 可以参考 NotBlank 注解
 */

@Documented
// 会在下边定义一个校验器类 validatedBy = ListValueConstraintValidator.class
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {

  // String message() default "{javax.validation.constraints.NotBlank.message}";
  // 这里使用自定义的message,在resource下创建新的配置文件
  String message() default "{com.atguigu.common.valid.ListValue.message}";

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

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

  int[] vals() default {};

}
```

### 2.2创建自定义注解消息配置文件:

gulimall-common/src/main/resources/ValidationMessages.properties

```
com.atguigu.common.valid.ListValue.message=必须提交指定的值 
```

### **2.3自定义注解说明:**

一个标注(annotation) 是通过@interface关键字来定义的. 这个标注中的属性是声明成类似方法 的样式的. 根据Bean Validation API 规范的要求:

-   message属性, 这个属性被用来定义默认得消息模版, 当这个约束条件被验证失败的时候,通过 此属性来输出错误信息。
-   groups 属性, 用于指定这个约束条件属于哪(些)个校验组. 这个的默认值必须是Class<?>类型数组。
-   payload 属性, Bean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用。

除了这三个强制性要求的属性(message, groups 和 payload) 之外, 我们还添加了一个属性用来指定所要求的值. 此属性的名称vals在annotation的定义中比较特殊, 如果只有这个属性被赋值了的话, 那么, 在使用此annotation到时候可以忽略此属性名称。

另外, 我们还给这个annotation标注了一些元标注( metaannotatioins):

-   @Target({ METHOD, FIELD, ANNOTATION_TYPE }): 表示此注解可以被用在方法, 字段或者 annotation声明上。
-   @Retention(RUNTIME): 表示这个标注信息是在运行期通过反射被读取的.
-   @Constraint(validatedBy = ListValueConstraintValidator.class): 指明使用哪个校验器(类) 去校验使用了此标注的元素.
-   @Documented: 表示在对使用了该注解的类进行javadoc操作到时候, 这个标注会被添加到javadoc当中.

### 2.4创建校验器

com/lcl/common/valid/ListValueConstraintValidator.java

```
package com.lcl.common.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

/**
 * 约束校验器
 
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

  private Set<Integer> set = new HashSet<Integer>();

  /**
   * 初始化方法
   *
   * @param constraintAnnotation
   */
  @Override
  public void initialize(ListValue constraintAnnotation) {
    int[] vals = constraintAnnotation.vals();
    for (int val : vals) {
      set.add(val);
    }

  }

  /**
   * 判断是否校验成功
   *
   * @param value 需要校验的值
   * @param context
   * @return
   */
  @Override
  public boolean isValid(Integer value, ConstraintValidatorContext context) {
    return set.contains(value);
  }
}
```

ListValueConstraintValidator定义了两个泛型参数, 第一个是这个校验器所服务到标注类型(在我们的例子中即ListValue), 第二个这个校验器所支持到被校验元素的类型 (即Integer)。

如果一个约束标注支持多种类型的被校验元素的话, 那么需要为每个所支持的类型定义一个ConstraintValidator,并且注册到约束标注中。

这个验证器的实现就很平常了, `initialize()`方法传进来一个所要验证的标注类型的实例, 在本例中, 我们通过此实例来获取其vals属性的值,并将其保存为Set集合中供下一步使用。

`isValid()`是实现真正的校验逻辑的地方, 判断一个给定的int对于@ListValue这个约束条件来说是否是合法的。

### 2.5在参数对象中为 `showStatus` 属性使用 `@ListValue` 注解。

`com/atguigu/gulimall/product/entity/BrandEntity.java`

```
package com.atguigu.gulimall.product.entity;

import com.atguigu.common.valid.ListValue;
import com.atguigu.common.valid.group.AddGroup;
import com.atguigu.common.valid.group.UpdateGroup;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Pattern;

/**
 * 品牌
 * 
 * @author kaiyi
 * @email corwienwong@gmail.com
 * @date 2020-08-10 15:45:20
 */
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 品牌id
     */
    @TableId
    @NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class})
    @Null(message = "新增不能指定id", groups = {AddGroup.class})
    private Long brandId;
    /**
     * 品牌名
     */
    @NotBlank(message = "品牌名不能为空", groups = {AddGroup.class, UpdateGroup.class})
    private String name;
    /**
     * 品牌logo地址
     */
    @NotEmpty(message = "URL地址不能为空", groups = {AddGroup.class})
    @URL(message = "logo必须是一个合法的url地址", groups = {AddGroup.class, UpdateGroup.class})
    private String logo;
    /**
     * 介绍
     */
    private String descript;
    /**
     * 显示状态[0-不显示;1-显示]
     */
    //@Pattern()
    // 使用自定义校验注解
    @ListValue(vals = {0,1}, groups = {AddGroup.class, UpdateGroup.class})
    private Integer showStatus;
    /**
     * 检索首字母
     */
    @NotEmpty
    @Pattern(regexp = "/^[a-zA-Z]$/", message = "检索首字母必须是一个字母", groups = {AddGroup.class, UpdateGroup.class})
    private String firstLetter;
    /**
     * 排序
     */
    @NotNull
    @Min(value = 0, message = "排序必须大于等于0", groups = {AddGroup.class, UpdateGroup.class})
    private Integer sort;

}
```

### 2.6最后,在Postman中测试一下,自定义的注解是否起作用,重启服务: