C++ Protobuf实现接口参数自动校验详解

目录
  • 1、背景
  • 2、方案简介
  • 3、 使用
  • 4、测试

1、背景

用C++做业务发开的同学是否还在不厌其烦的编写大量if-else模块来做接口参数校验呢?当接口字段数量多大几十个,这样的参数校验代码都能多达上百行,甚至超过了接口业务逻辑的代码体量,而且随着业务迭代,接口增加了新的字段,又不得不再加几个if-else,对于有Java、python等开发经历的同学,对这种原始的参数校验方法必定是嗤之以鼻。今天,我们就模拟Java里面通过注解实现参数校验的方式来针对C++ protobuf接口实现一个更加方便、快捷的参数校验自动工具。

2、方案简介

实现基本思路主要用到两个核心技术点:protobuf字段属性扩展和反射机制。

首先针对常用的协议字段数据类型(int32、int64、uint32、uint64、float、double、string、array、enum)定义了一套最常用的字段校验规则,如下表:

每个校验规则的protobuf定义如下:

// int32类型校验规则
message Int32Rule {
    oneof lt_rule {
        int32 lt = 1;
    }
    oneof lte_rule {
        int32 lte = 2;
    }
    oneof gt_rule {
        int32 gt = 3;
    }
    oneof gte_rule {
        int32 gte = 4;
    }
    repeated int32 in = 5;
    repeated int32 not_in = 6;
}

// int64类型校验规则
message Int64Rule {
    oneof lt_rule {
        int64 lt = 1;
    }
    oneof lte_rule {
        int64 lte = 2;
    }
    oneof gt_rule {
        int64 gt = 3;
    }
    oneof gte_rule {
        int64 gte = 4;
    }
    repeated int64 in = 5;
    repeated int64 not_in = 6;
}

// uint32类型校验规则
message UInt32Rule {
    oneof lt_rule {
        uint32 lt = 1;
    }
    oneof lte_rule {
        uint32 lte = 2;
    }
    oneof gt_rule {
        uint32 gt = 3;
    }
    oneof gte_rule {
        uint32 gte = 4;
    }
    repeated uint32 in = 5;
    repeated uint32 not_in = 6;
}

// uint64类型校验规则
message UInt64Rule {
    oneof lt_rule {
        uint64 lt = 1;
    }
    oneof lte_rule {
        uint64 lte = 2;
    }
    oneof gt_rule {
        uint64 gt = 3;
    }
    oneof gte_rule {
        uint64 gte = 4;
    }
    repeated uint64 in = 5;
    repeated uint64 not_in = 6;
}

// float类型校验规则
message FloatRule {
    oneof lt_rule {
        float lt = 1;
    }
    oneof lte_rule {
        float lte = 2;
    }
    oneof gt_rule {
        float gt = 3;
    }
    oneof gte_rule {
        float gte = 4;
    }
    repeated float in = 5;
    repeated float not_in = 6;
}

// double类型校验规则
message DoubleRule {
    oneof lt_rule {
        double lt = 1;
    }
    oneof lte_rule {
        double lte = 2;
    }
    oneof gt_rule {
        double gt = 3;
    }
    oneof gte_rule {
        double gte = 4;
    }
    repeated double in = 5;
    repeated double not_in = 6;
}

// string类型校验规则
message StringRule {
    bool not_empty = 1;
    oneof min_len_rule {
        uint32 min_len = 2;
    }
    oneof max_len_rule {
        uint32 max_len = 3;
    }
    string regex_pattern = 4;
}

// enum类型校验规则
message EnumRule {
    repeated int32 in = 1;
}

// array(数组)类型校验规则
message ArrayRule {
    bool not_empty = 1;
    oneof min_len_rule {
        uint32 min_len = 2;
    }
    oneof max_len_rule {
        uint32 max_len = 3;
    }
}

注意:校验规则中一些字段通过oneof关键字包装了一层,主要是因为protobuf3中全部字段都默认是optional的,即即使不显示设置其值,protobuf也会给它一个默认值,如数值类型的一般默认值就是0,这样当某个规则的值(如lt)为0的时候,我们无法确定是没有设置值还是就是设置的0,加了oneof后可以通过oneof字段的xxx_case方法来判断对应值是否有人为设定。

上述规则被划分为4大类:数值类规则(Int32Rule、Int64Rule、UInt32Rule、UInt64Rule、FloatRule、DoubleRule)、字符串类规则(StringRule)、枚举类规则(EnumRule)、数组类规则(ArrayRule), 每一类后续都会有一个对应的校验器(参数校验算法)。

