日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

巧用 Protobuf 反射来优化代码,拒做 PB Boy

發布時間:2024/2/28 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 巧用 Protobuf 反射来优化代码,拒做 PB Boy 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

作者:iversonluo,騰訊 WXG 應用開發工程師

有些后臺同學將自己稱為 SQL Boy,因為負責的業務主要是對數據庫進行增刪改查。經常和 Proto 打交道的同學,是不是也會叫自己 PB Boy?因為大部分工作也是對 Proto 進行 SET 和 GET。面對大量重復且丑陋的代碼,除了宏是否有更好的解決方法?本文結合 PB 反射給出了我在運營系統開發工作中的一些代碼優化實踐。

一、背景

Protobuf(下文稱為 PB)是一種常見的數據序列化方式,常常用于后臺微服務之間傳遞數據。

筆者目前主要的工作都是和表單打交道,而表單一般涉及到大量的數據輸入,表單調用方一般將數據格式化為 JSON 后傳給 CGI,而 CGI 和后臺服務、后臺服務之前會用 PB 傳遞數據。

在寫代碼時,經常會遇到一些丑陋的、圈復雜度較高、較難維護的關于 PB 的使用代碼:

  • 對字段的必填校驗硬編碼在代碼中:如果需要變更校驗規則,則需要修改代碼;

  • 一個字段一個 if 校驗,圈復雜度較高:對傳進來的字段每個字段都進行多種規則校驗,例如長度,XSS,正則校驗等,一個校驗一個 if 代碼,代碼圈復雜度很高;

  • 想要獲取 PB 中所有的非空字段,形成一個 map<string,string>,需要大量的 if 判斷和重復代碼;

  • 在后臺服務間傳遞數據,由于模塊由不同的人開發,導致相同字段的命名不一樣,從一個 PB 中挑選一部分內容到另外一個 PB 中,需要大量的 GET 和 SET 代碼。

  • 是否可以有方法解決上面的幾個問題呢?

    答案是使用PB 反射

    二、PB 反射的使用

    反射的一般定義如下:計算機程序在運行時可以訪問、檢測和修改它本身狀態或行為。

    protobuf 的類圖如下:

    從上圖我們可以看出,Message 類繼承于 MessageLite 類,業務一般自定義的 Person 類繼承于 Message 類。

    Descriptor 類和 Reflection 類都聚合于 Message,是弱依賴的關系。

    類名類描述
    Descriptor對 Message 進行描述,包括 message 的名字、所有字段的描述、原始 proto 文件內容等
    FieldDescriptor對 Message 中單個字段進行描述,包括字段名、字段屬性、原始的 field 字段等
    Reflection提供了動態讀和寫 message 中單個字段能力

    所以一般使用 PB 反射的步驟如下:

    1.?通過Message獲取單個字段的FieldDescriptor 2.?通過Message獲取其Reflection 3.?通過Reflection來操作FieldDescriptor,從而動態獲取或修改單個字段

    獲取 Descript、Reflection 的函數:

    const?google::protobuf::Reflection*?pReflection?=?pMessage->GetReflection(); const?google::protobuf::Descriptor*?pDescriptor?=?pMessage->GetDescriptor();

    獲取 FieldDescriptor 的函數:

    const?google::protobuf::FieldDescriptor?*?pFieldDesc?=?pDescriptor->FindFieldByName(id);

    下面分別介紹上面的三個類。

    2.1 類 Descriptor 介紹

    類 Descriptor 主要是對 Message 進行描述,包括 message 的名字、所有字段的描述、原始 proto 文件內容等,下面介紹該類中包含的函數。

    首先是獲取自身信息的函數:

    const?std::string?&?name()?const;?//?獲取message自身名字 int?field_count()?const;?//?獲取該message中有多少字段 const?FileDescriptor*?file()?const;?//?The?.proto?file?in?which?this?message?type?was?defined.?Never?nullptr.

    在類 Descriptor 中,可以通過如下方法獲取類 FieldDescriptor:

    const?FieldDescriptor*?field(int?index)?const;?//?根據定義順序索引獲取,即從0開始到最大定義的條目 const?FieldDescriptor*?FindFieldByNumber(int?number)?const;?//?根據定義的message里面的順序值獲取(option?string?name=3,3即為number) const?FieldDescriptor*?FindFieldByName(const?string&?name)?const;?//?根據field?name獲取 const?FieldDescriptor*?Descriptor::FindFieldByLowercaseName(const?std::string?&?lowercase_name)const;?//?根據小寫的field?name獲取 const?FieldDescriptor*?Descriptor::FindFieldByCamelcaseName(const?std::string?&?camelcase_name)?const;?//?根據駝峰的field?name獲取

    其中FieldDescriptor* field(int index)和FieldDescriptor* FindFieldByNumber(int number)這個函數中index和number的含義是不一樣的,如下所示:

    message?Student{optional?string?name?=?1;optional?string?gender?=?2;optional?string?phone?=?5; }

    其中字段phone,其index為 5,但是其number為 2。

    同時還有一個我們在調試中經常使用的函數:

    std::string?Descriptor::DebugString();?//?將message轉化成人可以識別出的string信息

    2.2 類 FieldDescriptor 介紹

    類 FieldDescriptor 的作用主要是對 Message 中單個字段進行描述,包括字段名、字段屬性、原始的 field 字段等。

    其獲取獲取自身信息的函數:

    const?std::string?&?name()?const;?//?Name?of?this?field?within?the?message. const?std::string?&?lowercase_name()?const;?//?Same?as?name()?except?converted?to?lower-case. const?std::string?&?camelcase_name()?const;?//?Same?as?name()?except?converted?to?camel-case. CppType?cpp_type()?const;?//C++?type?of?this?field.

    其中cpp_type()函數是來獲取該字段是什么類型的,在 PB 中,類型的類目如下:

    enum?FieldDescriptor::Type?{TYPE_DOUBLE?=?=?1,TYPE_FLOAT?=?=?2,TYPE_INT64?=?=?3,TYPE_UINT64?=?=?4,TYPE_INT32?=?=?5,TYPE_FIXED64?=?=?6,TYPE_FIXED32?=?=?7,TYPE_BOOL?=?=?8,TYPE_STRING?=?=?9,TYPE_GROUP?=?=?10,TYPE_MESSAGE?=?=?11,TYPE_BYTES?=?=?12,TYPE_UINT32?=?=?13,TYPE_ENUM?=?=?14,TYPE_SFIXED32?=?=?15,TYPE_SFIXED64?=?=?16,TYPE_SINT32?=?=?17,TYPE_SINT64?=?=?18,MAX_TYPE?=?=?18 }

    類 FieldDescriptor 中還可以判斷字段是否是必填,還是選填或者重復:

    bool?is_required()?const;?//?判斷字段是否是必填 bool?is_optional()?const;?//?判斷字段是否是選填 bool?is_repeated()?const;?//?判斷字段是否是重復值

    類 FieldDescriptor 中還可以獲取單個字段的index或者tag:

    int?number()?const;?//?Declared?tag?number. int?index()?const;?//Index?of?this?field?within?the?message's?field?array,?or?the?file?or?extension?scope's?extensions?array.

    類 FieldDescriptor 中還有一個支持擴展的函數,函數如下:

    //?Get?the?FieldOptions?for?this?field.??This?includes?things?listed?in //?square?brackets?after?the?field?definition.??E.g.,?the?field: //???optional?string?text?=?1?[ctype=CORD]; //?has?the?"ctype"?option?set.??Allowed?options?are?defined?by?FieldOptions?in //?descriptor.proto,?and?any?available?extensions?of?that?message. const?FieldOptions?&?FieldDescriptor::options()?const

    具體關于該函數的講解在 2.4 章。

    2.3 類 Reflection 介紹

    該類提供了動態讀、寫 message 中單個字段能力。

    讀單個字段的函數如下:

    //?這里由于篇幅,省略了一部分代碼,后面的代碼部分也有省略,有需要的可以自行閱讀源碼。 int32?GetInt32(const?Message?&?message,?const?FieldDescriptor?*?field)?conststd::string?GetString(const?Message?&?message,?const?FieldDescriptor?*?field)?constconst?Message?&?GetMessage(const?Message?&?message,?const?FieldDescriptor?*?field,?MessageFactory?*?factory?=?nullptr)?const?//?讀取單個message字段

    寫單個字段的函數如下:

    void?SetInt32(Message?*?message,?const?FieldDescriptor?*?field,?int32?value)?constvoid?SetString(Message?*?message,?const?FieldDescriptor?*?field,?std::string?value)?const

    獲取重復字段的函數如下:

    int32?GetRepeatedInt32(const?Message?&?message,?const?FieldDescriptor?*?field,?int?index)?conststd::string?GetRepeatedString(const?Message?&?message,?const?FieldDescriptor?*?field,?int?index)?constconst?Message?&?GetRepeatedMessage(const?Message?&?message,?const?FieldDescriptor?*?field,?int?index)?const

    寫重復字段的函數如下:

    void?SetRepeatedInt32(Message?*?message,?const?FieldDescriptor?*?field,?int?index,?int32?value)?constvoid?SetRepeatedString(Message?*?message,?const?FieldDescriptor?*?field,?int?index,?std::string?value)?constvoid?SetRepeatedEnumValue(Message?*?message,?const?FieldDescriptor?*?field,?int?index,?int?value)?const?//?Set?an?enum?field's?value?with?an?integer?rather?than?EnumValueDescriptor.?more..

    新增重復字段設計如下:

    void?AddInt32(Message?*?message,?const?FieldDescriptor?*?field,?int32?value)?constvoid?AddString(Message?*?message,?const?FieldDescriptor?*?field,?std::string?value)?const

    另外有一個較為重要的函數,其可以批量獲取字段描述并將其放置到 vector 中:

    void?Reflection::ListFields(const?Message?&?message,?std::vector<?const?FieldDescriptor?*?>?*?output)?const

    2.4 options 介紹

    PB 允許在 proto 中自定義選項并使用選項。在定義 message 的字段時,不僅可以定義字段內容,還可以設置字段的屬性,比如校驗規則,簡介等,結合反射,可以實現豐富豐富多彩的應用。

    下面來介紹下:

    import?"google/protobuf/descriptor.proto";extend?google.protobuf.FieldOptions?{optional?uint32?attr_id??????????????=?50000;?//字段idoptional?bool?is_need_encrypt????????=?50001?[default?=?false];?//?字段是否加密,0代表不加密,1代表加密optional?string?naming_conventions1??=?50002;?//?商戶組命名規范optional?uint32?length_min???????????=?50003??[default?=?0];?//?字段最小長度optional?uint32?length_max???????????=?50004??[default?=?1024];?//?字段最大長度optional?string?regex????????????????=?50005;?//?該字段的正則表達式 }message?SubMerchantInfo?{//?商戶名稱optional?string?merchant_name?=?1?[(attr_id)?=?1,(is_encrypt)?=?0,(naming_conventions1)?=?"company_name",(length_min)?=?1,(length_max)?=?80,(regex.field_rules)?=?"[a-zA-Z0-9]"];

    使用方法如下:

    #include?<google/protobuf/descriptor.h> #include?<google/protobuf/message.h>std::string?strRegex?=?FieldDescriptor->options().GetExtension(regex);uint32?dwLengthMinp?=?FieldDescriptor->options().GetExtension(length_min);bool?bIsNeedEncrypt?=?FieldDescriptor->options().GetExtension(is_need_encrypt);

    三、PB 反射的進階使用

    第二章給出了 PB 反射,以及具體的使用細節,在本章中,作者結合自己日常的代碼,給出 PB 反射一些使用場景。并且以開發一個表單系統為例,講一下 PB 反射在開發表單系統中的進階使用。

    3.1 獲取 PB 中所有非空字段

    在業務中,經常會需要獲取某個 Message 中所有非空字段,形成一個 map<string,string>,使用 PB 反射寫法如下:

    #include?"pb_util.h"#include?<sstream>namespace?comm_tools?{ int?PbToMap(const?google::protobuf::Message?&message,std::map<std::string,?std::string>?&out)?{ #define?CASE_FIELD_TYPE(cpptype,?method,?valuetype)????????????????????????????\case?google::protobuf::FieldDescriptor::CPPTYPE_##cpptype:?{?????????????????\valuetype?value?=?reflection->Get##method(message,?field);?????????????????\std::ostringstream?oss;????????????????????????????????????????????????????\oss?<<?value;??????????????????????????????????????????????????????????????\out[field->name()]?=?oss.str();????????????????????????????????????????????\break;?????????????????????????????????????????????????????????????????????\}#define?CASE_FIELD_TYPE_ENUM()?????????????????????????????????????????????????\case?google::protobuf::FieldDescriptor::CPPTYPE_ENUM:?{??????????????????????\int?value?=?reflection->GetEnum(message,?field)->number();?????????????????\std::ostringstream?oss;????????????????????????????????????????????????????\oss?<<?value;??????????????????????????????????????????????????????????????\out[field->name()]?=?oss.str();????????????????????????????????????????????\break;?????????????????????????????????????????????????????????????????????\}#define?CASE_FIELD_TYPE_STRING()???????????????????????????????????????????????\case?google::protobuf::FieldDescriptor::CPPTYPE_STRING:?{????????????????????\std::string?value?=?reflection->GetString(message,?field);?????????????????\out[field->name()]?=?value;????????????????????????????????????????????????\break;?????????????????????????????????????????????????????????????????????\}const?google::protobuf::Descriptor?*descriptor?=?message.GetDescriptor();const?google::protobuf::Reflection?*reflection?=?message.GetReflection();for?(int?i?=?0;?i?<?descriptor->field_count();?i++)?{const?google::protobuf::FieldDescriptor?*field?=?descriptor->field(i);bool?has_field?=?reflection->HasField(message,?field);if?(has_field)?{if?(field->is_repeated())?{return?-1;?//?不支持轉換repeated字段}const?std::string?&field_name?=?field->name();switch?(field->cpp_type())?{CASE_FIELD_TYPE(INT32,?Int32,?int);CASE_FIELD_TYPE(UINT32,?UInt32,?uint32_t);CASE_FIELD_TYPE(FLOAT,?Float,?float);CASE_FIELD_TYPE(DOUBLE,?Double,?double);CASE_FIELD_TYPE(BOOL,?Bool,?bool);CASE_FIELD_TYPE(INT64,?Int64,?int64_t);CASE_FIELD_TYPE(UINT64,?UInt64,?uint64_t);CASE_FIELD_TYPE_ENUM();CASE_FIELD_TYPE_STRING();default:return?-1;?//?其他異常類型}}}return?0; } }?//?namespace?comm_tools

    通過上面的代碼,如果需要在 proto 中增加字段,不再需要修改原來的代碼。

    3.2 將字段校驗規則放置在 Proto 中

    后臺服務接收到前端傳來的字段后,會對字段進行校驗,比如必填校驗,長度校驗,正則校驗,xss 校驗等,這些規則我們常常會硬編碼在代碼中。但是隨著后臺字段的增加,校驗規則代碼會變得越來越多,越來越難維護。如果我們把字段的定義和校驗規則和定義放在一起,這樣是不是更好的維護?

    示例 proto 如下:

    syntax?=?"proto2";package?student;import?"google/protobuf/descriptor.proto";message?FieldRule{optional?uint32?length_min?=?1;?//?字段最小長度optional?uint32?id?????????=?2;?//?字段映射id }extend?google.protobuf.FieldOptions{optional?FieldRule?field_rule?=?50000; }message?Student{optional?string?name???=1?[(field_rule).length_min?=?5,?(field_rule).id?=?1];optional?string?email?=?2?[(field_rule).length_min?=?10,?(field_rule).id?=?2]; }

    然后我們自己實現 xss 校驗,必填校驗,長度校驗,選項校驗等代碼。

    示例校驗最小長度代碼如下:

    #include?<iostream> #include?"student.pb.h" #include?<google/protobuf/descriptor.h> #include?<google/protobuf/message.h>using?namespace?std; using?namespace?student; using?namespace?google::protobuf;bool?minLengthCheck(const?std::string?&strValue,?const?uint32_t?&dwLenthMin)?{return?strValue.size()?<?dwLenthMin; }int?allCheck(const?google::protobuf::Message?&oMessage){const?auto?*poReflect?=?oMessage.GetReflection();vector<const?FieldDescriptor?*>?vecFD;poReflect->ListFields(oMessage,?&vecFD);for?(const?auto?&poFiled?:?vecFD)?{const?auto?&oFieldRule?=?poFiled->options().GetExtension(student::field_rule);if?(poFiled->cpp_type()?==?google::protobuf::FieldDescriptor::CPPTYPE_STRING?&&?!poFiled->is_repeated())?{//?類型是string并且選項非重復的才會校驗字段長度類型const?std::string?strValue?=?poReflect->GetString(oMessage,?poFiled);const?std::string?strName?=?poFiled->name();if?(oFieldRule.has_length_min())?{//?有才進行校驗,沒有則不進行校驗if?(minLengthCheck(strValue,?oFieldRule.length_min()))?{cout?<<?"the?length?of?"?<<?strName?<<?"?is?lower?than?"?<<?oFieldRule.length_min()<<endl;}?else?{cout?<<?"check?min?lenth?pass"<<endl;}}}}return?0; }int?main()?{Student?oStudent1;oStudent1.set_name("xiao");Student?oStudent2;oStudent2.set_name("xiaowei");allCheck(oStudent1);allCheck(oStudent2);return?0; }

    如上,如果需要校驗最大長度,必填,xss 校驗,只需要使用工廠模式,擴展代碼即可。

    新增一個字段或者變更某個字段的校驗規則,只需要修改 Proto,不需要修改代碼,從而防止因變更代碼導致錯誤。

    3.3 基于 PB 反射的前端頁面自動生成方案

    在我們常見的運營系統中,經常會涉及到各種各樣的表單頁面。在前后端交互方面,當需要增加字段或者變更字段的校驗規則時,需要面臨如下問題:

    • 前端:針對新字段編寫 html 代碼,同時需要修改前端頁面;

    • 后臺:針對每個字段做接收,并進行校驗。

    每增加或變更一個字段,我們都需要在前端和后臺進行修改,工作量大,同時頻繁變更容易導致錯誤。有什么方法可以解決這些問題嗎?答案是使用 PB 的反射能力。

    通過獲取 Message 中每個字段的描述然后返回給前端,前端根據字段描述來展示頁面,并且對字段進行校驗。同時通過這種方式,前后端可以共享一份表單校驗規則。

    在使用上述方案之后,當我們需要增加字段或者變更字段的校驗規則時,只需要在 Proto 中修改字段,大大節省了工作量,同時避免了因發布帶來的風險問題。

    3.4 通用存儲系統

    在運營系統中,前端輸入字段,傳入到后臺,后臺校驗字段之后,一般還需要把數據存儲到數據庫中。

    對于某些運營系統來說,其希望能夠快速接入一些數據,傳統開發常常會面臨如下問題:

    • 如何在不增加或變更表結構的基礎上,如何快速接入數據?

    • 如何零開發實現頻繁添加字段、新增渠道等需求?

    • 如何兼容不同業務、不同數據協議(比如 PB 中的不同 message)?

    答案是使用 PB 的反射,使得有結構的數據轉換為非結構的數據,然后存儲到非關系型數據庫(在微信支付側一般存入到 table kv)中。

    以 3.2 節中的 Proto 為例,舉例如下,學生類中定義了兩個字段,name 和 email 字段,原始信息為:

    Student?oStudent; oStudent.set_name("xiaowei"); oStudent.set_email("test@tencent.com");

    通過 PB 的反射,可以轉化為平鋪的結構:

    [{"id":"1","value":"xiaowei"},{"id":"2","value":"test@tencent.com"}]

    轉化為平鋪結構后,可以快速存入到數據庫中。如果現在學生信息里需要增加一個字段 address,則不需要修改表結構,從而完成存儲動作。利用 PB 反射,可以完成有結構數據和無結構數據之間的轉換,達到存儲和業務解耦的特性。

    四、總結

    本文首先給出了 PB 的反射函數,然后再結合自己平時負責的工作,給出了 PB 的進階使用。通過對 PB 的進階使用,可以大大提高開發和維護的效率,同時提升代碼的優雅度。有需要更進一步研究 PB 的,可以閱讀其源代碼,不得不說,通過閱讀優秀代碼能夠極大的促進編程能力。

    需要注意的是 PB 反射需要依賴大量計算資源,在密集使用 PB 的場景下,需要注意 CPU 的使用情況。

    加入我們

    微信支付境外支付團隊在不斷追求卓越的路上尋找同路人:

    https://careers.tencent.com/jobdesc.html?postId=1323514504423677952

    或點擊閱讀原文。

    11月26日19:30
    我們邀請到?TAPD團隊的明明?為大家分享
    團隊敏捷研發管理
    在TAPD甘特圖的應用與實踐

    超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生

    總結

    以上是生活随笔為你收集整理的巧用 Protobuf 反射来优化代码,拒做 PB Boy的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。