校驗註解的作用
系統執行業務邏輯之前,會對輸入數據進行校驗,檢測數據是否有效合法的。所以我們可能會寫大量的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);
}
}
}
使用自定義註解
創建Insert、Update分組別用於區分和開啓校驗
用於分組的類需要繼承javax.validation.groups.Default接口
public interface Update extends Default {}
public interface Insert extends Default {}
創建一個WebSite類,對其中url 、alternateUrl進行校驗,這個字段分別屬於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 | 對象級聯校驗,即校驗對象中對象的屬性 |