然后,拓展protobuf字段属性(google.protobuf.FieldOptions),将字段校验规则拓展为字段属性之一。如下图:扩展字段属性名为Rule, 其类型为ValidateRules,其具体校验规则通过oneof关键字限定至多为上述9种校验规则之一(针对某一个字段,其类型唯一,从而其校验规则也是确定的)。

// 校验规则(oneof取上述字段类型校验规则之一)
message ValidateRules {
    oneof rule {
        /* 基本类型规则 */
        Int32Rule int32  = 1;
        Int64Rule int64  = 2;
        UInt32Rule uint32  = 3;
        UInt64Rule uint64  = 4;
        FloatRule float = 5;
        DoubleRule double = 6;
        StringRule string = 7;

        /* 复杂类型规则 */
        EnumRule enum = 8;
        ArrayRule array = 9;
    }
}

// 拓展默认字段属性, 将ValidateRules设置为字段属性
extend google.protobuf.FieldOptions {
    ValidateRules Rule = 10000;
}

上述校验规则和字段属性扩展定义在validator.proto文件中,使用时通过import导入该proto文件便可以使用上述扩展字段属性用于定义字段,如:

说明: 上述接口定义中,通过扩展字段属性validator.Rule(其内容为上述定义9中类型校验规则之一)限制了用户年龄age字段值必须小于等于(lte)150;名字name字段不能为空且长度不能大于32;手机号字段phone不能为空且必须满足指定的手机号正则表达式规则;邮件字段允许为空(默认)但如果有传入值的话则必须满足对应邮件正则表达式规则;others数组字段不允许为空,且长度不小于2。

有了上述接口字段定义后,需要校验的字段都已经带上了validator.Rule属性,其中已包含了对应字段的校验规则,接下来需要实现一个参数自动校验算法, 基本思路就是通过反射逐个获取待校验Message结构体中各个字段值及其字段属性中校验规则validator.Rule,然后逐一匹配字段值是否满足每一项规则定义,不满足则返回FALSE;对于嵌套结构体类型则做递归校验,算法流程及实现如下:

#pragma once

#include <google/protobuf/message.h>
#include <butil/logging.h>
#include <regex>
#include <algorithm>
#include <sstream>
#include "proto/validator.pb.h"

namespace validator {

using namespace google::protobuf;

/** 不知道为什么protobuf对ValidateRules中float和double两个字段生成的字段名会加个后缀_(其他字段没有), 为了在宏里面统一处理加了下面两个定义 */
typedef float float_;
typedef double double_;

/**
 * 数值校验器(适用于int32、int64、uint32、uint64、float、double)
 * 支持大于、大于等于、小于、小于等于、in、not_in校验
*/
#define NumericalValidator(pb_cpptype, method_type, value_type)                                    \
    case google::protobuf::FieldDescriptor::CPPTYPE_##pb_cpptype: {                                \
        if (validate_rules.has_##value_type()) {                                                   \
            const method_type##Rule& rule = validate_rules.value_type();                           \
            value_type value              = reflection->Get##method_type(message, field);          \
            if ((rule.lt_rule_case() && value >= rule.lt()) ||                                     \
                (rule.lte_rule_case() && value > rule.lte()) ||                                    \
                (rule.gt_rule_case() && value <= rule.gt()) ||                                     \
                (rule.gte_rule_case() && value < rule.gte())) {                                    \
                std::ostringstream os;                                                             \
                os << field->full_name() << " value out of range.";                                \
                return {false, os.str()};                                                          \
            }                                                                                      \
            if ((!rule.in().empty() &&                                                             \
                 std::find(rule.in().begin(), rule.in().end(), value) == rule.in().end()) ||       \
                (!rule.not_in().empty() &&                                                         \
                 std::find(rule.not_in().begin(), rule.not_in().end(), value) !=                   \
                     rule.not_in().end())) {                                                       \
                std::ostringstream os;                                                             \
                os << field->full_name() << " value not allowed.";                                 \
                return {false, os.str()};                                                          \
            }                                                                                      \
        }                                                                                          \
        break;                                                                                     \
    }

