一個輕量級的引數校驗框架

wangchun_166發表於2017-12-07
一、引言
      大家還在為冗餘而又繁瑣的方法或者物件屬性的基礎性邊界校驗、個性化的邊界校驗煩惱嗎?下面介紹的這個小而美的框架【基礎&可定製化的引數校驗框架】能徹底解決大家的煩惱。作為一名經常寫業務程式碼的碼農,我們都遇到過類似的事情,每個業務方法或者RPC服務的引數是否為null、字串是否為empty、數字是否大於或小於設定的閥值或是否在某個特定的區間、集合列表是否為空、以及一些特定領域的約定校驗等; 想想以上的這些情形,我們可能在每個介面每個方法都需要寫重複的校驗判斷程式碼,冗餘的程式碼與業務邏輯耦合在一起,有沒有一種優雅的解決辦法,徹底分離這部分校驗程式碼,做到校驗邏輯與業務邏輯徹底解耦呢?答案就在下面。


二、框架原理
      在介紹框架的原理前,先說明一下,我們為什麼要設計這麼一個框架: 在電商體系下的業務系統目前大家基本都是採用中心化的RPC服務輸出業務能力,比如:商品這個業務,所有電商裡涉及到商品的業務能力基本由這個業務領域管理,最基本的有:商品查詢【單個/批量】、商品釋出/編輯、商品上下架、商品SKU管理、商品類目管理、商品庫存管理等,在做這些業務服務時就遇到大量的需要對服務介面引數的合法性、有效性、約定性的校驗工作,於是就有了這個動力,尋求一種更高抽象的解決辦法。

  1. 框架內部執行機制      一個輕量級的引數校驗框架

   2. 框架角色介紹 

   1)校驗規則:是框架裡最底層的校驗邏輯執行單元,基礎校驗、邊界校驗、個性化校驗都在這個模組裡完成,
              校驗規則有基礎的校驗規則(比如為空,為NULL等),也有個性化的校驗規則(比如針對不同
              業務的特殊校驗規則)大家只要實現一個校驗規則的介面,就可以寫自己的校驗規則。
             【稍後有程式碼例項】
   2)校驗入口:並不是所有的介面服務需要做校驗,校驗入口就是告訴框架那個服務介面或方法需要做引數資料
              校驗。
   3)校驗支架:連線校驗入口與校驗規則。告訴框架在那個入口的某個引數上要執行哪個校驗規則。他們三者的
              關係就比如烹飪一道菜,食材、菜譜、廚師之間的關係; 食材就好比校驗規則,菜譜就好比是
              校驗入口,廚師就是校驗支架他能按照菜譜,選擇適合的食材烹飪出一道美味的佳餚!
   他們之間的關係如下:複製程式碼

      一個輕量級的引數校驗框架

 3. 實現原理刨析

     1)我們先看一下校驗規則的實現原理,程式碼如下:

         所有校驗規則都需要實現 ParamCheck 這個介面,定義如下: 

/**
 * 校驗規則的定義
 * 主要有兩類:
 * 1、基本資料型別的為空為NULL等基本校驗,這些我已經寫好
 * 2、個性化自定義的校驗,比如批量查詢閥值校驗,這些往往有特定的場景和背景,只需要實現該介面
 */
public interface ParamCheck {

    /**
     * 所有需要校驗的邏輯類,都需要實現這個方法
     * @param t  待校驗的值
     * @param objectType 待校驗值的資料型別
     * @param c 自定義校驗時的規則內容
     * @return CheckResult 校驗結果&描述
     */
    CheckResult check(Object t, Class<?> objectType, String c) throws ServiceCheckException;

    /**
     * 檢驗規則的名稱,通過這個名稱來動態找到註解裡配置的校驗規則類
     * @return
     */
    String name();
}

