SpringBoot自定義校驗註解

eacape發表於2022-03-03

校驗註解的作用

系統執行業務邏輯之前,會對輸入資料進行校驗,檢測資料是否有效合法的。所以我們可能會寫大量的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物件級聯校驗,即校驗物件中物件的屬性

相關文章