接口入参注解aop验证

时间:2022-07-29 21:08:26
为什么要入参验证
        系统之间在进行接口调用时,往往是有入参传递的,入参是接口业务逻辑实现的先决条件,有时入参的缺失或错误会导致业务逻辑的异常,大量的异常捕获无疑增加了接口实现的复杂度,也让代码显得雍肿冗长,因此提前对入参进行验证是有必要的,可以提前处理入参数据的异常,并封装好异常转化成结果对象返回给调用方,也让业务逻辑解耦变得独立。

为什么要使用aop方式
        入参验证的方式有多种,传统的方式是在接口实现中代码注入,即在接口实现的业务逻辑处理之前,通过硬编码的方式对入参进行有效性验证,简单粗暴,也直接有效。但代码注入带来的问题是代码的重用,不同的接口不同的入参,都需要编写不同的入参验证逻辑,造成了代码的重复使用,而这些验证逻辑大部分是可以复用的,并且代码注入方式虽然从一定程度上对业务逻辑进行了解耦,但依然需要在接口实现中注入代码,从一定程度上不够独立。因此,从代码重用和业务完全解耦上看,aop注解方式验参更加有效。

怎么实现aop方式
        spring框架的aop是一种面向切面编程,说的简单点就是将非核心业务的公共逻辑从业务层面抽离开来封装成可重用的模块,实现对业务逻辑的高度解耦,减少系统的重复代码。
aop方式需要首先在spring配置中定义aop映射,使得服务能够依赖注解有效切入。然后在服务调用前先定义好注解接口类及注解验证方法,通过在业务接口实现方法上增加注解来实现aop。服务调用时会根据接口方法的注解在切面通知方法中进行参数验证,验证失败则抛出异常,服务中止,业务逻辑完全独立,如下:
    @Override
    @ParamValid(className = "orderRequestVoValidator")
    public GenericResult<OrderResponseVo> orderRequest(OrderRequestVo vo) {
        log.info("质押收单请求开始,vo:{}", GsonUtils.toJson(vo));
        GenericResult<OrderResponseVo> result = tradeOrderService.orderRequest(vo);
        log.info("质押收单请求完成,result:{}", GsonUtils.toJson(result));
        return result;
    }

        一行注解搞定入参验证,代码瞬间简单大气,注解的实现接下来分析。使用注解,必然是需要先定义注解,注解可以根据系统的需要定义多种验证方法,比如像是否需要验证token,是否有调用次数限制。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamValid {

    String className();

    /* 是否限制调用次数 */
    boolean isLimit() default false;

    /* 方法访问次数 */
    long limitNum() default 10l;
    
    /* 方法访问限制时效 */
    int limitTime() default 300;
}

        因为注解是采用aop方式实现,就一定会有aop通知方法来实现对参数的验证。在通知方法中,就需要根据注解接口的方法来验证参数了。

怎么验证参数
        参数验证有多种方式,可以验证参数非空、参数类型、参数格式、赋值范围等,最直接的方法就是在注解通知方法中依次验证,但验证逻辑就会很长,并且不同的参数需要验证的类型不尽相同,在同一个方法中显然很难做到灵活验证,因此,就需要将参数验证类型进行配置化管理。
        在spring配置文件中,定义spring入参验证配置,对需要验证的入参配置验证类型(非空、数值、金额、范围等),并自定义spring标签,配置多种验证类型,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:validator="http://com.jd.assetPledge/schema/validator"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://com.jd.assetPledge/schema/validator http://com.jd.assetPledge/schema/validator/validator.xsd"
       default-lazy-init="false" default-autowire="byName">

    <validator:validator id="orderRequestVoValidator" class="com.jd.assetPledge.trade.export.vo.OrderRequestVo">
        <validator:property name="pin">
            <validator:NotNull message="请填写pin"/>
        </validator:property>
        <validator:property name="assetNo">
            <validator:NotNull message="请填写资产编号"/>
        </validator:property>
        <validator:property name="assetType">
            <validator:NotNull message="请填写资产类型"/>
        </validator:property>
        <validator:property name="channelType">
            <validator:NotNull message="请填写渠道号"/>
        </validator:property>
        <validator:property name="sourceType">
            <validator:NotNull message="请填写移动来源"/>
        </validator:property>
        <validator:property name="token">
            <validator:Token message="系统token为空或错误"/>
        </validator:property>
    </validator:validator>

        这样就能实现对入参不同参数的验证类型灵活配置,也能对同一入参对象配置多个验证模块,以满足同一入参对象在不同接口中的多种验证类型需求(比如同一入参的同一属性,在A接口必传,却在B接口可空)。当然,所有这些验证标签都是在spring中自定义的,我们可以根据业务的需要增加各种类型验证。
        标签定义好后,就是实现标签的验证逻辑了。首先我们需要定义一个统一的参数验证接口,然后根据自定义的标签一一实现接口逻辑,再根据spring的反射机制,将自定义标签绑定到对应的接口实现类上,接口实现类如下:
public class NotNullValidator extends ValidatorImpl implements Validator {
    public NotNullValidator() {
    }

    public boolean isValid(Object object) {
        if(object instanceof String) {
            int length = ((String)object).length();
            return length > 0;
        } else {
            return object != null;
        }
    }
}

        当需要增加验证类型时,无需修改代码,只需要自定义一套标签并增加一个对应的验证实现类即可,非常方便灵活。
        定义好整套验证实现类后,就可以在上面的注解通知方法中来统一调用了。在通知方法中,获取入参对象,根据spring配置文件的定义,匹配到自定义标签集合。根据标签集合的验证类型调用不同的验证实现类,对入参的每个属性进行验证。直接上代码:
