寫自定義參數驗證方式

本次發表文章距上次發表已近有兩月有餘,原因是兩月前離開了上家公司(離開原因可能會在年終終結敘述,本篇暫且忽略),來到了現在所在的京東集團,需要花時間熟悉環境和沉澱一下新的東西,因此寫文章也暫時沒那麼勤奮了,不得不說這次是機遇也是對自己職業生涯的一次重要決定。

話說本篇內容主要分享的是自定義方法參數的驗證,參數的基本校驗在對外接口或者公用方法時經常所見,用過hibernate的驗證方式的朋友一定不會陌生,讀完本篇內容能夠很好的幫助各位朋友對自定義參數驗證方式有一定了解:

  • 自定義參數驗證的思路
  • 實戰參數驗證的公用方法
  • aop結合方法參數驗證實例

自定義參數驗證的思路

對於自定義參數驗證來說,需要注意的步驟有以下幾步:

  1. 怎麼區分需要驗證的參數,或者說參數實體類中需要驗證的屬性(答案:可用註解標記)
  2. 對於參數要驗證哪幾種數據格式(如:非空、郵箱、電話以及是否滿足正則等格式)
  3. 怎麼獲取要驗證的參數數據(如:怎麼獲取方法參數實體傳遞進來的數據)
  4. 驗證失敗時提示的錯誤信息描述(如:統一默認校驗錯誤信息,或者獲取根據標記驗證註解傳遞的錯誤提示文字暴露出去)
  5. 在哪一步做校驗(如:進入方法內部時校驗,或是可以用aop方式統一校驗位置)

實戰參數驗證的公用方法

根據上面思路描述,我們首先需要有註解來標記哪些實體屬性需要做不同的校驗,因此這裏創建兩種校驗註解(爲了本章簡短性):IsNotBlank(校驗不能爲空)和RegExp(正則匹配校驗),如下代碼:

1 @Documented
2 @Retention(RetentionPolicy.RUNTIME)
3 @Target(ElementType.FIELD)
4 public @interface IsNotBlank {
5     String des() default "";
6 }
1 @Documented
2 @Retention(RetentionPolicy.RUNTIME)
3 @Target(ElementType.FIELD)
4 public @interface RegExp {
5     String pattern();
6 
7     String des() default "";
8 }

然後爲了統一這裏創建公用的驗證方法,此方法需要傳遞待驗證參數的具體實例,其主要做的工作有:

  1. 通過傳遞進來的參數獲取該參數實體的屬性
  2. 設置field.setAccessible(true)允許獲取對應屬性傳進來的數據
  3. 根據對應標記屬性註解來驗證獲取的數據格式,格式驗證失敗直接提示des描述

這裏有如下公用的驗證方法:

 1 public class ValidateUtils {
 2 
 3     public static void validate(Object object) throws IllegalAccessException {
 4         if (object == null) {
 5             throw new NullPointerException("數據格式校驗對象不能爲空");
 6         }
 7         //獲取屬性列
 8         Field[] fields = object.getClass().getDeclaredFields();
 9         for (Field field : fields) {
10             //過濾無驗證註解的屬性
11             if (field.getAnnotations() == null || field.getAnnotations().length <= 0) {
12                 continue;
13             }
14             //允許private屬性被訪問
15             field.setAccessible(true);
16             Object val = field.get(object);
17             String strVal = String.valueOf(val);
18 
19             //具體驗證
20             validField(field, strVal);
21         }
22     }
23 
24     /**
25      * 具體驗證
26      *
27      * @param field  屬性列
28      * @param strVal 屬性值
29      */
30     private static void validField(Field field, String strVal) {
31         if (field.isAnnotationPresent(IsNotBlank.class)) {
32             validIsNotBlank(field, strVal);
33         }
34         if (field.isAnnotationPresent(RegExp.class)) {
35             validRegExp(field, strVal);
36         }
37         /** add... **/
38     }
39 
40     /**
41      * 匹配正則
42      *
43      * @param field
44      * @param strVal
45      */
46     private static void validRegExp(Field field, String strVal) {
47         RegExp regExp = field.getAnnotation(RegExp.class);
48         if (Strings.isNotBlank(regExp.pattern())) {
49             if (Pattern.matches(regExp.pattern(), strVal)) {
50                 return;
51             }
52             String des = regExp.des();
53             if (Strings.isBlank(des)) {
54                 des = field.getName() + "格式不正確";
55             }
56             throw new IllegalArgumentException(des);
57         }
58     }
59 
60     /**
61      * 非空判斷
62      *
63      * @param field
64      * @param val
65      */
66     private static void validIsNotBlank(Field field, String val) {
67         IsNotBlank isNotBlank = field.getAnnotation(IsNotBlank.class);
68         if (val == null || Strings.isBlank(val)) {
69             String des = isNotBlank.des();
70             if (Strings.isBlank(des)) {
71                 des = field.getName() + "不能爲空";
72             }
73             throw new IllegalArgumentException(des);
74         }
75     }
76 }

有了具體驗證方法,我們需要個測試實例,如下測試接口和實體:

1 public class TestRq extends BaseRq implements Serializable {
2 
3     @IsNotBlank(des = "暱稱不能爲空")
4     private String nickName;
5     @RegExp(pattern = "\\d{10,20}", des = "編號必須是數字")
6     private String number;
7     private String des;
8     private String remark;
9 }
1     @PostMapping("/send")
2     public BaseRp<TestRp> send(@RequestBody TestRq rq) throws IllegalAccessException {
3         ValidateUtils.validate(rq);
4         return testService.sendTestMsg(rq);
5     }