/**
 * 字符串校验器(string)
 * 支持字符串非空校验、最短(最长)长度校验、正则匹配校验
*/
#define StringValidator(pb_cpptype, method_type, value_type)                                       \
    case google::protobuf::FieldDescriptor::CPPTYPE_##pb_cpptype: {                                \
        if (validate_rules.has_##value_type()) {                                                   \
            const method_type##Rule& rule = validate_rules.value_type();                           \
            const value_type& value       = reflection->Get##method_type(message, field);          \
            if (rule.not_empty() && value.empty()) {                                               \
                std::ostringstream os;                                                             \
                os << field->full_name() << " can not be empty.";                                  \
                return {false, os.str()};                                                          \
            }                                                                                      \
            if ((rule.min_len_rule_case() && value.length() < rule.min_len()) ||                   \
                (rule.max_len_rule_case() && value.length() > rule.max_len())) {                   \
                std::ostringstream os;                                                             \
                os << field->full_name() << " length out of range.";                               \
                return {false, os.str()};                                                          \
            }                                                                                      \
            if (!value.empty() && !rule.regex_pattern().empty()) {                                 \
                std::regex ex(rule.regex_pattern());                                               \
                if (!regex_match(value, ex)) {                                                     \
                    std::ostringstream os;                                                         \
                    os << field->full_name() << " format invalid.";                                \
                    return {false, os.str()};                                                      \
                }                                                                                  \
            }                                                                                      \
        }                                                                                          \
        break;                                                                                     \
    }

/**
 * 枚举校验器(enum)
 * 仅支持in校验
*/
#define EnumValidator(pb_cpptype, method_type, value_type)                                          \
    case google::protobuf::FieldDescriptor::CPPTYPE_##pb_cpptype: {                                 \
        if (validate_rules.has_##value_type()) {                                                    \
            const method_type##Rule& rule = validate_rules.value_type();                            \
            int value                     = reflection->Get##method_type(message, field)->number(); \
            if (!rule.in().empty() &&                                                               \
                std::find(rule.in().begin(), rule.in().end(), value) == rule.in().end()) {          \
                std::ostringstream os;                                                              \
                os << field->full_name() << " value not allowed.";                                  \
                return {false, os.str()};                                                           \
            }                                                                                       \
        }                                                                                           \
        break;                                                                                      \
    }

/**
 * 数组校验器(array)
 * 支持数组非空校验、最短(最长)长度校验以及Message结构体元素递归校验
*/
#define ArrayValidator()                                                                           \
    uint32 arr_len = (uint32)reflection->FieldSize(message, field);                                \
    if (validate_rules.has_array()) {                                                              \
        const ArrayRule& rule = validate_rules.array();                                            \
        if (rule.not_empty() && arr_len == 0) {                                                    \
            std::ostringstream os;                                                                 \
            os << field->full_name() << " can not be empty.";                                      \
            return {false, os.str()};                                                              \
        }                                                                                          \
        if ((rule.min_len() != 0 && arr_len < rule.min_len()) ||                                   \
            (rule.max_len() != 0 && arr_len > rule.max_len())) {                                   \
            std::ostringstream os;                                                                 \
            os << field->full_name() << " length out of range.";                                   \
            return {false, os.str()};                                                              \
        }                                                                                          \
    }                                                                                              \
                                                                                                   \
    /* 如果数组元素是Message结构体类型,递归校验每个元素 */                   \
    if (field_type == FieldDescriptor::CPPTYPE_MESSAGE) {                                          \
        for (uint32 i = 0; i < arr_len; i++) {                                                     \
            const Message& sub_message = reflection->GetRepeatedMessage(message, field, i);        \
            ValidateResult&& result    = Validate(sub_message);                                    \
            if (!result.is_valid) {                                                                \
                return result;                                                                     \
            }                                                                                      \
        }                                                                                          \
    }

/**
 * 结构体校验器(Message)
 * (递归校验)
*/
#define MessageValidator()                                                                         \
    case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: {                                     \
        const Message& sub_message = reflection->GetMessage(message, field);                       \
        ValidateResult&& result    = Validate(sub_message);                                        \
        if (!result.is_valid) {                                                                    \
            return result;                                                                         \
        }                                                                                          \
        break;                                                                                     \
    }

class ValidatorUtil {
public:
    struct ValidateResult {
        bool is_valid;
        std::string msg;
    };

