C++?Protobuf實(shí)現(xiàn)接口參數(shù)自動(dòng)校驗(yàn)詳解
1、背景
用C++做業(yè)務(wù)發(fā)開(kāi)的同學(xué)是否還在不厭其煩的編寫(xiě)大量if-else模塊來(lái)做接口參數(shù)校驗(yàn)?zāi)兀慨?dāng)接口字段數(shù)量多大幾十個(gè),這樣的參數(shù)校驗(yàn)代碼都能多達(dá)上百行,甚至超過(guò)了接口業(yè)務(wù)邏輯的代碼體量,而且隨著業(yè)務(wù)迭代,接口增加了新的字段,又不得不再加幾個(gè)if-else,對(duì)于有Java、python等開(kāi)發(fā)經(jīng)歷的同學(xué),對(duì)這種原始的參數(shù)校驗(yàn)方法必定是嗤之以鼻。今天,我們就模擬Java里面通過(guò)注解實(shí)現(xiàn)參數(shù)校驗(yàn)的方式來(lái)針對(duì)C++ protobuf接口實(shí)現(xiàn)一個(gè)更加方便、快捷的參數(shù)校驗(yàn)自動(dòng)工具。
2、方案簡(jiǎn)介
實(shí)現(xiàn)基本思路主要用到兩個(gè)核心技術(shù)點(diǎn):protobuf字段屬性擴(kuò)展和反射機(jī)制。
首先針對(duì)常用的協(xié)議字段數(shù)據(jù)類(lèi)型(int32、int64、uint32、uint64、float、double、string、array、enum)定義了一套最常用的字段校驗(yàn)規(guī)則,如下表:
每個(gè)校驗(yàn)規(guī)則的protobuf定義如下:
// int32類(lèi)型校驗(yàn)規(guī)則 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類(lèi)型校驗(yàn)規(guī)則 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類(lèi)型校驗(yàn)規(guī)則 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類(lèi)型校驗(yàn)規(guī)則 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類(lèi)型校驗(yàn)規(guī)則 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類(lèi)型校驗(yàn)規(guī)則 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類(lèi)型校驗(yàn)規(guī)則 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類(lèi)型校驗(yàn)規(guī)則 message EnumRule { repeated int32 in = 1; } // array(數(shù)組)類(lèi)型校驗(yàn)規(guī)則 message ArrayRule { bool not_empty = 1; oneof min_len_rule { uint32 min_len = 2; } oneof max_len_rule { uint32 max_len = 3; } }
注意:校驗(yàn)規(guī)則中一些字段通過(guò)oneof關(guān)鍵字包裝了一層,主要是因?yàn)閜rotobuf3中全部字段都默認(rèn)是optional的,即即使不顯示設(shè)置其值,protobuf也會(huì)給它一個(gè)默認(rèn)值,如數(shù)值類(lèi)型的一般默認(rèn)值就是0,這樣當(dāng)某個(gè)規(guī)則的值(如lt)為0的時(shí)候,我們無(wú)法確定是沒(méi)有設(shè)置值還是就是設(shè)置的0,加了oneof后可以通過(guò)oneof字段的xxx_case方法來(lái)判斷對(duì)應(yīng)值是否有人為設(shè)定。
上述規(guī)則被劃分為4大類(lèi):數(shù)值類(lèi)規(guī)則(Int32Rule、Int64Rule、UInt32Rule、UInt64Rule、FloatRule、DoubleRule)、字符串類(lèi)規(guī)則(StringRule)、枚舉類(lèi)規(guī)則(EnumRule)、數(shù)組類(lèi)規(guī)則(ArrayRule), 每一類(lèi)后續(xù)都會(huì)有一個(gè)對(duì)應(yīng)的校驗(yàn)器(參數(shù)校驗(yàn)算法)。
然后,拓展protobuf字段屬性(google.protobuf.FieldOptions),將字段校驗(yàn)規(guī)則拓展為字段屬性之一。如下圖:擴(kuò)展字段屬性名為Rule, 其類(lèi)型為ValidateRules,其具體校驗(yàn)規(guī)則通過(guò)oneof關(guān)鍵字限定至多為上述9種校驗(yàn)規(guī)則之一(針對(duì)某一個(gè)字段,其類(lèi)型唯一,從而其校驗(yàn)規(guī)則也是確定的)。
// 校驗(yàn)規(guī)則(oneof取上述字段類(lèi)型校驗(yàn)規(guī)則之一) message ValidateRules { oneof rule { /* 基本類(lèi)型規(guī)則 */ Int32Rule int32 = 1; Int64Rule int64 = 2; UInt32Rule uint32 = 3; UInt64Rule uint64 = 4; FloatRule float = 5; DoubleRule double = 6; StringRule string = 7; /* 復(fù)雜類(lèi)型規(guī)則 */ EnumRule enum = 8; ArrayRule array = 9; } } // 拓展默認(rèn)字段屬性, 將ValidateRules設(shè)置為字段屬性 extend google.protobuf.FieldOptions { ValidateRules Rule = 10000; }
上述校驗(yàn)規(guī)則和字段屬性擴(kuò)展定義在validator.proto文件中,使用時(shí)通過(guò)import導(dǎo)入該proto文件便可以使用上述擴(kuò)展字段屬性用于定義字段,如:
說(shuō)明: 上述接口定義中,通過(guò)擴(kuò)展字段屬性validator.Rule(其內(nèi)容為上述定義9中類(lèi)型校驗(yàn)規(guī)則之一)限制了用戶年齡age字段值必須小于等于(lte)150;名字name字段不能為空且長(zhǎng)度不能大于32;手機(jī)號(hào)字段phone不能為空且必須滿足指定的手機(jī)號(hào)正則表達(dá)式規(guī)則;郵件字段允許為空(默認(rèn))但如果有傳入值的話則必須滿足對(duì)應(yīng)郵件正則表達(dá)式規(guī)則;others數(shù)組字段不允許為空,且長(zhǎng)度不小于2。
有了上述接口字段定義后,需要校驗(yàn)的字段都已經(jīng)帶上了validator.Rule屬性,其中已包含了對(duì)應(yīng)字段的校驗(yàn)規(guī)則,接下來(lái)需要實(shí)現(xiàn)一個(gè)參數(shù)自動(dòng)校驗(yàn)算法, 基本思路就是通過(guò)反射逐個(gè)獲取待校驗(yàn)Message結(jié)構(gòu)體中各個(gè)字段值及其字段屬性中校驗(yàn)規(guī)則validator.Rule,然后逐一匹配字段值是否滿足每一項(xiàng)規(guī)則定義,不滿足則返回FALSE;對(duì)于嵌套結(jié)構(gòu)體類(lèi)型則做遞歸校驗(yàn),算法流程及實(shí)現(xiàn)如下:
#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對(duì)ValidateRules中float和double兩個(gè)字段生成的字段名會(huì)加個(gè)后綴_(其他字段沒(méi)有), 為了在宏里面統(tǒng)一處理加了下面兩個(gè)定義 */ typedef float float_; typedef double double_; /** * 數(shù)值校驗(yàn)器(適用于int32、int64、uint32、uint64、float、double) * 支持大于、大于等于、小于、小于等于、in、not_in校驗(yàn) */ #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; \ } /** * 字符串校驗(yàn)器(string) * 支持字符串非空校驗(yàn)、最短(最長(zhǎng))長(zhǎng)度校驗(yàn)、正則匹配校驗(yàn) */ #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; \ } /** * 枚舉校驗(yàn)器(enum) * 僅支持in校驗(yàn) */ #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; \ } /** * 數(shù)組校驗(yàn)器(array) * 支持?jǐn)?shù)組非空校驗(yàn)、最短(最長(zhǎng))長(zhǎng)度校驗(yàn)以及Message結(jié)構(gòu)體元素遞歸校驗(yàn) */ #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()}; \ } \ } \ \ /* 如果數(shù)組元素是Message結(jié)構(gòu)體類(lèi)型,遞歸校驗(yàn)每個(gè)元素 */ \ 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; \ } \ } \ } /** * 結(jié)構(gòu)體校驗(yàn)器(Message) * (遞歸校驗(yàn)) */ #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()) { // 數(shù)組類(lèi)型校驗(yàn) ArrayValidator(); } else { // 非數(shù)組類(lèi)型,直接調(diào)用對(duì)應(yīng)類(lèi)型校驗(yàn)器 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、 使用
整個(gè)算法實(shí)現(xiàn)相當(dāng)輕量,規(guī)則定義不到200行,算法實(shí)現(xiàn)(也即規(guī)則解析)不到200行。使用方法也非常簡(jiǎn)便,只需要在業(yè)務(wù)proto中import導(dǎo)入validator.proto即可以使用規(guī)則定義,然后在業(yè)務(wù)接口代碼中include<validator_util.h>即可使用規(guī)則校驗(yàn)工具類(lèi)對(duì)接口參數(shù)做自動(dòng)校驗(yàn), 以后接口參數(shù)校驗(yàn)只需要下面幾行就行了(終于不用再寫(xiě)一大堆if_else了)如下:
4、測(cè)試
以上就是C++ Protobuf實(shí)現(xiàn)接口參數(shù)自動(dòng)校驗(yàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于C++ Protobuf接口參數(shù)校驗(yàn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
10個(gè)步驟Opencv輕松檢測(cè)出圖片中條形碼
這篇文章主要為大家詳細(xì)介紹了Opencv輕松檢測(cè)出圖片中條形碼的10個(gè)步驟,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01C語(yǔ)言實(shí)現(xiàn)九大排序算法的實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于C語(yǔ)言實(shí)現(xiàn)九大排序算法的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Qt實(shí)現(xiàn)帶字?jǐn)?shù)限制的文字輸入框
這篇文章介紹了Qt實(shí)現(xiàn)帶字?jǐn)?shù)限制文字輸入框的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04C語(yǔ)言解字符串逆序和單向鏈表逆序問(wèn)題的代碼示例
這篇文章主要介紹了C語(yǔ)言解字符串逆序和單向鏈表逆序問(wèn)題的代碼示例,求逆序也是考研和面試中的基礎(chǔ)算法題類(lèi)型,需要的朋友可以參考下2016-06-06C字符串操作函數(shù)的實(shí)現(xiàn)詳細(xì)解析
以下是對(duì)C語(yǔ)言中字符串操作函數(shù)的實(shí)現(xiàn)進(jìn)行了詳細(xì)的分析介紹,需要的朋友可以過(guò)來(lái)參考下2013-08-08C++連接mysql數(shù)據(jù)庫(kù)并讀取數(shù)據(jù)的具體步驟
在實(shí)際開(kāi)發(fā)中我們經(jīng)常需要對(duì)數(shù)據(jù)庫(kù)進(jìn)行訪問(wèn),針對(duì)不同類(lèi)型的數(shù)據(jù)庫(kù)(如MySQL、sqLite、Access、Excel等),如果采用不同的方法進(jìn)行連接,會(huì)把我們搞崩潰,下面這篇文章主要給大家介紹了關(guān)于C++連接mysql數(shù)據(jù)庫(kù)并讀取數(shù)據(jù)的具體步驟,需要的朋友可以參考下2023-04-04