七夕,带你生撸一个验证框架

【这是一猿小讲的第 39 篇原创分享】

你们之中大概率早已练就了代码的拷贝、粘贴,无敌的码农神功,其实做久了业务功能开发,练就这两个无敌神功,那是迟早的事儿。今天先抛一个小问题,来打通你的任督二脉,就是很好奇的问一下:业务功能开发中,输入参数校验占了你多少时间呢?有没有考虑如何进行验证模块化、通用化?

咱们还是换个问法,看看元芳怎么看。“到底该如何保证业务 API 的健壮性”,元芳你怎么看类?

关注“一猿小讲”公众号久了的元芳,多多少少都有点进步,所以不加思索的答到:首先要保证 API 输入参数的校验;然后……

听到“首先要保证 API 输入参数的校验”,就要给元芳打满分,因为他撬开了咱们今天的话题。

好了,请准备好小板凳,接下来看看如何玩转、生撸一个 API 参数校验的框架。

1. 

常规写法。

640?wx_fmt=png

备注:上面代码截图我仅用来举栗阐述,return 中的信息临时写死,应该动态设置返回码以及返回描述封装为 JSON 返回。

上图代码的栗子,也是众多新手最喜欢用的方式,毕竟在初入职场的程序猿眼中,错误的以为「代码量就是钱」,也可能是想在 SVN 或者 Git 上多留存更新记录,因为更新记录也是能体现代码量及劳动力的啊(捂嘴笑)。

但是往往一个 API 接口不简单只有两个输入参数,多则几十个参数,那岂不是大量代码的篇幅,都在进行参数校验,岂不是会耗费大量的时间精力,在参数校验上。

其实 API 接口入参校验步骤,我们大概可以分为:获取接口输入参数;校验必传参数是否传入;检验参数是否符合规则。

其实一旦找到规律,都可以交给机器去做,那咱们肯定可以打造一款高效的 API 参数校验的轮子。

2. 

轮子应该有什么组成?


A. 接口入参配置;

B. 参数校验规则配置;

C. 封装参数校验失败时异常码以及异常信息;

结合上面的思考,咱们还是画个简单的流程图吧,上一图就秒懂。

640?wx_fmt=png

3. 

轮子应该咋实现?


A. 定义 API 接口入参配置文件 param.properties

########业务功能健壮性参数规则配置############################	
#接口参数配置=Param group(按照|分割参数组,按照逗号分隔每组的参数)	
##########################################################	
#系统接口所需参数配置(PARAMS.apiURL=参数列表)	
PARAMS./sys/login.do=username,password

B. 定义 API 接口参数正则校验 regex.properties

########业务功能健壮性参数规则配置############################	
#接口参数正则表达式配置(PARAMS.接口URL.参数.regex=正则表达式)	
PARAMS./sys/login.do.username.regex=\\w{1,32}	
#全局参数正则表达式配置(参数.regex=正则表达式)	
password.regex=\\w{1,32}

C. 开始生撸代码

第一步:定义如何根据请求的 api 接口获取对应的入参配置。

640?wx_fmt=png

第二步:验证传入参数的值是否符合规则。由于每个 API 支持多组参数传入的情形,所以可以按照“|”分割多组参数。

640?wx_fmt=png

第三步:验证传入参数的值是否符合规则,真正的校验逻辑。

640?wx_fmt=png

第四步:按照咱们的流程图,把上面的方法串在一起,封装成一个 Service,想在哪儿用,在哪儿用,So Easy!!!其中 doService 方法的入参,apiUrl 就是接口的 url,json 是传入的参数。

640?wx_fmt=png


“一猿小讲”公众号是个有态度、有温度的公众号,所以还是一定要把主要代码分享给大家,以便你们更好的进步。

import com.alibaba.fastjson.JSONObject;	
import org.apache.commons.lang3.ArrayUtils;	
import org.apache.commons.lang3.StringUtils;	

	
/**	
 * 请求参数校验服务	
 * desc:	
 * 1、通过传入的apiUrl找到需要校验的数据<b>(param.properties)</b>,	
 * 2、通过传入的参数key找到对应的正则表达式<b>(regex.properties)</b>,	
 * 3、对需要校验的数据进行正则表达式<b>	
 * 4、支持必填非必填字段校验	
 */	