    static ValidateResult Validate(const Message& message) {
        const Descriptor* descriptor = message.GetDescriptor();
        const Reflection* reflection = message.GetReflection();

        for (int i = 0; i < descriptor->field_count(); i++) {
            const FieldDescriptor* field        = descriptor->field(i);
            FieldDescriptor::CppType field_type = field->cpp_type();
            const ValidateRules& validate_rules = field->options().GetExtension(validator::Rule);

            if (field->is_repeated()) {
                // 数组类型校验
                ArrayValidator();
            } else {
                // 非数组类型,直接调用对应类型校验器
                switch (field_type) {
                    NumericalValidator(INT32, Int32, int32);
                    NumericalValidator(INT64, Int64, int64);
                    NumericalValidator(UINT32, UInt32, uint32);
                    NumericalValidator(UINT64, UInt64, uint64);
                    NumericalValidator(FLOAT, Float, float_);
                    NumericalValidator(DOUBLE, Double, double_);
                    StringValidator(STRING, String, string);
                    EnumValidator(ENUM, Enum, enum_);
		    MessageValidator();
                    default:
                        break;
                }
            }
        }
        return {true, ""};
    }
};

} // namespace validator

3、 使用

整个算法实现相当轻量,规则定义不到200行,算法实现(也即规则解析)不到200行。使用方法也非常简便,只需要在业务proto中import导入validator.proto即可以使用规则定义,然后在业务接口代码中include<validator_util.h>即可使用规则校验工具类对接口参数做自动校验, 以后接口参数校验只需要下面几行就行了(终于不用再写一大堆if_else了)如下:

4、测试

以上就是C++ Protobuf实现接口参数自动校验详解的详细内容,更多关于C++ Protobuf接口参数校验的资料请关注我们其它相关文章!

(0)

