Second, the principle of framework Before this paper introduces the principle of the framework, to explain, why do we need to design such a framework: under the system of electricity business systems now everyone is using centralized basic RPC service output ability, such as: the goods business, all the electrical contractor involved in the commodity business ability and basic by the areas of business management, the most basic are: Query goods, goods issued/edit a single/bulk 】 【 that all materials, goods, goods SKU management, commodity category management, inventory management, etc., are doing the business services will meet a lot of need to legality, validity, service interface parameters about qualitative check work, and there was this power, seek a more abstract solution.

  1. Internal running mechanism of the framework

2. Introduction to framework roles

1) Verification rules: Is at the bottom of the validation logic execution units in the framework, basic calibration, boundary, personalized calibration is completed in this module, check the basic rules of validation rules (such as empty, NULL, etc.), also has the personalized validation rules (such as special calibration for different business rules) everyone as long as the realization of a validation rules of interface, You can write your own validation rules. 2) Validation entry: not all interface services need validation. Validation entry tells the framework which service interface or method needs validation. 3) Check bracket: connect the check inlet and check rule. Tells the framework which validation rule to execute on a parameter in that entry. The relationship between them is like cooking a dish, the relationship between the ingredients, the recipe, the chef; Ingredients are like checking rules, recipes are like checking entrances, and the chef is like checking brackets he can follow the recipe and choose the right ingredients to cook a delicious meal! Their relationship is as follows:Copy the code

      

3. Analysis of implementation principle

1) Let’s take a look at the implementation principle of the verification rule. The code is as follows:

All verification rules need to implement the interface ParamCheck, which is defined as follows:

/** * There are two main types of validation rules: * 1, the basic data type is NULL or NULL, which I have written * 2, personalized custom validation, such as batch query threshold validation, these often have specific scenarios and backgrounds. */ public interface ParamCheck {/** * All logical classes that need to be verified, * @param t Value to be verified * @param objectType Data type of the value to be verified * @param c User-defined verification rule content * @returnCheckResult Check result & Description */ CheckResult check(Object t, Class<? > objectType, String c) throws ServiceCheckException; /** * The name of the validation rule to dynamically find the validation rule class * @ configured in the annotationreturn*/ String name(); } This interface defines two methods: the check() method is the basic validation logic for all validation rules. The name() method returns the name of the validation rule, which is initialized and stored in local memory, and all validation work is done by the in-memory validation rule classes. Let's implement a basic verification rule class and a personalized verification rule class respectively, as follows: /** * Base check rule - Check whether the object is NUll, the most basic check, */ @checkrule public class ObjectCheck implements ParamCheck {/** * All logical classes that need to be checked, * * @param t Value to be verified * @param objectType Data type of the value to be verified * @param c User-defined verification rule content * @return*/ @override public CheckResult check(Object t, Class<? > objectType, String c) {returnCheckUtils.objectIsNullCheck(t); } /** * The name of the validation rule to dynamically find the validation rule class ** @ configured in the annotationreturn
     */
    @Override
    public String name() {
        returnObject.class.getName(); */ @checkRule public class MaxSizeCheck implements ParamCheck {private final Static Logger logger = LoggerFactory.getLogger(MaxSizeCheck.class); /** * All logical classes that need to be verified need to implement this method ** @param t value to be verified * @param objectType Data type of value to be verified * @param c User-defined verification rule content * @return*/ @override public CheckResult check(Object t, Class<? > objectType, String c) throws ServiceCheckException { CheckResult checkResult = new CheckResult(true); // If the number of checklists exceeds the set threshold and no threshold is passed, it passes by defaultif(StringUtils.isEmpty(c)){
            returncheckResult; } Integer maxSize; JSONObject objectCondition = json.parseObject (c); 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());
        }

        returnCheckUtils.sizeGreaterThanFeedCheck(t,maxSize,objectType); } /** * The name of the validation rule to dynamically find the validation rule class ** @ configured in the annotationreturn
     */
    @Override
    public String name() {
        returnthis.getClass().getSimpleName(); }}Copy the code

Note: Each rule requires an annotation, which is used by the system to identify the validation rule and load it into memory automatically.

The annotation code of the verification rule is as follows:

