寫自定義參數驗證方式

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

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

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

自定義參數驗證的思路

對於自定義參數驗證來講,須要注意的步驟有如下幾步:springboot

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

實戰參數驗證的公用方法

根據上面思路描述,咱們首先須要有註解來標記哪些實體屬性須要作不一樣的校驗,所以這裏建立兩種校驗註解(爲了本章簡短性):IsNotBlank(校驗不能爲空)和RegExp(正則匹配校驗),以下代碼:app

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描述

這裏有以下公用的驗證方法:spring-boot

 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參數註解(標記方法上的某個參數):spa

 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:hibernate

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

而後aop須要作以下邏輯:code

  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     }

相關文章
相關標籤/搜索