這個介面定義了兩個方法:
check()方法就是所有校驗規則的基本校驗邏輯.
name()方法返回校驗規則的名稱,系統會初始化載入這些規則並置入本地記憶體裡,之後所有的校驗工作都會由這些
在記憶體中的校驗規則類來完成。


我們來分別刨析實現一個基礎的校驗規則類和一個個性化的校驗規則類,如下:

/**
 * 基礎校驗規則-物件是否為NUll的校驗,最基礎的校驗,每一個引數都需要檢驗這一步
 */
@CheckRule
public class ObjectCheck implements ParamCheck {
    /**
     * 所有需要校驗的邏輯類,都需要實現這個方法
     *
     * @param t          待校驗的值
     * @param objectType 待校驗值的資料型別
     * @param c          自定義校驗時的規則內容
     * @return CheckResult 校驗結果&描述
     */
    @Override
    public CheckResult check(Object t, Class<?> objectType, String c) {
        return CheckUtils.objectIsNullCheck(t);
    }

    /**
     * 檢驗規則的名稱,通過這個名稱來動態找到註解裡配置的校驗規則類
     *
     * @return
     */
    @Override
    public String name() {
        return Object.class.getName();
    }
}

/**
 * 個性化校驗規則-列表的個數是否超越設定的閥值
 */
@CheckRule
public class MaxSizeCheck implements ParamCheck {

    private final static Logger logger = LoggerFactory.getLogger(MaxSizeCheck.class);

    /**
     * 所有需要校驗的邏輯類,都需要實現這個方法
     *
     * @param t          待校驗的值
     * @param objectType 待校驗值的資料型別
     * @param c          自定義校驗時的規則內容
     * @return CheckResult 校驗結果&描述
     */
    @Override
    public CheckResult check(Object t, Class<?> objectType, String c) throws ServiceCheckException {

        CheckResult checkResult = new CheckResult(true);

        //如果校驗列表的個數是否超越設定的閥值,沒有傳閥值過來,預設通過
        if(StringUtils.isEmpty(c)){
            return checkResult;
        }

        Integer maxSize;
        try {
            /**
              * 這裡可以做優化,將所有個性化的校驗條件載入時都初始化好
              */
            JSONObject objectCondition = JSON.parseObject(c);
            maxSize = objectCondition.getInteger("maxSize");

            if (null == maxSize) {
                return checkResult;
            }
        }catch (Exception e){
            logger.error("MaxSizeCheck Error: msg="+c,e);
            throw new ServiceCheckException("MaxSizeCheck Error: msg=" + c + e.getMessage());
        }

        return CheckUtils.sizeGreaterThanFeedCheck(t,maxSize,objectType);
    }

    /**
     * 檢驗規則的名稱,通過這個名稱來動態找到註解裡配置的校驗規則類
     *
     * @return
     */
    @Override
    public String name() {
        return this.getClass().getSimpleName();
    }
}複製程式碼

 注意:每一個規則都需要加一個註解,系統就是通過這個註解來識別校驗規則,並自動載入到記憶體裡的。 

校驗規則註解程式碼如下:

/**
 * 校驗規則註解,每一個校驗規則類加一個這個註解,就可以通過
 * applicationContext.getBeansWithAnnotation() 完成初始化
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface CheckRule {
    String name() default ""; //校驗規則名稱(確保唯一)
}複製程式碼

以上程式碼,就是校驗規則的核心程式碼及執行機制,那麼校驗規則可以橫向的無限擴充套件,您只需要實現 ParamCheck 這個介面,並在規則前面使用@CheckRule 標註一下即可,系統就會自動把您的校驗規則載入到記憶體裡。那麼校驗規則有了,該如何使用呢?看如下的程式碼:

public class TagWriteServiceImpl implements TagWriteService {

    @Autowired
    private TagManager tagManager;

    /**
     * @param tagOption
     * @return
     * @ParameterCheck 這個註解是 介面的入參校驗 通過註解+AOP完成
     */
    @ParameterCheck(exceptionHandler = ServiceCheckFailedHanlder.class)
    public ResultDO<Boolean> tagAdd(tagWriteOption tagOption) {
        
        //todo 自己的業務邏輯
        (在做業務邏輯之前,系統其實已經通過ParameterCheck這個註解對 tagWriteOption這個物件&物件
        指定的屬性做了基礎的校驗工作)
        
        .......    
    }
}

