本次發表文章距上次發表已近有兩月有餘,原因是兩月前離開了上家公司(離開原因可能會在年終終結叙述,本篇暫且忽略),來到了現在所在的京東集團,需要花時間熟悉環境和沉澱一下新的東西,是以寫文章也暫時沒那麼勤奮了,不得不說這次是機遇也是對自己職業生涯的一次重要決定。
話說本篇内容主要分享的是自定義方法參數的驗證,參數的基本校驗在對外接口或者公用方法時經常所見,用過hibernate的驗證方式的朋友一定不會陌生,讀完本篇内容能夠很好的幫助各位朋友對自定義參數驗證方式有一定了解:
- 自定義參數驗證的思路
- 實戰參數驗證的公用方法
- aop結合方法參數驗證執行個體
對于自定義參數驗證來說,需要注意的步驟有以下幾步:
- 怎麼區分需要驗證的參數,或者說參數實體類中需要驗證的屬性(答案:可用注解标記)
- 對于參數要驗證哪幾種資料格式(如:非空、郵箱、電話以及是否滿足正則等格式)
- 怎麼擷取要驗證的參數資料(如:怎麼擷取方法參數實體傳遞進來的資料)
- 驗證失敗時提示的錯誤資訊描述(如:統一預設校驗錯誤資訊,或者擷取根據标記驗證注解傳遞的錯誤提示文字暴露出去)
- 在哪一步做校驗(如:進入方法内部時校驗,或是可以用aop方式統一校驗位置)
根據上面思路描述,我們首先需要有注解來标記哪些實體屬性需要做不同的校驗,是以這裡建立兩種校驗注解(為了本章簡短性):IsNotBlank(校驗不能為空)和RegExp(正則比對校驗),如下代碼:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface IsNotBlank {
String des() default "";
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RegExp {
String pattern();
String des() default "";
}
然後為了統一這裡建立公用的驗證方法,此方法需要傳遞待驗證參數的具體執行個體,其主要做的工作有:
- 通過傳遞進來的參數擷取該參數實體的屬性
- 設定field.setAccessible(true)允許擷取對應屬性傳進來的資料
- 根據對應标記屬性注解來驗證擷取的資料格式,格式驗證失敗直接提示des描述
這裡有如下公用的驗證方法:
public class ValidateUtils {
public static void validate(Object object) throws IllegalAccessException {
if (object == null) {
throw new NullPointerException("資料格式校驗對象不能為空");
}
//擷取屬性列
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
//過濾無驗證注解的屬性
if (field.getAnnotations() == null || field.getAnnotations().length <= 0) {
continue;
}
//允許private屬性被通路
field.setAccessible(true);
Object val = field.get(object);
String strVal = String.valueOf(val);
//具體驗證
validField(field, strVal);
}
}
/**
* 具體驗證
*
* @param field 屬性列
* @param strVal 屬性值
*/
private static void validField(Field field, String strVal) {
if (field.isAnnotationPresent(IsNotBlank.class)) {
validIsNotBlank(field, strVal);
}
if (field.isAnnotationPresent(RegExp.class)) {
validRegExp(field, strVal);
}
/** add... **/
}
/**
* 比對正則
*
* @param field
* @param strVal
*/
private static void validRegExp(Field field, String strVal) {
RegExp regExp = field.getAnnotation(RegExp.class);
if (Strings.isNotBlank(regExp.pattern())) {
if (Pattern.matches(regExp.pattern(), strVal)) {
return;
}
String des = regExp.des();
if (Strings.isBlank(des)) {
des = field.getName() + "格式不正确";
}
throw new IllegalArgumentException(des);
}
}
/**
* 非空判斷
*
* @param field
* @param val
*/
private static void validIsNotBlank(Field field, String val) {
IsNotBlank isNotBlank = field.getAnnotation(IsNotBlank.class);
if (val == null || Strings.isBlank(val)) {
String des = isNotBlank.des();
if (Strings.isBlank(des)) {
des = field.getName() + "不能為空";
}
throw new IllegalArgumentException(des);
}
}
}
有了具體驗證方法,我們需要個測試執行個體,如下測試接口和實體:
public class TestRq extends BaseRq implements Serializable {
@IsNotBlank(des = "昵稱不能為空")
private String nickName;
@RegExp(pattern = "\\d{10,20}", des = "編号必須是數字")
private String number;
private String des;
private String remark;
}
@PostMapping("/send")
public BaseRp<TestRp> send(@RequestBody TestRq rq) throws IllegalAccessException {
ValidateUtils.validate(rq);
return testService.sendTestMsg(rq);
}

