C++ Protobuf实现接口参数自动校验详解
用C做业务发开的同学是否还在不厌其烦的编写大量if-else模块来做接口参数校验呢当接口字段数量多大几十个这样的参数校验代码都能多达上百行甚至超过了接口业务逻辑的代码体量而且随着业务迭代接口增加了新的字段又不得不再加几个if-else对于有Java、python等开发经历的同学对这种原始的参数校验方法必定是嗤之以鼻。今天我们就模拟Java里面通过注解实现参数校验的方式来针对C protobuf接口实现一个更加方便、快捷的参数校验自动工具。2、方案简介实现基本思路主要用到两个核心技术点protobuf字段属性扩展和反射机制。首先针对常用的协议字段数据类型int32、int64、uint32、uint64、float、double、string、array、enum定义了一套最常用的字段校验规则如下表每个校验规则的protobuf定义如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135// 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 {floatlt 1;}oneof lte_rule {floatlte 2;}oneof gt_rule {floatgt 3;}oneof gte_rule {floatgte 4;}repeatedfloatin 5;repeatedfloatnot_in 6;}// double类型校验规则message DoubleRule {oneof lt_rule {doublelt 1;}oneof lte_rule {doublelte 2;}oneof gt_rule {doublegt 3;}oneof gte_rule {doublegte 4;}repeateddoublein 5;repeateddoublenot_in 6;}// string类型校验规则message StringRule {boolnot_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 {boolnot_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种校验规则之一(针对某一个字段其类型唯一从而其校验规则也是确定的)。1234567891011121314151617181920212223// 校验规则(oneof取上述字段类型校验规则之一)message ValidateRules {oneof rule {/* 基本类型规则 */Int32Rule int32 1;Int64Rule int64 2;UInt32Rule uint32 3;UInt64Rule uint64 4;FloatRulefloat 5;DoubleRuledouble 6;StringRule string 7;/* 复杂类型规则 */EnumRuleenum 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对于嵌套结构体类型则做递归校验算法流程及实现如下123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185#pragma once#include google/protobuf/message.h#include butil/logging.h#include regex#include algorithm#include sstream#include proto/validator.pb.hnamespacevalidator {usingnamespacegoogle::protobuf;/** 不知道为什么protobuf对ValidateRules中float和double两个字段生成的字段名会加个后缀_(其他字段没有), 为了在宏里面统一处理加了下面两个定义 */typedeffloatfloat_;typedefdoubledouble_;/*** 数值校验器(适用于int32、int64、uint32、uint64、float、double)* 支持大于、大于等于、小于、小于等于、in、not_in校验*/#define NumericalValidator(pb_cpptype, method_type, value_type) \casegoogle::protobuf::FieldDescriptor::CPPTYPE_##pb_cpptype: { \if(validate_rules.has_##value_type()) { \constmethod_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) \casegoogle::protobuf::FieldDescriptor::CPPTYPE_##pb_cpptype: { \if(validate_rules.has_##value_type()) { \constmethod_type##Rule rule validate_rules.value_type(); \constvalue_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) \casegoogle::protobuf::FieldDescriptor::CPPTYPE_##pb_cpptype: { \if(validate_rules.has_##value_type()) { \constmethod_type##Rule rule validate_rules.value_type(); \intvalue 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()) { \constArrayRule 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) { \constMessage sub_message reflection-GetRepeatedMessage(message, field, i); \ValidateResult result Validate(sub_message); \if(!result.is_valid) { \returnresult; \} \} \}/*** 结构体校验器(Message)* (递归校验)*/#define MessageValidator() \casegoogle::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { \constMessage sub_message reflection-GetMessage(message, field); \ValidateResult result Validate(sub_message); \if(!result.is_valid) { \returnresult; \} \break; \}classValidatorUtil {public:structValidateResult {boolis_valid;std::string msg;};staticValidateResult Validate(constMessage message) {constDescriptor* descriptor message.GetDescriptor();constReflection* reflection message.GetReflection();for(inti 0; i descriptor-field_count(); i) {constFieldDescriptor* field descriptor-field(i);FieldDescriptor::CppType field_type field-cpp_type();constValidateRules 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 validator3、 使用整个算法实现相当轻量规则定义不到200行算法实现也即规则解析不到200行。使用方法也非常简便只需要在业务proto中import导入validator.proto即可以使用规则定义然后在业务接口代码中includevalidator_util.h即可使用规则校验工具类对接口参数做自动校验 以后接口参数校验只需要下面几行就行了终于不用再写一大堆if_else了如下4、测试以上就是C Protobuf实现接口参数自动校验详解的详细内容