使用刨析:

第一步:需要在待進行引數校驗的方法前面加 @ParameterCheck這個註解。
第二步:在@ParameterCheck註解裡指定一個校驗不通過的錯誤資訊返回物件,如上程式碼中的 ServiceCheckFailedHanlder
      類。這個類的具體實現如下:
 
public class ServiceCheckFailedHanlder implements ICheckFailedHandler {    
    /**
      * 框架本身是一個通用的框架,但總會有一些資訊是需要定製化的,比如不同的業務程式碼中有自己不同的錯誤提示包裝類等
      * 框架為了增加通用性和靈活性,這裡框架只定義了一個介面ICheckFailedHandler,
      * 具體的有業務方自己去實現      
      */
    @Override
    public Object validateFailed(String msg, Class returnType, Object... args) {
         
        //todo,這裡就可以寫自己業務的錯誤資訊封裝程式碼了
        BaseResultDO result = new BaseResultDO<>();
        result.setSuccess(false);
        result.setResultCode(BaseResultTypeEnum.PARAM_ERROR.getCode());
        result.setResultMessage(msg);
        return result;
    }
}

上面說到,在做業務邏輯之前,系統其實已經通過ParameterCheck這個註解對 tagWriteOption這個物件&物件
指定的屬性做了基礎的校驗工作,是如何做到的呢?

1、首先只要方法上有ParameterCheck這個註解,系統都會針對方法裡邊所有的引數進行基本的為NULL校驗。實現如下:
   系統通過AOP自動攔截有ParameterCheck註解的方法。AOP攔截實現如下:

@Component
@Aspect
@Order(1)
public class ParamCheckAop  extends AbsAopServiceParamterCheck {    

     private static Logger logger = LoggerFactory.getLogger("aopServiceParameterCheck");

