如何自动优雅的校验方法入参并友好返回

方法的入参校验,应该是编码过程中高频的操作了。这个费时费力又没有技术含量的步骤,有没有更优雅的解决方案?

看过很多篇关于入参校验的文章,感觉提供的解决方案还是不够便捷,索性就自己写一个吧。

常规做法一:写很多的if....,弊端就不用多说了

直接上代码

if (Objects.isNull(userType)) {
    logger.warn( "test verify:{}, {}", "userType", ErrorCode.PARAMETER_CANNOT_NULL.getMessage() );
    return ResultWrapper.fail( ErrorCode.PARAMETER_CANNOT_NULL, "userType" );
}
if (Objects.isNull(testDto.getUserId())) {
    logger.warn( "test verify:{}, {}", "userId", ErrorCode.PARAMETER_CANNOT_NULL.getMessage() );
    return ResultWrapper.fail( ErrorCode.PARAMETER_CANNOT_NULL, "userId" );
}
if (Objects.isNull(testAccountDto.getBusiness())) {
    logger.warn( "test verify:{}, {}", "business", ErrorCode.PARAMETER_CANNOT_NULL.getMessage() );
    return ResultWrapper.fail( ErrorCode.PARAMETER_CANNOT_NULL, "business" );
}
...

常规做法二:if 少了些,但返回的错误信息不够精确

if(demoDto==null || demoDto.getSid()==null || demoDto.getBusiness()==null){
    logger.info("demoMethod校验参数不通过,demoDto:{}",JSONObject.toJSONString(demoDto));
    return ResultUtil.buildFailResult("校验参数不通过");
}

到底哪个参数校验不通过,你倒是告诉我啊!!!

最惨的情况有可能把所有的字段都比对够一遍,才能知道具体哪个字段为空。字段少还好,如果是插入的场景,几十个字段,一一核查就要了命了。

常规做法三:属性上加注解

@NotNull(describe = "商家Id不能为null")
private Long sid;
@NotLessEqualsZero(describe = "业务线Id不能小于0")
private Long businessId;
...

校验逻辑

public static String checkObject(Object object) {
    if (null == object) {
        return "参数不能为null";
    }
    try {
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            String name = field.getName();
            name = name.substring(0, 1).toUpperCase() + name.substring(1);
            //检测是否为null
            String notNullDescribe = checkNotNull(object, field, name);
            if (null != notNullDescribe) {
                return notNullDescribe;
            }
            //检测是否大于0
            String notLessEqualsZeroDescribe = checkNotLessEqualZero(object, field, name);
            if (null != notLessEqualsZeroDescribe) {
                return notLessEqualsZeroDescribe;
            }
            //检测是否大于等于0
            String notNotLessZeroDescribe = checkNotLessZero(object, field, name);
            if (null != notNotLessZeroDescribe) {
                return notNotLessZeroDescribe;
            }
        }
        return null;
    } catch (Exception e) {
        logger.error("参数校验异常!params={}.", JSON.toJSONString(object), e);
        return "参数校验异常";
    }
}

在需要校验的地方调用校验

String errMsg = DtosUtil.checkObject(withdrawDto);
if (errMsg != null) {
    return ResultUtil.buildFailResult(errMsg);
}

这种写法,看着还比较优雅,但是也不够灵活,还得手工处理返回结果。如果某些属性在某些场景不能为空,有些场景又可以为空,这就不好处理了。

举个例子,在插入的时候很多参数是不能为空的,但是在更新的时候大部分参数是都可以为空的。

在属性上使用注解,代码侵入性高。如果是对外提供的jar包,jar包内还需要额外引入其他jar包(一般都会引用三方现有的注解功能,如果是自定义注解就没有这个问题了)。

我期望的效果+解决方案

1、能够灵活的控制需要校验的参数;

2、校验失败时能够返回并且是自动返回具体哪些参数失败,失败的原因;

3、有些场景,校验失败可以继续执行,打个警告的日志即可;

4、校验失败的日志级别可以根据业务场景自己控制;

5、除了非空,其他的校验最好也能支持,容易扩展;

6、捎带把日志也打印了。

自定义注解

public @interface NotNull {
    /**
     * 待校验的参数/属性
     * @return
     */
    String fields();
    /**
     * 提示信息
     * @return
     */
    String message() default "not null";
    /**
     * 业务场景描述
     */
    String businessDesc() default "";
    /**
     * 日志级别
     */
    String logLevel() default "ERROR";
    /**
     * 校验失败,自动返回
     * @return
     */
    boolean autoReturn() default true;
}

切面处理

@Aspect
@Component
public class CheckAspect {
    private static final Logger logger = LoggerFactory.getLogger(CheckAspect.class);
    private final String BASE_TYPES = "void,int,byte,short,char,double,long";
    private final String NUMBER_TYPES = "int,byte,short,double,long";
    private final String CHAR_TYPES = "char";


