本次發表文章距上次發表已近有兩月有余,原因是兩月前離開了上家公司(離開原因可能會在年終終結敘述,本篇暫且忽略),來到了現在所在的京東集團,需要花時間熟悉環境和沉淀一下新的東西,因此寫文章也暫時沒那么勤奮了,不得不說這次是機遇也是對自己職業生涯的一次重要決定。
話說本篇內容主要分享的是自定義方法參數的驗證,參數的基本校驗在對外接口或者公用方法時經常所見,用過hibernate的驗證方式的朋友一定不會陌生,讀完本篇內容能夠很好的幫助各位朋友對自定義參數驗證方式有一定了解:
- 自定義參數驗證的思路
- 實戰參數驗證的公用方法
- aop結合方法參數驗證實例
自定義參數驗證的思路
對於自定義參數驗證來說,需要注意的步驟有以下幾步:
- 怎么區分需要驗證的參數,或者說參數實體類中需要驗證的屬性(答案:可用注解標記)
- 對於參數要驗證哪幾種數據格式(如:非空、郵箱、電話以及是否滿足正則等格式)
- 怎么獲取要驗證的參數數據(如:怎么獲取方法參數實體傳遞進來的數據)
- 驗證失敗時提示的錯誤信息描述(如:統一默認校驗錯誤信息,或者獲取根據標記驗證注解傳遞的錯誤提示文字暴露出去)
- 在哪一步做校驗(如:進入方法內部時校驗,或是可以用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 }
然后為了統一這里創建公用的驗證方法,此方法需要傳遞待驗證參數的具體實例,其主要做的工作有:
- 通過傳遞進來的參數獲取該參數實體的屬性
- 設置field.setAccessible(true)允許獲取對應屬性傳進來的數據
- 根據對應標記屬性注解來驗證獲取的數據格式,格式驗證失敗直接提示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需要做如下邏輯:
- 獲取方法上傳遞參數(param1,param2...)
- 遍歷每個參數實體,如有驗證注解就做校驗
- 遍歷標記有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 }