    /**
     * 只要有ParameterCheck 這個註解的方法,都會被攔截
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("@annotation(parameterCheck)")
    public Object around(ProceedingJoinPoint pjp, ParameterCheck parameterCheck) throws Throwable {

        long startTime = System.currentTimeMillis();

        //執行校驗方法
        CheckResult checkSuccess = super.check(pjp, true);
        long annExceTime = System.currentTimeMillis() - startTime;
        if (logger.isDebugEnabled()) {
            logger.debug(pjp.getTarget().getClass().getSimpleName() + "|checkTime=" + annExceTime);
        }


        if (!checkSuccess.isSuccess()) {
            Method method = getMethod(pjp);
            ICheckFailedHandler handler = CheckFailedHandlerWrapper.getInstance()
                    .getCheckFailedHander(parameterCheck.exceptionHandler());

            return handler.validateFailed(checkSuccess.getMsg(),
                    method.getReturnType(),
                    pjp.getArgs());
        }


        return pjp.proceed();
    }

    private Method getMethod(JoinPoint pjp) {
        MethodSignature method = (MethodSignature) pjp.getSignature();
        return method.getMethod();
    }
}
 

AbsAopServiceParamterCheck是框架提供的一個抽象類,實現如下:

public abstract class AbsAopServiceParamterCheck {    
    private static Logger logger = LoggerFactory.getLogger(AbsAopServiceParamterCheck.class);

    @Resource
    private ServiceParameterCheck serviceParameterCheck;

    protected CheckResult check(ProceedingJoinPoint pjp, boolean isWriteLog) throws Throwable {

        Signature sig = pjp.getSignature();
        MethodSignature msig;
        if (!(sig instanceof MethodSignature)) {
            throw new IllegalArgumentException("該註解只能用於方法");
        }
        msig = (MethodSignature) sig;
        Object target = pjp.getTarget();
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());

        //當前方法是否ParameterCheck這個註解
        if(currentMethod.isAnnotationPresent(ParameterCheck.class)){

            //方法引數
            Object[] args = pjp.getArgs();
            Object[] params = new Object[args.length+2];
            params[0] = pjp.getTarget();         //類名全路徑
            params[1] = currentMethod.getName(); //方法名
            for(int i = 0;i<args.length;i++){
                params[i+2] = args[i];
            }

            //執行校驗方法-引數的基本校驗 + 自定義的校驗
            CheckResult checkBaseParamResult = serviceParameterCheck.checkMethod(params);
            if(!checkBaseParamResult.isSuccess()){
                logger.warn(pjp.getTarget().getClass().getSimpleName()+"."+currentMethod.getName()+"|checkSuccess=false"+"|param="+ JSON.toJSONString(args));
                return checkBaseParamResult;
            }

            //執行校驗方法-引數如果是自定義物件還需要校驗一下 物件裡的屬性是否有校驗規則

            CheckResult checkObjectParamResult = serviceParameterCheck.batchCheckObjecs(args);
            if(!checkObjectParamResult.isSuccess()){
                logger.warn(pjp.getTarget().getClass().getSimpleName()+"."+currentMethod.getName()+"|checkSuccess=false"+"|param="+JSON.toJSONString(args));
                return checkObjectParamResult;
            }

            if(isWriteLog){
                logger.warn("look i am here");
            }
        }
        return new CheckResult(true);
    }
}

基礎的校驗就是這一行程式碼:

CheckResult checkBaseParamResult = serviceParameterCheck.checkMethod(params);

程式碼如下:

@Service
public class ServiceParameterCheck implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    /**
     * 註冊的初始化物件列表
     */
    private Map<String,IAnnotationManager> annotationManagerMap = new HashMap<>();

    /**
     * 在初始化類的時候執行,將每一個註冊的物件的屬性快取起來
     */
    @PostConstruct
    protected void init() throws ServiceCheckException {

        Map<String,Object> objectMap = applicationContext.getBeansWithAnnotation(ServiceCheckPoint.class);

        /**  初始化-入參【物件級&方法級】的註解屬性 **/
        for(Object o : objectMap.values()){
            if(o instanceof IAnnotationManager){
                annotationManagerMap.put(((IAnnotationManager)o).getAnnotationCheckType().name(),((IAnnotationManager)o));
                ((IAnnotationManager)o).init();
            }
        }
    }

    /**
     * 根據方法上的入參做校驗
     * @param args
     * @return
     */
    public CheckResult checkMethod(Object ...args){
        return annotationManagerMap.get(AnnotationCheckType.METHOD.name()).check(args);
    }

    /**
     * 根據入參物件上的註解
     * @param o
     * @return
     */
    public CheckResult checkObject(Object o){
        return annotationManagerMap.get(AnnotationCheckType.OBJECT.name()).check(o);
    }

    /**
     * 根據入參物件上的註解批量校驗
     * @param objects
     * @return
     */
    public CheckResult batchCheckObjecs(Object[] objects){

        IAnnotationManager iAnnotationManager = annotationManagerMap.get(AnnotationCheckType.OBJECT.name());

        if(ArrayUtils.isEmpty(objects)){
            return new CheckResult(true);
        }

        for(Object o : objects){
            Class<?> objectType = o.getClass();
            if(objectType.getSimpleName().endsWith("List")){
                o = ((List)o).get(0);
            }else if(objectType.getSimpleName().endsWith("Map")){
                o = ((Map)o).values().toArray()[0];
            }else if(objectType.getSimpleName().endsWith("Set")){
                o = ((Set)o).toArray()[0];
            }else if(objectType.isArray()){
                o = Arrays.asList(o).get(0);
            }


            CheckResult checkRult = iAnnotationManager.check(o);
            if(!checkRult.isSuccess()){
                return checkRult;
            }
        }

        return new CheckResult(true);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}  

具體校驗在這裡:

/**
 * 針對服務介面的方法引數做校驗
 */
@ServiceCheckPoint
public class ServiceMethodAnnotationManager  implements IAnnotationManager,ApplicationContextAware {
    private final static Logger logger = LoggerFactory.getLogger(ServiceMethodAnnotationManager.class);

