C++?Protobuf實現接口參數自動校驗詳解
1、背景
用C++做業(yè)務發(fā)開的同學是否還在不厭其煩的編寫大量if-else模塊來做接口參數校驗呢?當接口字段數量多大幾十個,這樣的參數校驗代碼都能多達上百行,甚至超過了接口業(yè)務邏輯的代碼體量,而且隨著業(yè)務迭代,接口增加了新的字段,又不得不再加幾個if-else,對于有Java、python等開發(fā)經歷的同學,對這種原始的參數校驗方法必定是嗤之以鼻。今天,我們就模擬Java里面通過注解實現參數校驗的方式來針對C++ protobuf接口實現一個更加方便、快捷的參數校驗自動工具。

2、方案簡介
實現基本思路主要用到兩個核心技術點:protobuf字段屬性擴展和反射機制。
首先針對常用的協(xié)議字段數據類型(int32、int64、uint32、uint64、float、double、string、array、enum)定義了一套最常用的字段校驗規(guī)則,如下表:

每個校驗規(guī)則的protobuf定義如下:
// int32類型校驗規(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類型校驗規(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類型校驗規(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類型校驗規(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類型校驗規(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類型校驗規(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類型校驗規(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類型校驗規(guī)則
message EnumRule {
repeated int32 in = 1;
}
// array(數組)類型校驗規(guī)則
message ArrayRule {
bool not_empty = 1;
oneof min_len_rule {
uint32 min_len = 2;
}
oneof max_len_rule {
uint32 max_len = 3;
}
}注意:校驗規(guī)則中一些字段通過oneof關鍵字包裝了一層,主要是因為protobuf3中全部字段都默認是optional的,即即使不顯示設置其值,protobuf也會給它一個默認值,如數值類型的一般默認值就是0,這樣當某個規(guī)則的值(如lt)為0的時候,我們無法確定是沒有設置值還是就是設置的0,加了oneof后可以通過oneof字段的xxx_case方法來判斷對應值是否有人為設定。
上述規(guī)則被劃分為4大類:數值類規(guī)則(Int32Rule、Int64Rule、UInt32Rule、UInt64Rule、FloatRule、DoubleRule)、字符串類規(guī)則(StringRule)、枚舉類規(guī)則(EnumRule)、數組類規(guī)則(ArrayRule), 每一類后續(xù)都會有一個對應的校驗器(參數校驗算法)。
然后,拓展protobuf字段屬性(google.protobuf.FieldOptions),將字段校驗規(guī)則拓展為字段屬性之一。如下圖:擴展字段屬性名為Rule, 其類型為ValidateRules,其具體校驗規(guī)則通過oneof關鍵字限定至多為上述9種校驗規(guī)則之一(針對某一個字段,其類型唯一,從而其校驗規(guī)則也是確定的)。
// 校驗規(guī)則(oneof取上述字段類型校驗規(guī)則之一)
message ValidateRules {
oneof rule {
/* 基本類型規(guī)則 */
Int32Rule int32 = 1;
Int64Rule int64 = 2;
UInt32Rule uint32 = 3;
UInt64Rule uint64 = 4;
FloatRule float = 5;
DoubleRule double = 6;
StringRule string = 7;
/* 復雜類型規(guī)則 */
EnumRule enum = 8;
ArrayRule array = 9;
}
}
// 拓展默認字段屬性, 將ValidateRules設置為字段屬性
extend google.protobuf.FieldOptions {
ValidateRules Rule = 10000;
}上述校驗規(guī)則和字段屬性擴展定義在validator.proto文件中,使用時通過import導入該proto文件便可以使用上述擴展字段屬性用于定義字段,如:

說明: 上述接口定義中,通過擴展字段屬性validator.Rule(其內容為上述定義9中類型校驗規(guī)則之一)限制了用戶年齡age字段值必須小于等于(lte)150;名字name字段不能為空且長度不能大于32;手機號字段phone不能為空且必須滿足指定的手機號正則表達式規(guī)則;郵件字段允許為空(默認)但如果有傳入值的話則必須滿足對應郵件正則表達式規(guī)則;others數組字段不允許為空,且長度不小于2。
有了上述接口字段定義后,需要校驗的字段都已經帶上了validator.Rule屬性,其中已包含了對應字段的校驗規(guī)則,接下來需要實現一個參數自動校驗算法, 基本思路就是通過反射逐個獲取待校驗Message結構體中各個字段值及其字段屬性中校驗規(guī)則validator.Rule,然后逐一匹配字段值是否滿足每一項規(guī)則定義,不滿足則返回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兩個字段生成的字段名會加個后綴_(其他字段沒有), 為了在宏里面統(tǒng)一處理加了下面兩個定義 */
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、 使用
整個算法實現相當輕量,規(guī)則定義不到200行,算法實現(也即規(guī)則解析)不到200行。使用方法也非常簡便,只需要在業(yè)務proto中import導入validator.proto即可以使用規(guī)則定義,然后在業(yè)務接口代碼中include<validator_util.h>即可使用規(guī)則校驗工具類對接口參數做自動校驗, 以后接口參數校驗只需要下面幾行就行了(終于不用再寫一大堆if_else了)如下:

4、測試





以上就是C++ Protobuf實現接口參數自動校驗詳解的詳細內容,更多關于C++ Protobuf接口參數校驗的資料請關注腳本之家其它相關文章!
相關文章
VSCode 搭建 Arm 遠程調試環(huán)境的步驟詳解
這篇文章主要介紹了VSCode 搭建 Arm 遠程調試環(huán)境的步驟詳解,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04

