校验注解的作用

系统执行业务逻辑之前,会对输入数据进行校验,检测数据是否有效合法的。所以我们可能会写大量的if else等判断逻辑,特别是在不同方法出现相同的数据时,校验的逻辑代码会反复出现,导致代码冗余,阅读性和可维护性极差。

自定义校验注解

引入依赖

Hibernate框架中有一个组件hibernate-validator专门用于数据校验,在平常的Spring项目中虽然数据层不使用Hibernate做ORM框架,但是hibernate-validator也经常被集成来做数据校验。

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

下面我们写一个用于URL校验的注解,实现一个简单的网站信息管理的URL校验,做校验的方式我们也使用现成的apache工具包中提供的校验工具。

<dependency>
    <groupId>commons-validator</groupId>
    <artifactId>commons-validator</artifactId>
    <version>1.7</version>
</dependency>

实现注解

校验注解

/**
 * 会将注解信息包含在javadoc中
 */
@Documented
/**
 * 1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
 * 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,
 *   这是默认的生命周期;
 * 3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
 *
 * 一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解,比如@Deprecated使用RUNTIME注解
 * 如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;
 * 如果只是做一些检查性的操作,比如 @Override 和 @Deprecated,使用SOURCE 注解。
 */
@Retention(RetentionPolicy.RUNTIME)
/**
 * 作用在字段上
 * TYPE - 作用在类上面
 * FILED - 作用在属性上面
 * METHOD - 作用在方法上
 * CONSTRUCTION - 作用在构造器上
 * PARAM - 作用在方法参数上
 * 允许多种的情况是 @Target({ElementType.FIELD,ElementType.METHOD})
 */
@Target(ElementType.FIELD)
/**
 * 对应校验类
 */
@Constraint(validatedBy = IsUrlValidator.class)
public @interface IsUrl {
    /**
     * 是否 强校验
     * @return
     */
    boolean required() default true;

    /**
     * 校验不通过返回信息
     * @return
     */
    String message() default "请输入正确的url";

    /**
     * 所属分组,即在有分组的情况下,只校验所在分组参数
     * @return
     */
    Class<?>[] groups() default {};

    /**
     * 主要是针对bean,很少使用
     *
     * @return 负载
     */
    Class<? extends Payload>[] payload() default {};

}

校验类

校验类需要实现ConstraintValidator<T,E>接口,第一个泛型为注解,第二个为校验的数据类型。

实现这个接口必须要重写isValid()方法,在其中实现主要的校验逻辑。

public class IsUrlValidator implements ConstraintValidator<IsUrl,String> {
    private boolean isRequired;

    /**
     * 初始化,获取是否强校验
     * @param constraintAnnotation
     */
    @Override
    public void initialize(IsUrl constraintAnnotation) {
        isRequired = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext context) {
        if (!isRequired){
            return true;
        }else {
            UrlValidator validator = UrlValidator.getInstance();
            return validator.isValid(s);
        }
    }
}

使用自定义注解

创建InsertUpdate分组别用于区分和开启校验

用于分组的类需要继承javax.validation.groups.Default接口

public interface Update extends Default {}
public interface Insert extends Default {}

创建一个WebSite类,对其中urlalternateUrl进行校验,这个字段分别属于Insert分组、Update分组的时候进行字段校验。

public class WebSite {
    /**
     * id
     */
    private Integer id;
    /**
     * 网站名称
     */
    private String name;
    /**
     * 网址
     */
    @IsUrl(groups = Insert.class)
    private String url;
    /**
     * 备用网址
     */
    @IsUrl(groups = Update.class)
    private String alternateUrl;
}

具体校验方式如下,在insert接口对Insert分组进行校验,也就是校验url属性,在updateAlternate接口对Update分组进行校验,也就是对alternateUrl字段进行校验。

@RestController
@RequestMapping("/website")
public class WebSiteController {
    @RequestMapping("/insert")
    public void insert(@RequestBody @Validated(Insert.class) WebSite site){
        System.out.println(site);
    }

    @RequestMapping("/updateAlternate")
    public void updateAlternateUrl(@RequestBody @Validated(Update.class) WebSite site){
        System.out.println(site);
    }
}

若校验不通过,代码会抛出MethodArgumentNotValidException异常,我们实现一个统一异常处理类来处理这个异常报错,并返回校验提示信息。

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    // 处理接口参数数据格式错误异常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseBody
    public Object errorHandler(HttpServletRequest request, MethodArgumentNotValidException e) {
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();

        String message = allErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
        log.error("{}请求,发生参数校验异常:{}",request.getServletPath(),message);
        return message;
    }
}

使用http工具调用接口,返回相关信息

首先使用一个错误的 url 参数调用 insert接口,校验不通过,但是调用updateAlternate接口可以通过。

POST http://localhost:8080/website/insert
Content-Type: application/json
{
  "id": 1,
  "name": "百度",
  "url":"htps://www.baidu.com/",
  "alternateUrl":"https://www.baidu.com/"
}

###

POST http://localhost:8080/website/updateAlternate
Content-Type: application/json
{
  "id": 1,
  "name": "百度",
  "url":"htps://www.baidu.com/",
  "alternateUrl":"https://www.baidu.com/"
}

调用insert接口的返回及日志打印如下

HTTP/1.1 200
Content-Type: text/plain;
charset=UTF-8
Content-Length: 21
Date: Wed, 02 Mar 2022 15:30:23
GMTKeep-Alive: timeout=60
Connection: keep-alive

请输入正确的url
--------------------------------------
xxx.GlobalExceptionHandler : /website/insert请求,发生参数校验异常:请输入正确的url

常用校验注解

@Null被注释的元素必须为 null
@NotNull被注释的元素必须不为 null
@AssertTrue被注释的元素必须为 true
@AssertFalse被注释的元素必须为 false
@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min)被注释的元素的大小必须在指定的范围内,元素必须为集合,代表集合个数
@Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past被注释的元素必须是一个过去的日期
@Future被注释的元素必须是一个将来的日期
@Length(min=, max=)被注释的字符串的大小必须在指定的范围内,必须为数组或者字符串,若微数组则表示为数组长度,字符串则表示为字符串长度
@NotEmpty被注释的字符串的必须非空
@Range(min=, max=)被注释的元素必须在合适的范围内
@NotBlank被注释的字符串的必须非空
@Pattern(regexp = )正则表达式校验
@Valid对象级联校验,即校验对象中对象的属性
03-05 21:57