    /**
     * 每個服務對應的方法集合
     */
    private Map<String,Method> methodMap = new HashMap<>();

    /**
     * 每個服務對應的方法引數列表
     */
    private Map<String,List<Class<?>>> methodParamMap = new HashMap<>();

    /**
     * 前面兩位用作其他用途
     */
    private final static Integer reserveLen = 2;

    @Resource
    ParamCheckManager paramCheckManager;

    private ApplicationContext applicationContext;

    @Override
    @PostConstruct
    public void init() throws ServiceCheckException {

        Map<String,Object> objectMap = applicationContext.getBeansWithAnnotation(ServiceMethodCheck.class);

        try {
            for(Object o: objectMap.values()){
                Class<?> clazz = o.getClass().getSuperclass();

                //獲取方法列表,如果沒有註冊,直接跳出返回
                Method[] methods = clazz.getDeclaredMethods();
                if(ArrayUtils.isEmpty(methods)){
                    break;
                }

                for(Method method : methods){
                    //方法上是否有ParameterCheck這個註解
                    if(method.isAnnotationPresent(ParameterCheck.class)){
                        String key = clazz.getName() + "." + method.getName();

                        Class<?>[] parameterTypes = method.getParameterTypes();
                        if(!ArrayUtils.isEmpty(parameterTypes)) {
                            methodParamMap.put(key, Arrays.asList(parameterTypes));
                        }
                        methodMap.put(key,method);
                    }
                }
            }

            logger.warn(" ServiceMethodAnnotationManager init success ,methodMap:" + JSON.toJSONString(methodMap));

        }catch (Exception e){
            logger.error("ServiceMethodAnnotationManager error!",e);
            throw new ServiceCheckException("ServiceMethodAnnotationManager Init Error! " + e.getMessage());
        }
    }