@Component
@Aspect
public class ParamValidAspect {

    private static Logger log = LoggerFactory.getLogger(ParamValidAspect.class);

    @Autowired
    private CacheRpc cacheRpc;

    @Resource(name = "checkService")
    private CheckService checkService;

    @Value("${paramValid.token}")
    private String token;

    @Value("${paramValid.isLimit}")
    private boolean isLimit;

    @Value("${paramValid.limitNum}")
    private long limitNum;

    @Value("${paramValid.limitTime}")
    private int limitTime;

    @Pointcut("execution(* *(..)) && @annotation(com.jd.assetPledge.trade.web.validator.ParamValid)")
    public void paramValidPointcut() {
        log.info("paramValid aspect pointcut initialize successful...");
    }

    @Around("paramValidPointcut()")
    public Object aroundParamValidReturn(ProceedingJoinPoint pjp) throws Throwable {
        Method method = getMethod(pjp);
        Class[] parameterTypes = method.getParameterTypes();
        Object[] args = pjp.getArgs();
        if (parameterTypes.length < 1 || args.length < 1) {
            return pjp.proceed();
        }

        try {
            ParamValid paramValid = method.getAnnotation(ParamValid.class);
            String objName = paramValid.className();
            WebApplicationContext applicationContext = ContextLoader.getCurrentWebApplicationContext();
            Object obj = args[0];

            /* 判断入参是否正确 */
            String checkMsgTip = checkService.check(obj, (ValidatorBean) applicationContext.getBean(objName));
            if(StringUtils.isNotBlank(checkMsgTip)) {
                return getResObj(method,ResultInfoEnum.REQUEST_PARAMS_ERROR, checkMsgTip);
            }

            /* 判断调用次数限制 */
            if(paramValid.isLimit() || isLimit) {
                if(paramValid.isLimit()) {
                    limitNum = paramValid.limitNum();
                    limitTime = paramValid.limitTime();
                }

                BaseRequestVo base = (BaseRequestVo)args[0];
                long count = cacheRpc.countKey(base.getPin() + method.getName(), limitTime);
                if(count > limitNum) {
                    return getResObj(method,ResultInfoEnum.REQUEST_LIMIT_ERROR, null);
                }
            }
        } catch (Exception e) {
            return getResObj(method,ResultInfoEnum.UNKNOW_ERROR, null);
        }

        return pjp.proceed();
    }

    private Object getResObj(Method method,ResultInfoEnum enumType, String checkMsgTip) {
        Class<?> returnType = method.getReturnType();
        Class[] classArgs = new Class[2];
        classArgs[0] = String.class;
        classArgs[1] = String.class;

        try {
            Constructor constructor = returnType.getConstructor(classArgs);
            String code = enumType.getErrorCode();
            String message = enumType.getErrorMsg(checkMsgTip);
            return constructor.newInstance(code, message);
        } catch (Exception e) {
            log.error("exception", e);
        }

        return null;
    }

    private Method getMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException {
        Signature sig = pjp.getSignature();
        MethodSignature msig = (MethodSignature) sig;
        return getClass(pjp).getMethod(msig.getName(), msig.getParameterTypes());
    }

    private Class<? extends Object> getClass(ProceedingJoinPoint pjp)
            throws NoSuchMethodException {
        return pjp.getTarget().getClass();
    }
}


public class CheckService {
    private static final Logger logger = LoggerFactory.getLogger(CheckService.class);

    public CheckService() {
    }

    public <T> String check(T t, ValidatorBean nameValidator) throws IllegalAccessException {
        long t1 = System.currentTimeMillis();
        logger.info("参数验证开始-----------------------");
        Map filedValidatorListMap = nameValidator.getFiledValidatorListMap();
        ArrayList fieldList = new ArrayList();
        DynamicUtil.getClassAllField(t.getClass(), fieldList);
        StringBuffer checkMsgTip = new StringBuffer("");
        Iterator i$ = fieldList.iterator();

        while(true) {
            Field field;
            List validatorList;
            do {
                if(!i$.hasNext()) {
                    logger.info("参数验证结束---------------------------时间{}", Long.valueOf(System.currentTimeMillis() - t1));
                    return checkMsgTip.toString();
                }

                field = (Field)i$.next();
                String fieldName = field.getName();
                field.setAccessible(true);
                validatorList = (List)filedValidatorListMap.get(fieldName);
            } while(validatorList == null);

            Iterator i$1 = validatorList.iterator();

            while(i$1.hasNext()) {
                Validator validator = (Validator)i$1.next();
                ValidatorImpl validator1 = (ValidatorImpl)validator;
                boolean checkResult = validator.isValid(field.get(t));
                if(!checkResult) {
                    logger.info(nameValidator.getName() + ":" + "field=" + validator1.getValidatorField() + ",value=" + field.get(t) + ",validatorType=" + validator1.getValidatorType() + "结果:" + checkResult + ",消息" + ((ValidatorImpl)validator).getMessage());
                    checkMsgTip.append(((ValidatorImpl)validator).getMessage() + "\n");
                }
            }
        }
    }
}

        验证完成后,如果验证失败集合不为空,则使用公共返回对象封装实例化,返回调用方相关的错误代码与提示信息。
        到此,接口入参的注解aop验证方法介绍完毕,当然,上面所贴的代码并非完整的验证代码,诸如spring标签定义、接口反射等逻辑就不在这里展示了,这里主要是想表达下自己的见解和原理。从上面的分析可以看出,注解aop验证入参,最大的好处就是让参数验证与业务逻辑高度解耦,用专业的方法干专业的事,然后就是实现了验证方式的灵活配置,这个能让我们对代码的伤害降到最低。