aop結合方法參數驗證實例

上面是圍繞公用驗證方法來寫的,通常實際場景中都把它和aop結合來做統一驗證;來定製兩個註解,MethodValid方法註解(是否驗證所有參數)和ParamValid參數註解(標記方法上的某個參數):

 1 @Documented
 2 @Retention(RetentionPolicy.RUNTIME)
 3 @Target(value = {ElementType.METHOD})
 4 public @interface MethodValid {
 5     /**
 6      * 驗證所有參數
 7      *
 8      * @return true
 9      */
10     boolean isValidParams() default true;
11 }
1 @Documented
2 @Retention(RetentionPolicy.RUNTIME)
3 @Target(value = {ElementType.PARAMETER})
4 public @interface ParamValid {
5 }

有了兩個標記註解再來創建aop,我這裏是基於springboot框架的實例,所有引入如下mvn:

1         <dependency>
2             <groupId>org.springframework.boot</groupId>
3             <artifactId>spring-boot-starter-aop</artifactId>
4         </dependency>

然後aop需要做如下邏輯:

  1. 獲取方法上傳遞參數(param1,param2...)
  2. 遍歷每個參數實體,如有驗證註解就做校驗
  3. 遍歷標記有ParamValid註解的參數,如有驗證註解就做校驗

這裏特殊的地方是,想要獲取方法參數對應的註解,需要method.getParameterAnnotations()獲取所有所有參數註解後,再用索引來取參數對應的註解;如下aop代碼:

 1 package com.shenniu003.common.validates;
 2 
 3 import com.shenniu003.common.validates.annotation.MethodValid;
 4 import com.shenniu003.common.validates.annotation.ParamValid;
 5 import org.aspectj.lang.ProceedingJoinPoint;
 6 import org.aspectj.lang.annotation.Around;
 7 import org.aspectj.lang.annotation.Aspect;
 8 import org.aspectj.lang.reflect.MethodSignature;
 9 import org.springframework.stereotype.Component;
10 
11 import java.lang.annotation.Annotation;
12 import java.lang.reflect.Method;
13 import java.util.Arrays;
14 
15 /**
16  * des:
17  *
18  * @author: shenniu003
19  * @date: 2019/12/01 11:04
20  */
21 @Aspect
22 @Component
23 public class ParamAspect {
24 
25     @Around(value = "@annotation(methodValid)", argNames = "joinPoint,methodValid")
26     public Object validMethod(ProceedingJoinPoint joinPoint, MethodValid methodValid) throws Throwable {
27         MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
28         Method method = methodSignature.getMethod();
29         System.out.println("method:" + method.getName());
30         String strArgs = Arrays.toString(joinPoint.getArgs());
31         System.out.println("params:" + strArgs);
32 
33         //獲取方法所有參數的註解
34         Annotation[][] parametersAnnotations = method.getParameterAnnotations();
35 
36         for (int i = 0; i < joinPoint.getArgs().length; i++) {
37             Object arg = joinPoint.getArgs()[i];
38             if (arg == null) {
39                 continue; //
40             }
41 
42             if (methodValid.isValidParams()) {
43                 //驗證所有參數
44                 System.out.println(arg.getClass().getName() + ":" + arg.toString());
45                 ValidateUtils.validate(arg);
46             } else {
47                 //只驗證參數前帶有ParamValid註解的參數
48                 //獲取當前參數所有註解
49                 Annotation[] parameterAnnotations = parametersAnnotations[i];
50                 //是否匹配參數校驗註解
51                 if (matchParamAnnotation(parameterAnnotations)) {
52                     System.out.println(Arrays.toString(parameterAnnotations) + " " + arg.getClass().getName() + ":" + arg.toString());
53                     ValidateUtils.validate(arg);
54                 }
55             }
56         }
57         return joinPoint.proceed();
58     }
59 
60     /**
61      * 是否匹配參數的註解
62      *
63      * @param parameterAnnotations 參數對應的所有註解
64      * @return 是否包含目標註解
65      */
66     private boolean matchParamAnnotation(Annotation[] parameterAnnotations) {
67         boolean isMatch = false;
68         for (Annotation parameterAnnotation : parameterAnnotations) {
69             if (ParamValid.class == parameterAnnotation.annotationType()) {
70                 isMatch = true;
71                 break;
72             }
73         }
74         return isMatch;
75     }
76 }

這裏編寫3中方式的測試用例,驗證方法所有參數、無參數不驗證、驗證方法參數帶有@ParamValid的參數,以此達到不同需求參數的校驗方式:

 1     //驗證方法所有參數
 2     @MethodValid
 3     public void x(TestRq param1, String param2) {
 4     }
 5     //無參數不驗證
 6     @MethodValid
 7     public void xx() {
 8     }
 9     //驗證方法參數帶有@ParamValid的參數
10     @MethodValid(isValidParams = false)
11     public void xxx(TestRq param1, @ParamValid String param2) {
12     }

同樣用send接口作爲測試入口,調用上面3種方法:

1     @PostMapping("/send")
2     @MethodValid
3     public BaseRp<TestRp> send(@RequestBody TestRq rq) throws IllegalAccessException {
4 //        ValidateUtils.validate(rq);
5         testController.x(rq, "驗證方法所有參數");
6         testController.xx();
7         testController.xxx(rq, "驗證方法參數帶有@ParamValid的參數");
8         return testService.sendTestMsg(rq);
9     }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章