上面是圍繞公用驗證方法來寫的,通常實際場景中都把它和aop結合來做統一驗證;來定制兩個注解,MethodValid方法注解(是否驗證所有參數)和ParamValid參數注解(标記方法上的某個參數):
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface MethodValid {
/**
* 驗證所有參數
*
* @return true
*/
boolean isValidParams() default true;
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.PARAMETER})
public @interface ParamValid {
}
有了兩個标記注解再來建立aop,我這裡是基于springboot架構的執行個體,所有引入如下mvn:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然後aop需要做如下邏輯:
- 擷取方法上傳遞參數(param1,param2...)
- 周遊每個參數實體,如有驗證注解就做校驗
- 周遊标記有ParamValid注解的參數,如有驗證注解就做校驗
這裡特殊的地方是,想要擷取方法參數對應的注解,需要method.getParameterAnnotations()擷取所有所有參數注解後,再用索引來取參數對應的注解;如下aop代碼:
package com.shenniu003.common.validates;
import com.shenniu003.common.validates.annotation.MethodValid;
import com.shenniu003.common.validates.annotation.ParamValid;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* des:
*
* @author: shenniu003
* @date: 2019/12/01 11:04
*/
@Aspect
@Component
public class ParamAspect {
@Around(value = "@annotation(methodValid)", argNames = "joinPoint,methodValid")
public Object validMethod(ProceedingJoinPoint joinPoint, MethodValid methodValid) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
System.out.println("method:" + method.getName());
String strArgs = Arrays.toString(joinPoint.getArgs());
System.out.println("params:" + strArgs);
//擷取方法所有參數的注解
Annotation[][] parametersAnnotations = method.getParameterAnnotations();
for (int i = 0; i < joinPoint.getArgs().length; i++) {
Object arg = joinPoint.getArgs()[i];
if (arg == null) {
continue; //
}
if (methodValid.isValidParams()) {
//驗證所有參數
System.out.println(arg.getClass().getName() + ":" + arg.toString());
ValidateUtils.validate(arg);
} else {
//隻驗證參數前帶有ParamValid注解的參數
//擷取目前參數所有注解
Annotation[] parameterAnnotations = parametersAnnotations[i];
//是否比對參數校驗注解
if (matchParamAnnotation(parameterAnnotations)) {
System.out.println(Arrays.toString(parameterAnnotations) + " " + arg.getClass().getName() + ":" + arg.toString());
ValidateUtils.validate(arg);
}
}
}
return joinPoint.proceed();
}
/**
* 是否比對參數的注解
*
* @param parameterAnnotations 參數對應的所有注解
* @return 是否包含目标注解
*/
private boolean matchParamAnnotation(Annotation[] parameterAnnotations) {
boolean isMatch = false;
for (Annotation parameterAnnotation : parameterAnnotations) {
if (ParamValid.class == parameterAnnotation.annotationType()) {
isMatch = true;
break;
}
}
return isMatch;
}
}
這裡編寫3中方式的測試用例,驗證方法所有參數、無參數不驗證、驗證方法參數帶有@ParamValid的參數,以此達到不同需求參數的校驗方式:
//驗證方法所有參數
@MethodValid
public void x(TestRq param1, String param2) {
}
//無參數不驗證
@MethodValid
public void xx() {
}
//驗證方法參數帶有@ParamValid的參數
@MethodValid(isValidParams = false)
public void xxx(TestRq param1, @ParamValid String param2) {
}
同樣用send接口作為測試入口,調用上面3種方法:
@PostMapping("/send")
@MethodValid
public BaseRp<TestRp> send(@RequestBody TestRq rq) throws IllegalAccessException {
// ValidateUtils.validate(rq);
testController.x(rq, "驗證方法所有參數");
testController.xx();
testController.xxx(rq, "驗證方法參數帶有@ParamValid的參數");
return testService.sendTestMsg(rq);
}