首頁 > 軟體

C++ Protobuf實現介面引數自動校驗詳解

2023-08-28 18:05:46

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介面引數校驗的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com