public class RequestParamCheckService {	

	
    public static final String REGKEY = "PARAMS.";	
    public static final String REGEX = ".regex";	

	
    public void doService(String apiUrl, JSONObject json) {	
        //1. 校验 API URL 是否传入	
        if (StringUtils.isEmpty(apiUrl)) {	
            System.out.println("Miss api url");	
            return;	
        }	
        //2. 根据 API URL 获取参数的配置	
        String paramKeyConf = StringUtils.trim(getKeyByApiUrl(apiUrl));	
        if (StringUtils.isEmpty(paramKeyConf)) {	
            System.out.println(String.format("%s found no check info", apiUrl));	
            return;	
        }	
        //3. 验证传入参数的值是否符合配置定义的规则	
        boolean[] validateAry = validate(apiUrl, paramKeyConf, json);	
        //4. 校验参数校验结果	
        if (!ArrayUtils.contains(validateAry, true)) {	
            System.out.println(String.format("%s param validate fail", apiUrl));	
            //TODO 此处需要根据字段获取对应的返回码以及返回信息,可以自行扩展实现一下,本次硬编码	
            throw new ValidateException("99999999", "参数校验失败");	
        }	
    }	

	
    /**	
     * 验证传入参数的值是否符合配置定义的规则	
     *	
     * @param apiUrl       接口	
     * @param paramKeyConf 配置的参数key	
     * @param json         传入的json报文内容	
     * @return 验证结果	
     */	
    private boolean[] validate(String apiUrl, String paramKeyConf, JSONObject json) {	
        // 将KEY分隔	
        String[] chkKeys = paramKeyConf.split("\\|");	
        //判断拆分后的参数是否为组合配置	
        //针对每组参数校验是否传入,最后得出参数传入是否正确	
        boolean[] validateAry = new boolean[chkKeys.length];	
        for (int i = 0; i < chkKeys.length; i++) { //针对每个参数组进行验证	
            String[] unitKeyAry = chkKeys[i].split(","); //按照逗号拆分每组的参数key配置	
            validateAry[i] = check(apiUrl, json, unitKeyAry); //校验参数是否符合规则	
        }	
        return validateAry;	
    }	

	
    /**	
     * 校验传入的参数是否符合规则	
     *	
     * @param apiUrl 校验的apiUrl	
     * @param json   传入的json报文数据	
     * @param keyAry 参数数组	
     * @return 校验结果	
     */	
    private boolean check(String apiUrl, JSONObject json, String[] keyAry) {	
        //验证是否通过,默认为true通过	
        boolean isCheckSucc = true;	
        for (String unitKey : keyAry) {	
            //按照括号拆分,区分是必填还是选填,参数传入就要进行验证正则	
            //获取"("首次出现的位置	
            boolean isOptional = isOptionalParam(unitKey);	
            //非必传	
            if (isOptional) {	
                unitKey = removBrackets(unitKey);	
            }	
            //针对每组参数进行必传校验、针对每组参数进行格式校验	
            if (!check(apiUrl, unitKey, json, !isOptional)) {	
                //验证失败	
                isCheckSucc = false;	
                break;	
            }	
        }	
        return isCheckSucc;	
    }	

	
    /**	
     * 根据apiUrl从配置文件读取key	
     *	
     * @param apiUrl	
     * @return apiUrl 对应的入参配置	
     */	
    public String getKeyByApiUrl(String apiUrl) {	
        //例如:请求的apiUrl为 /sys/login.do 则 key 为 PARAMS./sys/login.do	
        //     其中apiUrl 对应的参数值为 username,password	
        return ConfigUtils.getConfig(REGKEY + apiUrl);	
    }	

	
    /**	
     * 根据apiUrl从配置文件读取入参对应的正则	
     *	
     * @param apiUrl	
     * @return	
     */	
    public String getKeyRegex(String apiUrl, String paramKey) {	
        // 优先根据 PARAMS.接口URL.参数.regex 为key获取对应的正则校验规则	
        String key = REGKEY + apiUrl + "." + paramKey + REGEX;	
        //TODO 根据 key 读取 regex.properties 配置文件获取正则表达式	
        String keyRegex = StringUtils.trim(ConfigUtils.getConfig(key));	
        if (StringUtils.isEmpty(keyRegex)) {	
            // 如果根据 PARAMS.接口URL.参数.regex 为key没有对应的正则校验规则	
            // 则直接根据参数 参数.regex 为key 获取配置的正则校验规则	
            keyRegex = ConfigUtils.getConfig(paramKey + REGEX);	
        }	
        return keyRegex;	
    }	

	
    /**	
     * 真正的校验逻辑,通过配置的正则表达式校验字段是否符合规范	
     *	
     * @param apiUrl   待校验的接口URL	
     * @param paramKey 参数key	
     * @param json     传入的json报文串	
     * @param must     是否必传参数	
     * @return 验证结果	
     */	
    protected boolean check(String apiUrl, String paramKey, JSONObject json, boolean must) {	
        String value = StringUtils.trim(json.getString(paramKey));	
        if (StringUtils.isEmpty(value)) {	
            // 必传字段为空,验证fail	
            if (must) {	
                System.out.println(String.format("Request Parameter %s is Empty", paramKey));	
                return false;	
            }	
        } else {	
            String regex = getKeyRegex(apiUrl, paramKey);	
            if (StringUtils.isEmpty(regex)) {	
                System.out.println(String.format("Request Parameter %s found no regex info", paramKey));	
                return true;	
            }	
            // 去掉空格字符	
            value = value.replaceAll("\\s*", "");	
            // 不符合正则表达式	
            if (!value.matches(regex)) {	
                System.out.println(String.format("Request Parameter %s[%s] cannot match the regex[%s]", paramKey, value, regex));	
                return false;	
            }	
        }	
        return true;	
    }	

	
    /**	
     * 去除中括号	
     *	
     * @param key 待校验的Key	
     * @return 去除中括号的字符串	
     */	
    private String removBrackets(String key) {	
        return key.substring(1, key.length() - 1);	
    }	

	
    /**	
     * 是否是必须的字段	
     *	
     * @param key 待校验的Key	
     * @return 必须字段返回<tt>true</tt>, 否则返回<tt>false</tt>	
     */	
    private boolean isOptionalParam(String key) {	
        return key.startsWith("[") && key.endsWith("]");	
    }	
}	

好了,时间也不早了,生撸验证框架就分享到这儿吧,其实还有很多细节没有深入去说,只是讲了大体的思路,不过代码中注释已经写的很清晰了。如果你能搞明白,并在实际项目中应用,那肯定会大幅度提升开发效率,腾出更多时间去冲咖啡。

最后,愿大家七夕快乐!如果你是单身程序猿(媛),那么看完这篇硬核文章,懂与不懂,都请多点赞、收藏、分享,因为你每一个不经意的神操作,我都认真当成了喜欢。

640?wx_fmt=png

推荐阅读:

码农故事汇

这部技术葵花宝典真的很硬核

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值