    /**
     * 具體執行校驗的入口之一
     * @param args
     * @return  CheckResult 校驗結果 & 錯誤描述
     */
    @Override
    public CheckResult check(Object... args) {
        CheckResult checkResult = new CheckResult(true);

        /**引數列表為空,直接返回true,不做校驗**/
        if(ArrayUtils.isEmpty(args)){
            return checkResult;
        }

        /**引數長度必須大於兩個,第一個是介面服務類物件,第二個是呼叫的方法簽名,剩餘的是入參**/
        if(args.length < reserveLen){
            return checkResult;
        }

        Object[] objects = args;

        //第二個是呼叫的方法簽名
        String methodName = args[1].toString();
        //類名+方法名作為key
        String key = args[0].getClass().getName()+"."+methodName;

        /** 這個類+方法下的引數列表,methodParamMap在初始化的時候已經設定好了 **/
        List<Class<?>> paramTypeNameList = methodParamMap.get(key);

        /** 說明不需要檢驗 **/
        if(CollectionUtils.isEmpty(paramTypeNameList)){
            return checkResult;
        }

        //獲取對應key的方法物件
        Method method = methodMap.get(key);

        //獲取對應方法上的註解
        ParameterCheck annotation = method.getAnnotation(ParameterCheck.class);
        if(null == annotation){
            return checkResult;
        }

        //獲取方法引數裡對應的註解列表
        Map<Integer,Annotation> annotationAndParamIndexMap = getAnnotationAndParamIndex(method);

        /**
         * 迴圈校驗傳入的引數,為什麼要從2開始,
         * 因為第一個引數是服務物件。
         * 第二個引數是方法簽名。
         * 從第三個引數開始,才是方法的引數列表
         */
        try {
            for (int i = reserveLen; i < objects.length; i++) {
                int paramIndex = i-reserveLen;

                //欄位上有uncheck這個註解,說明不需要做檢驗,忽略
                if (isCheck(annotationAndParamIndexMap, paramIndex)) {
                    //如果引數上沒有自定義註解,那麼就以方法註解上的自定義註解為檢驗的規則
                    if(isHaveSelfCheck(annotationAndParamIndexMap, paramIndex)){
                        //引數上的自定義檢驗規則註解
                        SelfCheck paramAnnotation = (SelfCheck)annotationAndParamIndexMap.get(paramIndex);
                        checkResult = paramCheckManager.check(objects[i], paramTypeNameList.get(paramIndex),
                                Arrays.asList(paramAnnotation.check()),
                                paramAnnotation.condition(),
                                paramAnnotation.msg());
                    }else{
                        checkResult = paramCheckManager.check(objects[i], paramTypeNameList.get(paramIndex),
                                Arrays.asList(annotation.selfCheck()),
                                annotation.condition(),
                                annotation.msg());
                    }

                    if (!checkResult.isSuccess()) {
                        return checkResult;
                    }
                }
            }
        }catch (Exception e){
            /**如果檢驗裡邊發生了異常,預設通過校驗。可以往下走*/
            logger.error("ServiceMethodAnnotationManager error ,msg=",e);
        }

        return new CheckResult(true);
    }

    /**
     * 每個實現這個介面,都需要返回一個校驗的級別:方發入參級別
     * @return
     */
    @Override
    public AnnotationCheckType getAnnotationCheckType() {
        return AnnotationCheckType.METHOD;
    }

    /**
     * 獲取不需要檢查的引數的索引集合
     * @param method
     * @return
     */
    private Map<Integer,Annotation> getAnnotationAndParamIndex(Method method){
        Map<Integer,Annotation> annotationAndParamIndexMap = new HashMap<>();

        //獲取方法引數裡是否有 uncheck這個註解
        Annotation[][] annotations = method.getParameterAnnotations();
        if(!ArrayUtils.isEmpty(annotations)) {
            for (int i = 0; i < annotations.length; i++) {
                for (int j = 0; j < annotations[i].length; j++) {
                    //把引數及對應引數上的註解記錄解析出來 迴圈下標是從0開始,i=0 實際上是指第一個引數。
                    if(null != annotations[i][j]) {
                        annotationAndParamIndexMap.put(i,annotations[i][j]);
                    }
                }
            }
        }

        return annotationAndParamIndexMap;
    }

    /**
     * 欄位上有uncheck這個註解,不需要做檢驗,忽略
     * @param annotationAndParamIndexMap
     * @param paramIndex
     * @return
     */
    private boolean isCheck(Map<Integer,Annotation> annotationAndParamIndexMap,Integer paramIndex) {
        if(CollectionUtils.isEmpty(annotationAndParamIndexMap)){
            return true;
        }

        //如果對應的引數裡包含 Uncheck這個註解,就返回false,不校驗這個引數
        if(annotationAndParamIndexMap.get(paramIndex) instanceof Uncheck){
            return false;
        }

        return true;
    }

    /**
     * 欄位上是否有 自定義的註解 selfCheck
     * @param annotationAndParamIndexMap
     * @param paramIndex
     * @return
     */
    private boolean isHaveSelfCheck(Map<Integer,Annotation> annotationAndParamIndexMap,Integer paramIndex){
        if(CollectionUtils.isEmpty(annotationAndParamIndexMap)){
            return false;
        }

        if(annotationAndParamIndexMap.get(paramIndex) instanceof SelfCheck){
            return true;
        }

        return false;
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}          

這一行程式碼就是具體的校驗
checkResult = paramCheckManager.check(objects[i], paramTypeNameList.get(paramIndex),
                                Arrays.asList(paramAnnotation.check()),
                                paramAnnotation.condition(),
                                paramAnnotation.msg());

我們看下paramCheckManager的實現如下:

@Service
public class ParamCheckManager {
    @Resource
    private ParamCheckCollection paramCheckCollection;