/** * Validation rule annotation, one for each validation rule class, Can pass * applicationContext getBeansWithAnnotation () finish initialization * / @ Target ({ElementType. TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Service public @interface CheckRule { String name() default""; // Verify rule name (to be unique)}Copy the code

The above code is the core code and operation mechanism of the verification rule, so the verification rule can be horizontally infinite expansion, you just need to implement the interface ParamCheck, and use the @checkrule annotation in front of the rule, the system will automatically load your verification rule into memory. So how do you use the validation rules? Look at the following code:

public class TagWriteServiceImpl implements TagWriteService {

    @Autowired
    private TagManager tagManager;

    /**
     * @param tagOption
     * @return* @ ParameterCheck this annotation is the interface into the parameter calibration Complete * / @ ParameterCheck through annotations + AOP (exceptionHandler = ServiceCheckFailedHanlder. Class) to the public ResultDO<Boolean> tagAdd(tagWriteOption tagOption) {// Todo own business logic (before doing business logic, ParameterCheck does some basic validation of the tagWriteOption object and the properties specified by the object)....... Step 1: You need to annotate @parameterCheck in front of the method you want to validate. The second step: in @ ParameterCheck annotations to specify a check not return objects from the error information as above ServiceCheckFailedHanlder class in your code. The implementation of this class is as follows: Public class ServiceCheckFailedHanlder implements ICheckFailedHandler {/ * * * the framework itself is a generic framework, but there will always be some information is need to be customized, For example, different business code has its own different error message wrapper class, etc. * Framework to increase versatility and flexibility, here the framework only defines an interface ICheckFailedHandler, * / @override public Object validateFailed(String MSG, ClassreturnType, Object... BaseResultDO result = new BaseResultDO<>(); BaseResultDO result = new BaseResultDO<>(); result.setSuccess(false);
        result.setResultCode(BaseResultTypeEnum.PARAM_ERROR.getCode());
        result.setResultMessage(msg);
        returnresult; }} ParameterCheck is used to verify the tagWriteOption object and its properties before ParameterCheck is performed. ParameterCheck is NULL for all parameters in a ParameterCheck method. ParameterCheck is NULL for all parameters in a ParameterCheck method. The implementation is as follows: The system automatically intercepts methods annotated with ParameterCheck via AOP. AOP interception is implemented as follows:  @Component @Aspect @Order(1) public class ParamCheckAop extends AbsAopServiceParamterCheck { private static Logger logger = LoggerFactory.getLogger("aopServiceParameterCheck"); /** * all methods with ParameterCheck annotations will be blocked * @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();
        returnmethod.getMethod(); }} AbsAopServiceParamterCheck is framework provides an abstract class, implementation is as follows:  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("This annotation can only be used for methods"); } msig = (MethodSignature) sig; Object target = pjp.getTarget(); Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes()); // Whether the current method ParameterCheck this annotationif(currentMethod isAnnotationPresent (ParameterCheck. Class)) {/ / method parameter Object [] args = PJP. GetArgs (); Object[] params = new Object[args.length+2]; params[0] = pjp.getTarget(); Params [1] = currentMethod.getName(); params[1] = currentMethod.getName(); / / the method namefor(int i = 0; i<args.length; i++){ params[i+2] = args[i]; } / / perform calibration method, the parameters of basic + custom check CheckResult checkBaseParamResult = serviceParameterCheck. CheckMethod (params);if(! checkBaseParamResult.isSuccess()){ logger.warn(pjp.getTarget().getClass().getSimpleName()+"."+currentMethod.getName()+"|checkSuccess=false"+"|param="+ JSON.toJSONString(args));
                returncheckBaseParamResult; } // Execute the verification method - If the object is a user-defined object, you need to check whether the attributes of the object have verification rules. 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); }} based check is this line of code: CheckResult checkBaseParamResult = serviceParameterCheck. CheckMethod (params); The code is as follows:  @Service public class ServiceParameterCheck implements ApplicationContextAware { private ApplicationContext applicationContext; / * * * initializes the object list * / registered private Map < String, IAnnotationManager > annotationManagerMap = new HashMap < > (); @postconstruct protected void init() throws ServiceCheckException {@postconstruct protected void init() throws ServiceCheckException { Map<String,Object> objectMap = applicationContext.getBeansWithAnnotation(ServiceCheckPoint.class); /** initializes - enters the object level & method level annotation attribute **/for(Object o : objectMap.values()){
            if(o instanceof IAnnotationManager){ annotationManagerMap.put(((IAnnotationManager)o).getAnnotationCheckType().name(),((IAnnotationManager)o)); ((IAnnotationManager)o).init(); }}} /** * Verify against the input parameters of the method * @param args * @return*/ public CheckResult checkMethod(Object ... args){returnannotationManagerMap.get(AnnotationCheckType.METHOD.name()).check(args); } /** * according to the annotation * @param o * @ on the input objectreturn
     */
    public CheckResult checkObject(Object o){
        returnannotationManagerMap.get(AnnotationCheckType.OBJECT.name()).check(o); } /** * Batch validate * @param objects * @ with annotations on the input objectreturn
     */
    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()){returncheckRult; }}return new CheckResult(true);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }} The specific verification is here: / * * * service interface method arguments against doing check * / @ 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(); [] methods = clazz.getDeclaredMethods();if(ArrayUtils.isEmpty(methods)){
                    break;
                }

                for(Method Method: methods){ParameterCheck is available on the Methodif(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()); }} /** * one of the entries to perform the verification * @param args * @return*/ @override public CheckResult check(Object... args) { CheckResult checkResult = new CheckResult(true); /** If the parameter list is empty, return directlytrue, do not perform verification **/if(ArrayUtils.isEmpty(args)){
            returncheckResult; } /** arguments must be longer than two, the first is the interface service class object, the second is the method signature of the call, and the rest is the input parameter **/if(args.length < reserveLen){
            returncheckResult; } Object[] objects = args; String methodName = args[1].toString(); String key = args[0].getClass().getName()+"."+methodName; /** The List of arguments under this Class + method, methodParamMap is set when initialized **/ List<Class<? >> paramTypeNameList = methodParamMap.get(key); /** indicates no need to check **/if(CollectionUtils.isEmpty(paramTypeNameList)){
            returncheckResult; Method Method = methodmap.get (key); ParameterCheck annotation = method.getannotation (parameterCheck.class); ParameterCheck annotation = method.getannotation (parameterCheck.class);if(null == annotation){
            returncheckResult; } / / to obtain a list of method parameter in the corresponding annotations Map > < Integer, an Annotation annotationAndParamIndexMap = getAnnotationAndParamIndex (method); /** * loop validates the passed argument, why start at 2, * because the first argument is the service object. * The second argument is the method signature. * From the third argument, is the method argument list */ try {for(int i = reserveLen; i < objects.length; i++) { int paramIndex = i-reserveLen; // The uncheck annotation on the field indicates that no checking is requiredif(isCheck (annotationAndParamIndexMap paramIndex)) {/ / if no custom annotations on parameters, then to approach the custom annotations on the annotation for inspection rulesif(isHaveSelfCheck(annotationAndParamIndexMap, ParamIndex)) {/ / parameter inspection rules custom annotations 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()) {returncheckResult; }}}}catch (Exception e){/** If an Exception occurs in the check, the check passes by default. Can go down */ logger.error("ServiceMethodAnnotationManager error ,msg=",e);
        }

        return new CheckResult(true); } /** * Each implementation of this interface needs to return a validation level: the input parameter level * @return
     */
    @Override
    public AnnotationCheckType getAnnotationCheckType() {
        returnAnnotationCheckType.METHOD; } /** * gets an index set of parameters that do not need to be checked * @param method * @return*/ private Map<Integer,Annotation> getAnnotationAndParamIndex(Method method){ Map<Integer,Annotation> annotationAndParamIndexMap = new HashMap<>(); / / to get if there is a uncheck the annotations in the method parameter Annotation [] [] annotations = method. The getParameterAnnotations ();if(! ArrayUtils.isEmpty(annotations)) {for (int i = 0; i < annotations.length; i++) {
                for(int j = 0; j < annotations[i].length; J++) {// parse the argument and the annotation on the corresponding argument. The loop subscript starts at 0. I =0 actually refers to the first argument.if(null ! = annotations[i][j]) { annotationAndParamIndexMap.put(i,annotations[i][j]); }}}}returnannotationAndParamIndexMap; } / * * * uncheck this annotation fields, don't need to do test, ignore the * @ param annotationAndParamIndexMap * @ param paramIndex * @return
     */
    private boolean isCheck(Map<Integer,Annotation> annotationAndParamIndexMap,Integer paramIndex) {
        if(CollectionUtils.isEmpty(annotationAndParamIndexMap)){
            return true; } // Return if the corresponding argument contains the Uncheck annotationfalse, does not validate this parameterif(annotationAndParamIndexMap.get(paramIndex) instanceof Uncheck){
            return false;
        }

        return true; } / * * * on the field to see if there is a custom annotations 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()); The paramCheckManager implementation is as follows:  @Service public class ParamCheckManager { @Resource private ParamCheckCollection paramCheckCollection; /** * base check & custom check * Base check is mandatory, The customized verification is performed based on the configuration in the annotations. * @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 {// The basic verification object is NULL. For the EMPTY CheckResult baseCheck = paramCheckCollection. GetParamCheckInstance (objectType. The getName ()). Check (v, objectType, condition); // If the base check fails, return directlyfalse
        if(! baseCheck.isSuccess()) {returnbaseCheck; } // Load custom data verificationif(! CollectionUtils.isEmpty(selfChecks)) {for (String selfCheck : selfChecks) {
                if(! StringUtils.isEmpty(selfCheck)) { CheckResult checkRult = paramCheckCollection.getParamCheckInstance(selfCheck).check(v,  objectType, condition); // The custom verification failsfalse
                    if(! Checkrult.issuccess ()) {// Use user-defined verification failure informationif (StringUtils.isNotBlank(failMsg)) {
                            checkRult.setMsg(failMsg);
                        }
                        returncheckRult; }}}}return new CheckResult(true); /** * All registered validation classes (object type, set type, custom type, etc.) */ @service public; /** * all registered validation classes (object type, set type, custom type, etc. class ParamCheckCollection implements ApplicationContextAware { private Map<String,ParamCheck> paramCheckMap; private ApplicationContext applicationContext; /** * Initialize the rule class that does all the validation and place it in the map by name */ @postconstruct protected voidinit(){
        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); }}}} /** * Returns the checkName of the rule. If no checkName is found, object checkName * @param checkName * @ is returnedreturn
     */
    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 voidsetApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }} 2. How is validation done on object properties? The code of the object to be verified is as follows: @minNum is a personalized verification rule in the framework. Whether the size of the verification number is greater than a certain number is determined by the service itself. ParameterCheck is a basic verification rule: Whether the verification object is NULL. Properties without annotations in front of them do not require validation by default, and the framework does not validate these properties. The validation logic, as described above, is that the framework loads the properties of the class and checks if there are annotations in the properties of the class. public class TagWriteOption implements Serializable { private static final long serialVersionUID = -1639997547043197452L; /** * Commodity ID, must be */ @minNum(value = 1, msg = "ItemId needs to be greater than 0") private Long itemId; $ParameterCheck(MSG = $ParameterCheck) $ParameterCheck(MSG = $ParameterCheck"The market cannot be empty.") private Integer market; /** * Private String appName; } ParameterCheck is NULL for all ParameterCheck parameters in a ParameterCheck method. The framework takes this into account. All you need to do is add the @uncheck annotation in front of the parameter, and the framework will ignore the validation of the parameter if it detects the annotation before the parameter, which is implemented in the code above. Do not need to check / * * * * / @ Target ({ElementType. FIELD, ElementType METHOD, ElementType. PARAMETER}) @ Retention (RetentionPolicy. RUNTIME)  @Documented public @interface Uncheck {}Copy the code

Now that you know the core implementation principles and code of the framework, you know how to define your own validation rules to apply to your own business scenarios. Completely transparent and extensible, the framework essentially defines a common rule execution and rule definition standard and integrates basic validation rules. We can completely on the basis of the framework source code, secondary development and rule definition to achieve more personalized business scene verification.

The framework currently supports the following validation annotations:

@minnum Specifies the minimum number

@maxNum Indicates the maximum number verification

@minsize Check the minimum number of sets

MaxSize Verifies the maximum number

@strSize Verifies the string length

@notnull Non-null check

@selfdef User-defined verification

@Num Indicates the digit range check

CollectionSize Collection interval verification


4. Summary of framework features

1) Code isolation: completely isolate and decouple the verified code logic from the business logic code.

Data verification code, all independent encapsulation into a verification rule, we can maintain a check rule library, with only need to configure the annotations + rule name.



2) Strong scalability, the verification rules can support unlimited horizontal expansion according to the actual situation of business scenarios.



3) Excellent performance, through the preload module to load all classes into memory, use without creating objects.



4) Code 0 intrusion is complete, only need to configure a few annotations, can realize the basic & personalized input parameter data verification, no pollution to the business code.

5) Intelligent verification. The framework can implement intelligent data verification according to the data type of the parameter, and perform corresponding basic verification according to the type of the parameter, such as the object is null, the string is empty, the list size, etc. For the user-defined object, the necessary verification can be completed according to the annotation on the object attribute.

6) Verification results can be customized according to needs. Different business fields or codes will have different prompt messages when the input data fails verification. The framework provides flexible verification result return information and only needs to realize an exception return interface.

7) The configuration of verification rules is flexible. By default, basic verification is performed for all parameters, and specified parameters can also be verified. Of course, verification is not required.

8) The code is highly reusable. For a basic verification, it only needs to be encapsulated in a basic verification rule. In the future, it only needs to configure a note to be reused, which indirectly improves the development efficiency and code maintenance cost.


This article mainly introduces the basic principle of the framework and how to use, limited to space and personal ability reasons, you have any questions and questions in the process of reading, you can directly ask questions, welcome to put forward any suggestions and opinions, together to improve the framework, let the framework do better, more valuable and meaningful!

The framework was completed by me and another colleague of the company. If you have any questions, please email us [email protected] or [email protected]. We are always waiting for your suggestions and opinions!