AOP+自定義注解實現全局參數校驗


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的硬編碼問題。想到了一種解決思路,稍后可以嘗試一下。

思路

  1. 創建一個注解加在方法的參數上,其可以指定一個或一組Entity類的全路徑。
  2. 在Aspect中通過獲取此注解獲取所有Entity。
  3. 再使用反射來獲取這些Entity中加入注解的屬性。
  4. 通過屬性名(匹配key)屬性上注解的實例(匹配校驗規則)
  5. 從而實現全局值校驗。

對於普通類型(包括包裝類型)的優化

對於普通類型(包括包裝類型),可以編寫一些單獨的校驗注解。當參數上增加了這些注解,就進行相關的校驗。

對於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再進行反射判斷


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM