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

用C++做业务发开的同学是否还在不厌其烦的编写大量if-else模块来做接口参数校验呢?今天,我们就模拟Java里面通过注解实现参数校验的方式来针对C++ protobuf接口实现一个更加方便、快捷的参数校验自动工具,希望对大家有所帮助

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  #include  #include  #include  #include  #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 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.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.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 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 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即可使用规则校验工具类对接口参数做自动校验, 以后接口参数校验只需要下面几行就行了(终于不用再写一大堆if_else了)如下:

4、测试

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

以上就是C++ Protobuf实现接口参数自动校验详解的详细内容,更多请关注0133技术站其它相关文章!

赞(0) 打赏
未经允许不得转载:0133技术站首页 » C语言