    @Pointcut("@annotation(com.daojia.dop.checkcenter.annotation.NotNull)")
    public void notNull(){}
    @Pointcut("@annotation(com.daojia.dop.checkcenter.annotation.Pattern)")
    public void pattern(){}


    @Around("notNull()||pattern())
    public Object doAccessCheck(ProceedingJoinPoint point){
        long start = System.currentTimeMillis();
        boolean checkFlag = true;
        boolean autoReturn = true;
        //校验失败的字段信息
        Map<String, String> checkFailMap = new HashMap<>();
        // 校验失败返回内容格式:类名.方法名(): businessDesc [field:message]
        StringBuilder returnMsg = new StringBuilder();
        //切面信息
        Class target = point.getTarget().getClass();
        Signature signature = point.getSignature();
        String fullSignature = signature.toLongString();


        // 拿到类名,方法名,所有参数名,所有参数值,注解信息,返回值类型
        String className = target.getSimpleName();
        // 获取当前被切面的方法
        MethodSignature methodSignature = (MethodSignature)signature;
        Method pointMethod = methodSignature.getMethod();
        // 方法名
        String methodName = pointMethod.getName();
        // 参数名称
        String[] paramNames = methodSignature.getParameterNames();
        // 日志级别
        String logLevel = "ERROR";


        if(paramNames.length < 1){ //无参方法
            checkFlag = false;
            checkFailMap.put(className + "." +methodName + "()", "Don't join in if the function don't have any arguments...");
        }else{
            // 参数值
            Object[] paramValues = point.getArgs();
            // 打印入参
            logger.info("{}() - request={}", methodName, JSON.toJSONString(paramValues, SerializerFeature.WriteDateUseDateFormat));


            // 参数名,参数值
            Map<String, Object> paramKV = new HashMap<>();
            // 提取1、2层参数,参数值 【只支持两层】
            try{
                for (int i = 0; i < paramNames.length; i++) {
                    Object tmp = paramValues[i];
                    // 方法参数的值-外层
                    paramKV.put(paramNames[i], tmp);
                    if(tmp != null && tmp.getClass().getName().indexOf(".") > 0 && !tmp.getClass().getName().startsWith("java.")){
                        // 方法参数对象内部各属性的值-内层
                        Field[] innerFields = tmp.getClass().getDeclaredFields();
                        for (Field innerField : innerFields) {
                            innerField.setAccessible(true);
                            String innerFieldName = innerField.getName();
                            paramKV.put(paramNames[i] + "." + innerFieldName, innerField.get(tmp));
                        }
                    }
                    if(tmp instanceof List){
                        //泛型类型
                        ParameterizedType p = (ParameterizedType)tmp.getClass().getGenericSuperclass();
                        //泛型类
                        Class c = (Class) p.getActualTypeArguments()[0];
                    }
                }
            }catch(Exception e){
                logger.warn("Handle parameterName/parameterValue failure! ", e);
            }


            returnMsg.append(className).append(".").append(methodName).append("():");


            // 校验参数
            Annotation[] annotations = pointMethod.getAnnotations();
            for (Annotation annotation : annotations) {
                String annotationName = annotation.annotationType().getSimpleName();
                switch (annotationName){
                    // 非空校验
                    case "NotNull" : {
                        NotNull notNull = (NotNull) annotation;
                        returnMsg.append(notNull.businessDesc()).append(" - ");
                        logLevel = notNull.logLevel();
                        autoReturn = notNull.autoReturn();
                        String fields = notNull.fields();
                        // fields格式校验
                        checkFlag = this.checkFormat(fields);
                        if(checkFlag){
                            String[] fieldsArray = notNull.fields().split("\\|");
                            for (String field : fieldsArray) {
                                if(field.indexOf(":") > 0 && !checkFailMap.keySet().contains(field.split(":")[0])){
                                    String paramObj = field.split(":")[0];
                                    String[] paramFields = field.split(":")[1].split(",");
                                    if(paramKV.get(paramObj) == null){
                                        checkFailMap.put(paramObj, notNull.message());
                                    }
                                    for (String paramField : paramFields) {
                                        if(paramKV.get(paramObj + "." + paramField) == null){
                                            checkFailMap.put(paramObj + "." + paramField, notNull.message());
                                        }
                                    }
                                    continue;
                                }
                                if(paramKV.get(field) == null){
                                    checkFailMap.put(field, notNull.message());
                                }
                            }
                        }
                    }
                    break;
                    // 正则校验... 其他校验                    
                    default:break;
                }
            }
        }


        // 根据校验结果处理
        if(checkFailMap.size() > 0 || !checkFlag){
            returnMsg = returnMsg.append(JSON.toJSONString(checkFailMap));
            // 根据日志级别打印日志
            if("ERROR".equals(logLevel.toUpperCase())){
                logger.error(returnMsg.toString());
            }else if("INFO".equals(logLevel.toUpperCase())){
                logger.info(returnMsg.toString());
            }else{
                logger.warn(returnMsg.toString());
            }


            // 处理返回类型
            Object returnObj = null;
            try {
                // 获取方法返回类型
                String returnTypeStr = fullSignature.split(" ")[1];


                if(!BASE_TYPES.contains(returnTypeStr)){
                    Class retCls = Class.forName(returnTypeStr);
                    if(retCls.isInterface()){
                        if(returnTypeStr.contains("List")){
                            returnObj = new ArrayList<>();
                        }
                        if(returnTypeStr.contains("Set")){
                            returnObj = new HashSet<>();
                        }
                        if(returnTypeStr.contains("Map")){
                            returnObj = new HashMap<>();
                        }
                    }else{
                        returnObj = retCls.newInstance();
                    }
                    if(returnTypeStr.toUpperCase().indexOf("RESULT") > 0){
                        Class retTypeCls = Class.forName(returnTypeStr);
                        Field[] retTypeFields = retTypeCls.getDeclaredFields();
                        for (Field retTypeField : retTypeFields) {
                            if (retTypeField.getName().equals("code") && retTypeField.getType().equals(int.class)){
                                Method setCode = retCls.getMethod("setCode", int.class);
                                if(setCode != null){
                                    setCode.invoke(returnObj, -1);
                                }
                            }if (retTypeField.getName().equals("status") && retTypeField.getType().equals(int.class)){
                                Method setCode = retCls.getMethod("setStatus", int.class);
                                if(setCode != null){
                                    setCode.invoke(returnObj, -1);
                                }
                            }else if (retTypeField.getName().equals("message") && retTypeField.getType().equals(String.class)){
                                Method setCode = retCls.getMethod("setMessage", String.class);
                                if(setCode != null){
                                    setCode.invoke(returnObj, returnMsg.toString());
                                }
                            }else if (retTypeField.getName().equals("msg") && retTypeField.getType().equals(String.class)){
                                Method setCode = retCls.getMethod("setMsg", String.class);
                                if(setCode != null){
                                    setCode.invoke(returnObj, returnMsg.toString());
                                }
                            }
                        }
                    }
                    return returnObj;
                }else if(NUMBER_TYPES.contains(returnTypeStr)){
                    return 0;
                }else if(CHAR_TYPES.contains(returnTypeStr)){
                    return '\u0000';
                }
            } catch (Exception e) {
                logger.warn("Handle return type failure! ", e);
            }
        }else {
            logger.info(returnMsg.append("check success!").toString());
            try {
                if(!autoReturn){
                Object obj = point.proceed();
                // 打印出参
                logger.info("{}() - result={}, cost:{}ms", methodName, JSON.toJSONString(obj, SerializerFeature.WriteDateUseDateFormat), System.currentTimeMillis()-start);
                return obj;
            } catch (Throwable throwable) {
                logger.error("check exception", throwable);
            }
        }
        return null;
    }


    /**
     * fields 格式校验
     * @param fields
     * @return
     */
    public boolean checkFormat(String fields){
        if(fields != null && fields.matches("\\w+(:\\w+(,\\w+)*)*(\\|\\w+(:\\w+(,\\w+)*)*)*")){
            return true;
        }else{
            logger.error("The format of fields to check is illegal! reference: fields=\"person:age,name|student\"");
            return false;
        }
    }

看看最终效果

方法多个参数‘|’分隔,类内的属性校验':'+','分隔

@NotNull(fields = "configDto:classificationId,classificationName,dataTypeId,dataTypeName," +
        "acquireDataPattern,getDataMode,getDataTime,ruleConfigStatus,operator|test", businessDesc = "新增配置")
@Override
public Result addConfig(TestDto configDto, String test) {
    return testService.addConfig(configDto) ? ResultWrapper.success() : ResultWrapper.fail();
}

运行效果

// 服务端日志
09:40:26.560 [] [DubboServerHandler-thread-200] INFO  ***.aspect.CheckAspect - addConfig() - request=[{"operator":"tony"}]
09:40:26.560 [] [DubboServerHandler-thread-200] ERROR ***.aspect.CheckAspect - TestAgentImpl.addConfig():新增配置 - {"configDto.getDataMode":"not null","configDto.ruleConfigStatus":"not null","configDto.acquireDataPattern":"not null","configDto.classificationId":"not null","configDto.getDataTime":"not null","configDto.classificationName":"not null","configDto.dataTypeId":"not null","configDto.dataTypeName":"not null"}
// 调用方收到的信息
{ status: -1,
message: "TestAgentImpl.addConfig():新增配置 - {"configDto.getDataMode":"not null","configDto.ruleConfigStatus":"not null","configDto.acquireDataPattern":"not null","configDto.classificationId":"not null","configDto.getDataTime":"not null","configDto.classificationName":"not null","configDto.dataTypeId":"not null","configDto.dataTypeName":"not null"}",
data: "" }

如果需要扩展其他校验功能,新增注解,在切面里增加处理逻辑即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值