相关推荐

  • ProtoBuf动态拆分Gradle Module解析

    目录 预期 buf.yaml 模板工程 deps 转化 多线程操作 加载壳Module 结尾 预期 当前安卓的所有proto都生成在一个module中,但是其实业务同学需要的并不是一个大杂烩, 只需要其中他们所关心的proto生成的类则足以.所以我们希望能将这样一个大杂烩的仓库打散,拆解成多个module. buf.yaml Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式,并于2008年对外开源.Pr

  • python下grpc与protobuf的编写使用示例

    目录 1. python下protobuf使用 1.1 安装protobuf 1.2 protobuf3定义格式 1.3 生成proto的python文件 1.4 对比一下protobuf生成的效果 2.python下grpc使用 2.1编写hello.proto文件 2.2 生成proto的python文件 2.3 编写server端 2.4 编写cilent端 1. python下protobuf使用 1.1 安装protobuf pip3.6 install grpcio #安装grpc

  • C++开发protobuf动态解析工具

    目录 为什么需要这个工具 需求描述 搜索现成方案 AST在哪里 开始写代码 第一步 第2步 第3步 第4步 总结 为什么需要这个工具 数据库中存储的protobuf序列化的内容,有时候查问题想直接解析查看内容.很多编码在网上很容易找到编解码工具,但protobuf没有找到编解码工具,可能这样的需求比较少吧,那就自己用C++实现一个. 需求描述 我们知道,要解析protobuf,需要有proto定义,所以我们的输入参数需要包含序列化的数据以及proto定义,如果proto中包含多个message,

  • 利用C++开发一个protobuf动态解析工具

    目录 为什么需要这个工具 需求描述 开发 搜索现成方案 AST在哪里 开始写代码 总结 为什么需要这个工具 数据库中存储的protobuf序列化的内容,有时候查问题想直接解析查看内容.很多编码在网上很容易找到编解码工具,但protobuf没有找到编解码工具,可能这样的需求比较少吧,那就自己用C++实现一个. 需求描述 我们知道,要解析protobuf,需要有proto定义,所以我们的输入参数需要包含序列化的数据以及proto定义,如果proto中包含多个message,还需要指定解析到哪个mes

  • Thinkphp5微信小程序获取用户信息接口的实例详解

    Thinkphp5微信小程序获取用户信息接口的实例详解 首先在官网下载示例代码, 选php的, 这里有个坑 官方的php文件,编码是UTF-8+的, 所以要把文件改为UTF-8 然后在Thinkphp5 extend文件夹下建立Wxxcx命名空间,把官方的几个类文件放进去(这里要注意文件夹名, 命名空间名, 类名的, 大小写,一定要一样,官方的文件名和类名大小写不一样) 然后是自己的thinkphp接口代码: <?php /** * Created by PhpStorm. * User: le

  • java 接口回调实例详解

    java 接口回调实例详解 首先官方对接口回调的定义是这样的,所谓回调:就是A类中调用B类中的某个方法C,然后B类中反过来调用A类中的方法D,D这个方法就叫回调方法.这样听起来有点绕,我们可以这么理解接口回调:比如我们想知道隔壁老王啥时候回家?但是我们有自己的事情做不能一直监视着老王,那么我们可以雇员小区的保安来完成这个任务,当老王回家口,保安就给我们打电话告诉我们,老王回来了!这样就完成了一个事件的传递: 首先我们定义了一个接口: public interface DynamicMessage

  • java Future 接口使用方法详解

    java Future 接口使用方法详解 在Java中,如果需要设定代码执行的最长时间,即超时,可以用Java线程池ExecutorService类配合Future接口来实现. Future接口是Java标准API的一部分,在java.util.concurrent包中.Future接口是Java线程Future模式的实现,可以来进行异步计算. Future模式可以这样来描述:我有一个任务,提交给了Future,Future替我完成这个任务.期间我自己可以去做任何想做的事情.一段时间之后,我就便

  • Android Parcelable接口使用方法详解

     Android Parcelable接口使用方法详解 1. Parcelable接口 Interface for classes whose instances can be written to and restored from a Parcel. Classes implementing the Parcelable interface must also have a static field called CREATOR, which is an object implementin

  • 对python调用RPC接口的实例详解

    要调用RPC接口,python提供了一个框架grpc,这是google开源的 rpc相关文档: https://grpc.io/docs/tutorials/basic/python.html 需要安装的python包如下: 1.grpc安装 pip install grpcio 2.grpc的python protobuf相关的编译工具 pip install grpcio-tools 3.protobuf相关python依赖库 pip install protobuf 4.一些常见原型的生成

  • SpringBoot之使用枚举参数案例详解

    接口开发过程中不免有表示类型的参数,比如 0 表示未知,1 表示男,2 表示女.通常有两种做法,一种是用数字表示,另一种是使用枚举实现. 使用数字表示就是通过契约形式,约定每个数字表示的含义,接口接收到参数,就按照约定对类型进行判断,接口维护成本比较大. 在 Spring 体系中,使用枚举表示,是借助 Spring 的 Converter 机制,可以将数字或字符串对应到枚举的序号或者 name,然后将前端的输入转换为枚举类型. 在场景不复杂的场景中,枚举可以轻松胜任. 于是,迅速实现逻辑,准备提

  • Go语言基础go接口用法示例详解

    目录 概述 语法 定义接口 实现接口 空接口 接口的组合 总结 概述 Go 语言中的接口就是方法签名的集合,接口只有声明,没有实现,不包含变量. 语法 定义接口 type [接口名] interface { 方法名1(参数列表) 返回值列表 方法名2(参数列表) 返回值列表 ... } 例子 type Isay interface{ sayHi() } 实现接口 例子 //定义接口的实现类 type Chinese struct{} //实现接口 func (_ *Chinese) sayHi(

  • Java+TestNG接口自动化入门详解

    目录 一.环境准备:(根据自己电脑配置来选择安装版本,我的电脑是64位,所以此处选择64位安装) 二.环境安装: 三.TestNG接口自动化实现 四.创建自己的第一个接口自动化脚本 五.批量执行自动化脚本 六.生成并查看自动化测试报告 一.环境准备:(根据自己电脑配置来选择安装版本,我的电脑是64位,所以此处选择64位安装) JDK下载: JDK 1.8下载地址: http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downl

  • Java中接口的多态详解

    目录 多态参数 多态数组 接口的多态传递现象 总结 多态参数 就像我们现实生活中电脑的usb接口,我们既可以接受手机对象,又可以接受相机对象,等等,体现了接口的多态,查看以下代码 接口: package InterfaceM; public interface Interface { public void join(); public void stop(); } 手机类: package InterfaceM; public class Phone implements Interface{

  • SpringBoot使用AOP实现统计全局接口访问次数详解

    目录 AOP是什么 AOP的作用和优势 常见的动态代理技术 AOP相关概念 实现 AOP是什么 AOP(Aspect Oriented Programming),也就是面向切面编程,是通过预编译方式和运行期间动态代理实现程序功能的传统已维护的一种技术. AOP的作用和优势 作用:在程序运行期间,在不修改源代码的情况下对某些方法进行功能增强 优势:减少重复代码,提高开发效率,并且便于维护 常见的动态代理技术 jdk代理:基于接口的动态代理技术 cglib代理:基于父类的动态代理技术 AOP相关概念

随机推荐