    /**
     * 基礎校驗&自定義校驗
     * 基礎校驗是必須要做的,自定義校驗根據註解上的配置來決定是否要做
     * @param v
     * @param objectType
     * @param selfChecks
     * @param condition
     * @param failMsg
     * @return
     */
    public CheckResult check(Object v,
                             Class<?> objectType,
                             List<String> selfChecks,
                             String condition,
                             String failMsg) throws ServiceCheckException {

        //基礎的校驗,物件為NULL,為EMPTY
        CheckResult baseCheck = paramCheckCollection.getParamCheckInstance(objectType.getName()).check(v, objectType, condition);

        //基礎校驗不通過,直接返回false
        if (!baseCheck.isSuccess()) {
            return baseCheck;
        }

        //載入自定義的資料校驗
        if (!CollectionUtils.isEmpty(selfChecks)) {
            for (String selfCheck : selfChecks) {
                if (!StringUtils.isEmpty(selfCheck)) {
                    CheckResult checkRult = paramCheckCollection.getParamCheckInstance(selfCheck).check(v, objectType, condition);

                    //自定義校驗不通過,直接返回false
                    if (!checkRult.isSuccess()) {
                        // 使用使用者自定義的校驗失敗資訊
                        if (StringUtils.isNotBlank(failMsg)) {
                            checkRult.setMsg(failMsg);
                        }
                        return checkRult;
                    }
                }
            }
        }

        return new CheckResult(true);
    }
}

其中 ParamCheckCollection這裡封裝了所有的校驗規則(包括框架裡的和將來自己要寫的都是在這裡載入完成的)

/**
 * 所有註冊的檢驗類(物件型別的、集合型別的、自定義型別等)
 */
@Service
public class ParamCheckCollection  implements ApplicationContextAware {
    private Map<String,ParamCheck> paramCheckMap;
    private ApplicationContext applicationContext;

    /**
     * 初始化完成所有校驗的規則類,並按照名稱植入map中
     */
    @PostConstruct
    protected void init(){
        Map<String,Object> tempParamCheckMap = applicationContext.getBeansWithAnnotation(CheckRule.class);

        if(!CollectionUtils.isEmpty(tempParamCheckMap)){
            paramCheckMap = new HashMap<>(tempParamCheckMap.size());
            for(Object o : tempParamCheckMap.values()){
                if(o instanceof ParamCheck){
                    ParamCheck paramCheck = (ParamCheck)o;
                    paramCheckMap.put(paramCheck.name(),paramCheck);
                }
            }
        }
    }

