AOP+自定義注解實現全局參數校驗
在開發過程中,用戶傳遞的數據不一定合法,雖然可以通過前端進行一些校驗,但是為了確保程序的安全性,保證數據的合法,在后台進行數據校驗也是十分必要的。
后台的參數校驗
在controller方法中校驗:
后台的參數是通過controller方法獲取的,所以最簡單的參數校驗的方法,就是在controller方法中進行參數校驗。在controller方法中如果進行參數校驗會有大量重復、沒有太大意義的代碼。
使用攔截器、過濾器校驗
為了保證controller中的代碼有更好的可讀性,可以將參數校驗的工作交由攔截器(Interceptor)或者過濾器(Filter)來完成,但是此時又存在一個問題:非共性的參數需要每個方法都創建一個與之對應的攔截器(或者過濾器)。
實現對Entity的統一校驗
鑒於上述解決方案的缺點,我們可以借助AOP的思想來進行統一的參數校驗。思想是通過自定義注解來完成對實體類屬性的標注,在AOP中掃描加了自定義注解的屬性,對其進行注解屬性標注的校驗。對於不滿足的參數直接拋出自定義異常,交由全局異常處理來處理並返回友好的提示信息。
在介紹此方法之前,我們先來介紹一下使用其會用到的一些內容。
自定義異常
在開發過程中,經常需要拋出一些異常,但是異常中沒有狀態碼,自定義描述等屬性。所以可以自定義一個異常。拋出異常時,使用全局異常處理,通過全局異常來處理此異常。
注意:Aspect中的異常只有RuntimeException(及其子類)能被全局異常處理。
所以我們通常將自定義異常定義為運行時異常。
package cn.rayfoo.common.exception;
import lombok.*;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/20 9:26 下午
* @Description: 自定義的異常...
*/
@Getter@Setter@Builder@NoArgsConstructor@AllArgsConstructor
public class MyException extends RuntimeException{
private int code;
private String msg;
}
斷言類
在代碼的執行過程中,我們經常需要在特定條件下(一般為是否滿足某條件)拋出異常,此時需要加入拋異常、返回狀態碼、錯誤信息、記錄日志等操作,此操作是大量重復的操作,所以借助Junit中Assert的思想,創建了下述的斷言工具類,用於在指定條件下拋出一個自定義異常。
package cn.rayfoo.common.exception;
import cn.rayfoo.common.response.HttpStatus;
import lombok.extern.slf4j.Slf4j;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>斷言類</p>
* @date 2020/8/7 9:43
*/
@Slf4j
public class MyAssert {
/**
* 如果為空直接拋出異常 類似於斷言的思想
* @param status 當status為false 就會拋出異常 不繼續執行后續語句
* @param msg 異常描述
*/
public static void assertMethod(boolean status, String msg) throws Exception {
//為false拋出異常
if (!status) {
//記錄錯誤信息
log.error(msg);
//拋出異常
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(msg).build();
}
}
/**
* 如果為空直接拋出異常 類似於斷言的思想
* @param status 當status為false 就會拋出異常 不繼續執行后續語句
* @param code 狀態碼
* @param msg 異常描述
*/
public static void assertMethod(boolean status,Integer code, String msg) throws Exception {
//為false拋出異常
if (!status) {
//記錄錯誤信息
log.error(msg);
//拋出異常
throw MyException.builder().code(code).msg(msg).build();
}
}
/**
* 如果為空直接拋出異常 類似於斷言的思想
* @param status 當status為false 就會拋出異常 不繼續執行后續語句
*/
public static void assertMethod(boolean status) throws Exception {
//為false拋出異常
if (!status) {
//記錄錯誤信息
log.error(HttpStatus.INTERNAL_SERVER_ERROR.name());
//拋出異常
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(HttpStatus.INTERNAL_SERVER_ERROR.name()).build();
}
}
}
當調用斷言方法時,只要傳遞一個boolean表達式,當表達式為false,就會拋出一個異常,提前結束方法。這個異常,通常由全局異常處理類來攔截。
全局異常處理攔截斷言拋出的方法
package cn.rayfoo.common.exception;
import cn.rayfoo.common.response.HttpStatus;
import cn.rayfoo.common.response.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.FileNotFoundException;
/**
* @author rayfoo@qq.com
* @version 1.0
* @date 2020/8/5 14:55
* @description 全局異常處理
*/
@ControllerAdvice@Slf4j
public class ServerExceptionResolver {
/**
* 對某種異常進行處理,如果非前后端分離的項目此處可以使用ModelAndView 返回錯誤頁面
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)@ResponseBody
public Result<String> resolveException(Exception ex) {
//打印完整的異常信息
ex.printStackTrace();
//創建result
Result<String> result = new Result<>();
//設置result屬性
result.setData(ex.getMessage());
result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
//判斷異常類型
if(ex instanceof FileNotFoundException){
log.error("文件問找到異常。。。");
// TODO 自定義一個Status
result.setMsg("文件未找到,請檢查文件是否存在!");
}
else if(ex instanceof RuntimeException){
log.error("服務器內部發生了異常");
result.setMsg(HttpStatus.INTERNAL_SERVER_ERROR.name());
}else{
log.error("服務器內部發生了異常");
result.setMsg(HttpStatus.INTERNAL_SERVER_ERROR.name());
}
//.....
return result;
}
/**
* 處理自定義的異常
* @param ex
* @return
*/
@ExceptionHandler(MyException.class)@ResponseBody
public Result<String> resolveMyException(MyException ex){
//打印完整的異常信息
ex.printStackTrace();
//創建result
Result<String> result = new Result<>();
//設置result屬性
result.setData(ex.getMsg());
result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
result.setMsg(ex.getMsg());
//保存自定義異常日志
log.error(ex.getMsg());
return result;
}
}
定義Verify注解
准備好上面的內容,我們就可以使用自定義注解+Aspect來完成全局的參數校驗了~
此注解用於注解實體類的屬性,這個注解中,創建了以下幾個屬性:
- name:用於描述修飾的字段,當校驗失敗時,提示用戶字段的具體名稱。
- maxLength:最大的長度,對字符串長度進行校驗,如果是默認值代表不進行長度校驗
- minLength:最小的長度,同樣進行字符串長度的校驗,如果是默認值代表不進行長度校驗
- required:是否是必填屬性,即進行非空判斷
- notNull:進行非空和非空串的判斷
- regular:指定用於校驗的正則表達式,如果為RegexOption.DEFAULT表示不進行正則校驗
package cn.rayfoo.common.annotation;
import cn.rayfoo.common.enums.RegexOption;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:33
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface Verify {
/** 參數名稱 */
String name();
/** 參數最大長度 */
int maxLength() default Integer.MAX_VALUE;
/** 是否必填 這里只是判斷是否為null */
boolean required() default true;
/** 是否為非空 是否為null和空串都判斷 */
boolean notNull() default true;
/** 最小長度 */
int minLength() default Integer.MIN_VALUE;
/** 正則匹配 */
RegexOption regular() default RegexOption.DEFAULT;
}
上面的自定義注解中使用到了RegexOption枚舉,此注解只寫了常見的正則校驗方法,如果需要拓展可以自定添加,下面是此枚舉的代碼:
package cn.rayfoo.common.enums;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:51
*/
public enum RegexOption {
/**
* 缺省,表示不進行正則校驗
*/
DEFAULT(""),
/**
* 郵箱正則
*/
EMAIL_REGEX("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$"),
/**
* 手機號正則
*/
PHONE_NUMBER_REGEX("^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\\d{8}$"),
/**
* 身份證正則
*/
IDENTITY_CARD_REGEX("(^\\d{18}$)|(^\\d{15}$)"),
/**
* URL正則
*/
URL_REGEX("http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?"),
/**
* IP地址正則
*/
IP_ADDR_REGEX("(25[0-5]|2[0-4]\\d|[0-1]\\d{2}|[1-9]?\\d)"),
/**
* 用戶名正則
*/
USERNAME_REGEX("^[a-zA-Z]\\w{5,20}$"),
/**
* 密碼正則
*/
PASSWORD_REGEX("^[a-zA-Z0-9]{6,20}$");
/**
* 正則
*/
private String regex;
/**
* 構造方法
*
* @param regex
*/
private RegexOption(String regex) {
this.regex = regex;
}
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}
使用Aspect進行全局參數校驗
前面的准備工作做好,就可以進行全局的參數校驗了~
package cn.rayfoo.common.aspect;
import cn.rayfoo.common.annotation.Verify;
import cn.rayfoo.common.enums.RegexOption;
import cn.rayfoo.common.exception.MyAssert;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>Controller中的全局參數校驗</p>
* @date 2020/8/7 14:03
*/
@Aspect
//@Component
@Slf4j
public class EntityValidatorAspect {
/**
* 定義一個方法,用於聲明切入表達式。
*/
@Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
public void validatorPointcut() {
}
@Before("validatorPointcut()")
public void parameterVerify(JoinPoint point) throws Exception {
//迭代所有參數
for (int i = 0; i < point.getArgs().length; i++) {
//切點對象
Object obj = point.getArgs()[i];
Class clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
//需要做校驗的參數
if (field.isAnnotationPresent(Verify.class)) {
//獲取注解對象
Verify verify = field.getAnnotation(Verify.class);
//取出注解的屬性
String name = verify.name();
int maxLength = verify.maxLength();
int minLength = verify.minLength();
boolean required = verify.required();
boolean notNull = verify.notNull();
RegexOption regular = verify.regular();
//屬性值
Object fieldObj = field.get(obj);
//是否時必傳 斷言判斷
if (required) {
MyAssert.assertMethod(fieldObj != null, String.format("【%s】為必傳參數", name));
}
//字符串的 非空校驗
if (notNull) {
MyAssert.assertMethod(!StringUtils.isEmpty(fieldObj), String.format("【%s】不能為空", name));
}
//是否有最大長度限制 斷言判斷
if (Integer.MAX_VALUE != maxLength) {
MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最大長度為【%s】", name, maxLength));
}
//是否有最小長度限制 斷言判斷
if (Integer.MIN_VALUE != minLength) {
MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最小長度為【%s】", name, minLength));
}
//是否有正則校驗
if (!"".equals(regular.getRegex())) {
Pattern pattern = Pattern.compile(regular.getRegex());
//斷言判斷正則
MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("參數【%s】的請求數據不符合規則", name));
}
}
}
}
}
}
上述的校驗適用於Controller方法中參數為自定義的實體類,但是對於Map類型、普通類型(包括包裝類型)的參數還無法完成校驗。后續可以考慮增加對自定義注解的拓展,即可以允許加在方法參數上。
對於Map類型的參數進行校驗
上述的校驗完成后,又發現了一個問題:如果Controller方法的參數是Map類型,如何完成參數的校驗?
經過一番思考,結合上面案例的解決方案,最終也實現了對map的校驗,但是要求比較嚴苛:由於其原理是通過key來匹配校驗規則,所以map中的key,必須是后端指定的key才能自動完成校驗。
下面介紹以下具體的實現方法:
創建校驗枚舉
這個枚舉是不是很眼熟呀,沒錯 就是基於上面的注解編寫的,增加了一個key屬性。通過key屬性可以判斷map中指定的key進行何種正則校驗。
package cn.rayfoo.common.enums;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:51
*/
public enum JSONRegexOption {
/**
* 缺省,表示不進行正則校驗
*/
DEFAULT("",""),
/**
* 郵箱正則
*/
EMAIL_REGEX("email","^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$"),
/**
* 手機號正則
*/
PHONE_NUMBER_REGEX("phoneNumber","^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\\d{8}$"),
/**
* 身份證正則
*/
IDENTITY_CARD_REGEX("identityCard","(^\\d{18}$)|(^\\d{15}$)"),
/**
* URL正則
*/
URL_REGEX("url","http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?"),
/**
* IP地址正則
*/
IP_ADDR_REGEX("ipAddr","(25[0-5]|2[0-4]\\d|[0-1]\\d{2}|[1-9]?\\d)"),
/**
* 用戶名正則
*/
USERNAME_REGEX("username","^[a-zA-Z]\\w{5,20}$"),
/**
* 密碼正則
*/
PASSWORD_REGEX("password","^[a-zA-Z0-9]{6,20}$");
/**
* JSON的key
*/
private String key;
/**
* 正則
*/
private String regex;
/**
* 構造方法
*
* @param regex
*/
private JSONRegexOption(String key,String regex) {
this.key = key;
this.regex = regex;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}
在Aspect中進行全局校驗
經過反復的斷點測試發現,Map類型的參數在JoinPoint中獲取時是通過java.util.LinkedHashMap類型來接受的。所以我們可以通過判斷參數的類型來判斷當前參數是否為map,如果為Map通過遍歷Map的key來實現全局的校驗:
package cn.rayfoo.common.aspect;
import cn.rayfoo.common.enums.JSONRegexOption;
import cn.rayfoo.common.exception.MyAssert;
import cn.rayfoo.common.exception.MyException;
import cn.rayfoo.common.response.HttpStatus;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>Controller中的JSON全局參數校驗</p>
* @date 2020/8/7 14:03
*/
@Aspect
@Component
@Slf4j
public class JsonValidatorAspect {
/**
* 定義一個方法,用於聲明切入表達式。
*/
@Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
public void validatorPointcut() {
}
@Before("validatorPointcut()")
public void parameterVerify(JoinPoint point) throws Exception {
//迭代所有參數
for (int i = 0; i < point.getArgs().length; i++) {
//切點對象
Object obj = point.getArgs()[i];
//將數據轉換為json
Class clazz = obj.getClass();
//如果是map接收參數
if ("java.util.LinkedHashMap".equals(clazz.getName())) {
//獲取集合
LinkedHashMap map = (LinkedHashMap) obj;
//獲取key列表
Set set = map.keySet();
//迭代key
for (Object key : set) {
//如果有空值 或者空字符串
if (StringUtils.isEmpty(map.get(key))) {
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg("數據中存在空值!").build();
}
//用戶名校驗
valueValidate(JSONRegexOption.USERNAME_REGEX, map, key, "您輸入的用戶名不符合規范");
//密碼校驗
valueValidate(JSONRegexOption.PASSWORD_REGEX, map, key, "您輸入的密碼不符合規范");
//郵箱校驗
valueValidate(JSONRegexOption.EMAIL_REGEX, map, key, "您輸入的郵箱不符合規范");
//手機號校驗
valueValidate(JSONRegexOption.PHONE_NUMBER_REGEX, map, key, "您輸入的手機號不符合規范");
//身份證號校驗
valueValidate(JSONRegexOption.IDENTITY_CARD_REGEX, map, key, "您輸入的身份證號不符合規范");
//ip校驗
valueValidate(JSONRegexOption.IP_ADDR_REGEX, map, key, "您輸入的IP不符合規范");
//url校驗
valueValidate(JSONRegexOption.URL_REGEX, map, key, "您輸入的URL不符合規范");
}
}
}
}
/**
* 正則校驗
*
* @param regex 正則
* @param param 需要校驗的值
* @return 校驗結果
*/
public boolean regexValidate(String regex, String param) {
Pattern pattern = Pattern.compile(regex);
return param.matches(regex);
}
/**
* @param regexOption 校驗類型
* @param map 數據集
* @param key 校驗的key
* @param msg 如果出錯返回的信息
*/
public void valueValidate(JSONRegexOption regexOption, LinkedHashMap map, Object key, String msg) throws Exception {
//密碼校驗
if (regexOption.getKey().equals(key.toString())) {
//根據key獲取值
String value = map.get(key).toString();
//值校驗
MyAssert.assertMethod(regexValidate(regexOption.getRegex(), value), msg);
}
}
}
對Map類型參數校驗的優化
對於Map類型參數的校驗還有優化的辦法,能夠解決key的硬編碼問題。想到了一種解決思路,稍后可以嘗試一下。
思路
- 創建一個注解加在方法的參數上,其可以指定一個或一組Entity類的全路徑。
- 在Aspect中通過獲取此注解獲取所有Entity。
- 再使用反射來獲取這些Entity中加入注解的屬性。
- 通過屬性名(匹配key)屬性上注解的實例(匹配校驗規則)
- 從而實現全局值校驗。
對於普通類型(包括包裝類型)的優化
對於普通類型(包括包裝類型),可以編寫一些單獨的校驗注解。當參數上增加了這些注解,就進行相關的校驗。
對於List、Set、List
經過上面的一些解決方案,其實寫出這樣的校驗已經不是什么難題,只需要在Aspect中進行相關的判斷即可,具體的實現大家可以多嘗試哈~~
有什么更好的解決方案歡迎留言一起交流
來自一小時后的更新。。。。
完善Map類型的校驗~
對於上述的想法立馬進行了實踐,完善了對Map類型參數的校驗,再說一遍思路:
首先要在map參數前加上一個自定義注解,此注解只有一個屬性,用於聲明此map中要校驗的數據來自哪些實體類。(實體類需要指定全類名,因為要對其進行反射)
package cn.rayfoo.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/8 19:50
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface VerifyEntity {
/**
* 實體類全類名列表
*/
String[] baseEntityList();
}
在方法上加上注解:
@PutMapping("/updatePhone")
public Result<Object> updatePhone(@RequestBody @VerifyEntity(baseEntityList = {"cn.rayfoo.modules.base.entity.User"}) Map<String, Object> record) {
return null;
}
校驗Aspect代碼:
package cn.rayfoo.common.aspect;
import cn.hutool.core.util.ArrayUtil;
import cn.rayfoo.common.annotation.Verify;
import cn.rayfoo.common.annotation.VerifyEntity;
import cn.rayfoo.common.exception.MyAssert;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>Controller中的JSON全局參數校驗</p>
* @date 2020/8/7 14:03
*/
@Aspect
@Component
@Slf4j
public class JsonValidatorAspectPlus {
/**
* 校驗的類型
*/
private static final String LINK_HASH_MAP_TYPE = "java.util.LinkedHashMap";
/**
* 定義一個方法,用於聲明切入表達式。
*/
@Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
public void validatorPointcut() {
}
@Before("validatorPointcut()")
public void parameterVerify(JoinPoint point) throws Exception {
//獲取參數列表
Object[] args = point.getArgs();
//通過簽名 獲取方法簽名
MethodSignature signature = (MethodSignature) point.getSignature();
//通過方法簽名獲取執行方法
Method method = signature.getMethod();
//獲取參數上的所有注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//獲取參數列表
Parameter[] parameters = method.getParameters();
//拆分方法,提高閱讀性
isVerifyEntity(parameterAnnotations, args);
}
/**
* 判斷是否加了@VerifyEntity注解 加了再進行下一步的操作
* @param parameterAnnotations 所有參數前的注解列表
* @param args 所有的參數列表
*/
private void isVerifyEntity(Annotation[][] parameterAnnotations, Object[] args) throws Exception {
//判斷是否加了VerifyEntity注解
for (Annotation[] parameterAnnotation : parameterAnnotations) {
//獲取當前參數的位置
int index = ArrayUtil.indexOf(parameterAnnotations, parameterAnnotation);
for (Annotation annotation : parameterAnnotation) {
//獲取注解的全類名
String verifyEntityName = VerifyEntity.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (verifyEntityName.equals(name)) {
//獲取此注解修飾的具體的參數
Object param = args[index];
//如果存在此注解,執行方法
isLinkedHashMap(annotation,param);
}
}
}
}
/**
* 判斷是否為LinkedHashMap,如果是,進行進一步的操作
* @param annotation 參數上的注解
* @param param 注解所修飾的參數
*/
private void isLinkedHashMap(Annotation annotation,Object param) throws Exception {
//獲取注解
VerifyEntity verifyEntity = (VerifyEntity) annotation;
//獲取要校驗的所有entity
String[] entitys = verifyEntity.baseEntityList();
//如果是map接收參數
if (LINK_HASH_MAP_TYPE.equals(param.getClass().getName())) {
//如果存在Verify注解
hasVerify(entitys, param);
}
}
/**
* 如果EntityList中的實體存在Verify注解
* @param entityList 實體列表
* @param param 加入@verifyEntity的注解 的參數
*/
private void hasVerify(String[] entityList, Object param) throws Exception {
//迭代entityList
for (int i = 0; i < entityList.length; i++) {
Field[] fields = Class.forName(entityList[i]).getDeclaredFields();
//迭代字段
for (Field field : fields) {
//判斷是否加入了Verify注解
if (field.isAnnotationPresent(Verify.class)) {
//如果有 獲取注解的實例
Verify verify = field.getAnnotation(Verify.class);
//校驗
validateMap(param, verify, field.getName());
}
}
}
}
/**
* 真正進行校驗的類
* @param param 增加@VerifyEntity注解的參數
* @param verify Verify注解的實例
* @param fieldName 加了Verify的屬性name值
*/
public void validateMap(Object param, Verify verify, String fieldName) throws Exception {
//獲取集合
LinkedHashMap map = (LinkedHashMap) param;
//獲取key列表
Set set = map.keySet();
//迭代key
for (Object key : set) {
//如果key和注解的fieldName一致
if (fieldName.equals(key)) {
//當前值
Object fieldObj = map.get(key);
//獲取verify的name
String name = verify.name();
//是否時必傳 斷言判斷
if (verify.required()) {
MyAssert.assertMethod(fieldObj != null, String.format("【%s】為必傳參數", name));
}
//字符串的 非空校驗
if (verify.notNull()) {
MyAssert.assertMethod(!StringUtils.isEmpty(fieldObj), String.format("【%s】不能為空", name));
}
//是否有最大長度限制 斷言判斷
int maxLength = verify.maxLength();
if (Integer.MAX_VALUE != maxLength) {
MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最大長度為【%s】", name, maxLength));
}
//是否有最小長度限制 斷言判斷
int minLength = verify.minLength();
if (Integer.MIN_VALUE != minLength) {
MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最小長度為【%s】", name, minLength));
}
//是否有正則校驗
if (!"".equals(verify.regular().getRegex())) {
//初始化Pattern
Pattern pattern = Pattern.compile(verify.regular().getRegex());
//斷言判斷正則
MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("參數【%s】的請求數據不符合規則", name));
}
}
}
}
}
此時,解決了Map和Entity兩種常見參數的統一校驗~
已經解決了常見的參數校驗啦~
再次更新,完成普通類型、map、eitity三種校驗的整合
-
增強了@verify對於普通類型參數的支持
-
增加了@RequestEntity、@RequestMap注解
-
可以實現對Map、Entity、普通類型(包括包裝類型)的全局校驗
-
對原有的多個Aspect進行了整合,JSONRegexOption、EntityValidatorAspect、JsonValidatorAspect都可以Deprecated了
-
具有一定的拓展性,如需增加校驗規則,只需要拓展RegexOption即可
廢話不多說,直接上代碼:
適用於普通參數和屬性的檢驗注解:
package cn.rayfoo.common.annotation;
import cn.rayfoo.common.enums.RegexOption;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:33
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface Verify {
/** 參數名稱 */
String name();
/** 參數最大長度 */
int maxLength() default Integer.MAX_VALUE;
/** 是否必填 這里只是判斷是否為null */
boolean required() default true;
/** 是否為非空 是否為null和空串都判斷 */
boolean notNull() default true;
/** 最小長度 */
int minLength() default Integer.MIN_VALUE;
/** 正則匹配 */
RegexOption regular() default RegexOption.DEFAULT;
}
適用於Controller參數中的Map類型:
package cn.rayfoo.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>對Map</p>
* @date 2020/8/8 19:50
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestMap {
/**
* 實體類全類名列表
*/
String[] baseEntityList();
}
適用於Controller方法中的Entity參數:
package cn.rayfoo.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/8 22:43
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestEntity {
String value() default "";
}
適用於@Verify注解的枚舉,如果需要新增校驗,可以對此枚舉進行拓展:
package cn.rayfoo.common.enums;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:51
*/
public enum RegexOption {
/**
* 缺省,表示不進行正則校驗
*/
DEFAULT(""),
/**
* 郵箱正則
*/
EMAIL_REGEX("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$"),
/**
* 手機號正則
*/
PHONE_NUMBER_REGEX("^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\\d{8}$"),
/**
* 身份證正則
*/
IDENTITY_CARD_REGEX("(^\\d{18}$)|(^\\d{15}$)"),
/**
* URL正則
*/
URL_REGEX("http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?"),
/**
* IP地址正則
*/
IP_ADDR_REGEX("(25[0-5]|2[0-4]\\d|[0-1]\\d{2}|[1-9]?\\d)"),
/**
* 用戶名正則
*/
USERNAME_REGEX("^[a-zA-Z]\\w{5,20}$"),
/**
* 密碼正則
*/
PASSWORD_REGEX("^[a-zA-Z0-9]{6,20}$");
/**
* 正則
*/
private String regex;
/**
* 構造方法
*
* @param regex
*/
private RegexOption(String regex) {
this.regex = regex;
}
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}
Aspect:
package cn.rayfoo.common.aspect;
import cn.hutool.core.util.ArrayUtil;
import cn.rayfoo.common.annotation.RequestEntity;
import cn.rayfoo.common.annotation.RequestMap;
import cn.rayfoo.common.annotation.Verify;
import cn.rayfoo.common.exception.MyAssert;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>Controller中的JSON全局參數校驗</p>
* @date 2020/8/7 14:03
*/
@Aspect
@Component
@Slf4j
public class JsonValidatorAspectPlus {
/**
* 校驗的類型
*/
private static final String LINK_HASH_MAP_TYPE = "java.util.LinkedHashMap";
/**
* 定義一個方法,用於聲明切入表達式。
*/
@Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
public void validatorPointcut() {
}
@Before("validatorPointcut()")
public void parameterVerify(JoinPoint point) throws Exception {
//通過簽名 獲取方法簽名
MethodSignature signature = (MethodSignature) point.getSignature();
//通過方法簽名獲取執行方法
Method method = signature.getMethod();
//獲取參數上的所有注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//獲取參數列表
Object[] args = point.getArgs();
//判斷是否加了RequestMap注解
for (Annotation[] parameterAnnotation : parameterAnnotations) {
//獲取當前參數的位置
int index = ArrayUtil.indexOf(parameterAnnotations, parameterAnnotation);
for (Annotation annotation : parameterAnnotation) {
//獲取此注解修飾的具體的參數
Object param = args[index];
//如果有@RequestEntity注解
hasRequestEntity(annotation, param);
//如果有Verify注解 由於是參數上的注解 注意:此處傳遞的是具體的param 而非args
hasVerify(annotation, param);
//如果有RequestMap注解 由於是參數上的注解 注意:此處傳遞的是具體的param 而非args
hasRequestMap(annotation, param);
}
// }
}
}
/**
* 如果參數存在RequestEntity注解
*
* @param annotation 參數上的注解
* @param param 具體的參數
*/
private void hasRequestEntity(Annotation annotation, Object param) throws Exception {
//獲取注解的全類名
String requestEntityName = RequestEntity.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (requestEntityName.equals(name)) {
//獲取參數的字節碼
Class clazz = param.getClass();
//獲取當前參數對應類型的所有屬性
Field[] fields = clazz.getDeclaredFields();
//遍歷屬性
for (Field field : fields) {
//獲取私有屬性值
field.setAccessible(true);
//需要做校驗的參數
if (field.isAnnotationPresent(Verify.class)) {
//獲取注解對象
Verify verify = field.getAnnotation(Verify.class);
//校驗的對象
Object fieldObj = field.get(param);
//校驗
validate(verify, fieldObj);
}
}
}
}
/**
* 如果參數上加的是Verify注解
*
* @param annotation 參數上的注解
* @param param 參數
*/
private void hasVerify(Annotation annotation, Object param) throws Exception {
//獲取注解的全類名
String verifyName = Verify.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (verifyName.equals(name)) {
//獲取此注解修飾的具體的參數
//獲取當前注解的具值
Verify verify = (Verify) annotation;
//進行校驗
validate(verify, param);
}
}
/**
* 判斷是否加了@RequestMap注解 加了再進行下一步的操作
*
* @param annotation 所有參數前的注解
* @param param 當前參數
*/
private void hasRequestMap(Annotation annotation, Object param) throws Exception {
//獲取注解的全類名
String RequestMapName = RequestMap.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (RequestMapName.equals(name)) {
//如果存在此注解,執行方法
isLinkedHashMap(annotation, param);
}
}
/**
* 判斷是否為LinkedHashMap,如果是,進行進一步的操作
*
* @param annotation 參數上的注解
* @param param 注解所修飾的參數
*/
private void isLinkedHashMap(Annotation annotation, Object param) throws Exception {
//獲取注解
RequestMap RequestMap = (RequestMap) annotation;
//獲取要校驗的所有entity
String[] entitys = RequestMap.baseEntityList();
//如果是map接收參數
if (LINK_HASH_MAP_TYPE.equals(param.getClass().getName())) {
//如果存在Verify注解
hasVerify(entitys, param);
}
}
/**
* 如果EntityList中的實體存在Verify注解
*
* @param entityList 實體列表
* @param param 加入@RequestMap的注解 的參數
*/
private void hasVerify(String[] entityList, Object param) throws Exception {
//迭代entityList
for (int i = 0; i < entityList.length; i++) {
//獲取所有字段
Field[] fields = Class.forName(entityList[i]).getDeclaredFields();
//迭代字段
for (Field field : fields) {
field.setAccessible(true);
//判斷是否加入了Verify注解
if (field.isAnnotationPresent(Verify.class)) {
//如果有 獲取注解的實例
Verify verify = field.getAnnotation(Verify.class);
//校驗
fieldIsNeedValidate(param, verify, field.getName());
}
}
}
}
/**
* 字段是否需要校驗
*
* @param param 增加@RequestMap注解的參數
* @param verify Verify注解的實例
* @param fieldName 加了Verify的屬性name值
*/
private void fieldIsNeedValidate(Object param, Verify verify, String fieldName) throws Exception {
//獲取集合
LinkedHashMap map = (LinkedHashMap) param;
//獲取key列表
Set set = map.keySet();
//迭代key
for (Object key : set) {
//如果key和注解的fieldName一致
if (fieldName.equals(key)) {
//當前值
Object fieldObj = map.get(key);
//真正的進行校驗
validate(verify, fieldObj);
}
}
}
/**
* 正則的校驗方法
*
* @param verify 校驗規則
* @param fieldObj 校驗者
*/
private void validate(Verify verify, Object fieldObj) throws Exception {
//獲取verify的name
String name = verify.name();
//是否時必傳 斷言判斷
if (verify.required()) {
MyAssert.assertMethod(fieldObj != null, String.format("【%s】為必傳參數", name));
}
//字符串的 非空校驗
if (verify.notNull()) {
MyAssert.assertMethod(!StringUtils.isEmpty(fieldObj), String.format("【%s】不能為空", name));
}
//是否有最大長度限制 斷言判斷
int maxLength = verify.maxLength();
if (Integer.MAX_VALUE != maxLength) {
MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最大長度為【%s】", name, maxLength));
}
//是否有最小長度限制 斷言判斷
int minLength = verify.minLength();
if (Integer.MIN_VALUE != minLength) {
MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最小長度為【%s】", name, minLength));
}
//是否有正則校驗
if (!"".equals(verify.regular().getRegex())) {
//初始化Pattern
Pattern pattern = Pattern.compile(verify.regular().getRegex());
//斷言判斷正則
MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("參數【%s】的請求數據不符合規則", name));
}
}
}
在controller類中使用上述注解:
@PutMapping("/updatePhone")
public Result<Object> updatePhone(@RequestBody @RequestMap(baseEntityList = {"cn.rayfoo.modules.base.entity.User"}) Map<String, Object> record) {
return null;
}
@PostMapping("/test")
public Result<Object> test(@RequestBody @RequestEntity User user) {
return Result.builder().msg("ok").code(200).data("success").build();
}
@GetMapping("/username")
public Result<Object> usernameTest(@Verify(name = "用戶名",regular = RegexOption.USERNAME_REGEX) String username) {
return Result.builder().msg("ok").code(200).data("success").build();
}
對於組合Entity、List
這類的數據還需要繼續優化,目前已經有一些頭緒。后續可能還會更新
思路:
對於組合Entity可以在@Verify增加一個屬性 修飾是否該屬性是一個Entity,進行遞歸式判斷
對於List可以先迭代list,再在list中的每個Object再進行反射判斷