    /**
     * 返回對應規則名稱的校驗類,如果沒有找到對應的規則類,那麼返回物件檢驗規則類
     * @param checkName
     * @return
     */
    public ParamCheck getParamCheckInstance(String checkName){
        if(StringUtils.isEmpty(checkName)){
            return paramCheckMap.get(Object.class.getName());
        }
        ParamCheck iParamCheck = paramCheckMap.get(checkName);
        return (null != iParamCheck)? iParamCheck : paramCheckMap.get(Object.class.getName());
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

2、物件屬性上的校驗是如何做到的呢?
  待校驗的物件程式碼如下:
  @MinNum 就是框架裡的一個個性化校驗規則:校驗數字的大小是否大於某個數字,數字由業務自己定義。
  @ParameterCheck 是一個基礎校驗規則:校驗物件是否為NULL。
  屬性前沒有註解的,預設不需要進行校驗,框架就不會去校驗這些屬性。
  校驗邏輯,如上面所講,框架會載入類裡的屬性並,檢查類的屬性裡是否有註解。

public class TagWriteOption implements Serializable {
    private static final long serialVersionUID = -1639997547043197452L;

    /**
     * 商品id,必填
     */
    @MinNum(value = 1, msg = "itemId需要大於0")
    private Long itemId;

    /**
     * 商品所屬市場
     */
    @ParameterCheck(msg = "market不能為空")
    private Integer market;


    /**
     * 呼叫的業務系統的名稱
     */
    private String appName;
}

3、方法上有ParameterCheck這個註解,系統都會針對方法裡邊所有的引數進行基本的為NULL校驗,
   那如果有些方法的引數我不想做校驗,如何實現?
   
   框架考慮到了這個情況,您只需要在該引數前面加@Uncheck註解,框架檢測到引數前有這個註解,
   就會忽略這個引數的校驗,上面的程式碼中有實現。

/**
 * 不需要檢查
*/
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Uncheck {}複製程式碼

至此,您已經知道了這個框架的核心實現原理和程式碼,並知道如何定義自己的校驗規則運用到您自己的業務場景裡去。完全是透明可擴充套件的,框架其實就是定義了一個通用的規則執行與規則定義標準以及整合了基礎的校驗規則。大家完全可以在框架原始碼的基礎上,進行二次開發與規則定義以達到更加個性化的業務場景校驗。

目前框架支援的校驗註解如下:

@MinNum 數字最小值校驗

@MaxNum 數字最大值校驗

@MinSize 集合最小個數校驗

@MaxSize 結合最大個數校驗

@StrSize 字串長度校驗

@NotNull 非空校驗

@SelfDef 自定義校驗

@Num 數字區間校驗

@CollectionSize 集合區間校驗


4. 框架特點總結

    1)程式碼隔離,將校驗的程式碼邏輯與業務邏輯程式碼徹底隔離解耦。

        資料校驗的程式碼,全部獨立封裝到一個個的校驗規則裡,大家可以維護一個自己的校驗規則庫,用的時候只需要配置一下註解+規則名稱即可。

一個輕量級的引數校驗框架

   2)可擴充套件性極強,校驗規則可根據業務場景實際情況支援無限橫向擴充套件。

一個輕量級的引數校驗框架

   3)效能極佳,通過預載入模組將所有類載入到記憶體,使用時無需建立物件。

一個輕量級的引數校驗框架

   4)程式碼0侵入完成,只需要配置幾個註解,就可以實現基礎&個性化的入引數據校驗,對業務程式碼無任何汙染。

   5)智慧化校驗,框架可根據引數的資料型別實現智慧的資料校驗,針對引數的型別做對應的基礎校驗,比如物件為null,字串為空,列表大小等,自定義的物件,根據物件屬性上的註解完成必要的校驗。

   6)校驗結果按需定製,不同的業務領域或程式碼對於入引數據校驗不通過的返回有不一樣的提示資訊,框架提供靈活的校驗結果返回資訊,僅需要實現一個異常返回介面即可。

   7)校驗規則配置靈活,預設所有引數都會做基礎校驗,也可以校驗指定的引數,當然也可以指定不需要進行校驗等。

   8)程式碼高度重複利用,對於一個基礎性的校驗,只需要封裝在一個基礎性的校驗規則裡即可,以後使用只需要配置一個註解,就可以重複利用,間接的提升了開發效率和程式碼維護成本。


本文主要介紹了框架的基礎原理和如何使用,限於篇幅和個人能力原因,大家在閱讀過程中有任何疑問和問題,可以直接提問,歡迎大家提出任何建議和意見,一起完善這個框架,讓這個框架做的更好,更有價值和意義!

框架是我和公司另外一個同事一起完成,大家有問題可以發郵件給我們wangchun_166@126.com、yihuihuiyi@gmail.com,我們隨時恭候您的建議和意見!





相關文章