巧用 Protobuf 反射来优化代码,拒做 PB Boy
作者:iversonluo,騰訊 WXG 應(yīng)用開發(fā)工程師
有些后臺(tái)同學(xué)將自己稱為 SQL Boy,因?yàn)樨?fù)責(zé)的業(yè)務(wù)主要是對(duì)數(shù)據(jù)庫(kù)進(jìn)行增刪改查。經(jīng)常和 Proto 打交道的同學(xué),是不是也會(huì)叫自己 PB Boy?因?yàn)榇蟛糠止ぷ饕彩菍?duì) Proto 進(jìn)行 SET 和 GET。面對(duì)大量重復(fù)且丑陋的代碼,除了宏是否有更好的解決方法?本文結(jié)合 PB 反射給出了我在運(yùn)營(yíng)系統(tǒng)開發(fā)工作中的一些代碼優(yōu)化實(shí)踐。
一、背景
Protobuf(下文稱為 PB)是一種常見的數(shù)據(jù)序列化方式,常常用于后臺(tái)微服務(wù)之間傳遞數(shù)據(jù)。
筆者目前主要的工作都是和表單打交道,而表單一般涉及到大量的數(shù)據(jù)輸入,表單調(diào)用方一般將數(shù)據(jù)格式化為 JSON 后傳給 CGI,而 CGI 和后臺(tái)服務(wù)、后臺(tái)服務(wù)之前會(huì)用 PB 傳遞數(shù)據(jù)。
在寫代碼時(shí),經(jīng)常會(huì)遇到一些丑陋的、圈復(fù)雜度較高、較難維護(hù)的關(guān)于 PB 的使用代碼:
對(duì)字段的必填校驗(yàn)硬編碼在代碼中:如果需要變更校驗(yàn)規(guī)則,則需要修改代碼;
一個(gè)字段一個(gè) if 校驗(yàn),圈復(fù)雜度較高:對(duì)傳進(jìn)來(lái)的字段每個(gè)字段都進(jìn)行多種規(guī)則校驗(yàn),例如長(zhǎng)度,XSS,正則校驗(yàn)等,一個(gè)校驗(yàn)一個(gè) if 代碼,代碼圈復(fù)雜度很高;
想要獲取 PB 中所有的非空字段,形成一個(gè) map<string,string>,需要大量的 if 判斷和重復(fù)代碼;
在后臺(tái)服務(wù)間傳遞數(shù)據(jù),由于模塊由不同的人開發(fā),導(dǎo)致相同字段的命名不一樣,從一個(gè) PB 中挑選一部分內(nèi)容到另外一個(gè) PB 中,需要大量的 GET 和 SET 代碼。
是否可以有方法解決上面的幾個(gè)問(wèn)題呢?
答案是使用PB 反射。
二、PB 反射的使用
反射的一般定義如下:計(jì)算機(jī)程序在運(yùn)行時(shí)可以訪問(wèn)、檢測(cè)和修改它本身狀態(tài)或行為。
protobuf 的類圖如下:
從上圖我們可以看出,Message 類繼承于 MessageLite 類,業(yè)務(wù)一般自定義的 Person 類繼承于 Message 類。
Descriptor 類和 Reflection 類都聚合于 Message,是弱依賴的關(guān)系。
| Descriptor | 對(duì) Message 進(jìn)行描述,包括 message 的名字、所有字段的描述、原始 proto 文件內(nèi)容等 |
| FieldDescriptor | 對(duì) Message 中單個(gè)字段進(jìn)行描述,包括字段名、字段屬性、原始的 field 字段等 |
| Reflection | 提供了動(dòng)態(tài)讀和寫 message 中單個(gè)字段能力 |
所以一般使用 PB 反射的步驟如下:
1.?通過(guò)Message獲取單個(gè)字段的FieldDescriptor 2.?通過(guò)Message獲取其Reflection 3.?通過(guò)Reflection來(lái)操作FieldDescriptor,從而動(dòng)態(tài)獲取或修改單個(gè)字段獲取 Descript、Reflection 的函數(shù):
const?google::protobuf::Reflection*?pReflection?=?pMessage->GetReflection(); const?google::protobuf::Descriptor*?pDescriptor?=?pMessage->GetDescriptor();獲取 FieldDescriptor 的函數(shù):
const?google::protobuf::FieldDescriptor?*?pFieldDesc?=?pDescriptor->FindFieldByName(id);下面分別介紹上面的三個(gè)類。
2.1 類 Descriptor 介紹
類 Descriptor 主要是對(duì) Message 進(jìn)行描述,包括 message 的名字、所有字段的描述、原始 proto 文件內(nèi)容等,下面介紹該類中包含的函數(shù)。
首先是獲取自身信息的函數(shù):
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 中,可以通過(guò)如下方法獲取類 FieldDescriptor:
const?FieldDescriptor*?field(int?index)?const;?//?根據(jù)定義順序索引獲取,即從0開始到最大定義的條目 const?FieldDescriptor*?FindFieldByNumber(int?number)?const;?//?根據(jù)定義的message里面的順序值獲取(option?string?name=3,3即為number) const?FieldDescriptor*?FindFieldByName(const?string&?name)?const;?//?根據(jù)field?name獲取 const?FieldDescriptor*?Descriptor::FindFieldByLowercaseName(const?std::string?&?lowercase_name)const;?//?根據(jù)小寫的field?name獲取 const?FieldDescriptor*?Descriptor::FindFieldByCamelcaseName(const?std::string?&?camelcase_name)?const;?//?根據(jù)駝峰的field?name獲取其中FieldDescriptor* field(int index)和FieldDescriptor* FindFieldByNumber(int number)這個(gè)函數(shù)中index和number的含義是不一樣的,如下所示:
message?Student{optional?string?name?=?1;optional?string?gender?=?2;optional?string?phone?=?5; }其中字段phone,其index為 5,但是其number為 2。
同時(shí)還有一個(gè)我們?cè)谡{(diào)試中經(jīng)常使用的函數(shù):
std::string?Descriptor::DebugString();?//?將message轉(zhuǎn)化成人可以識(shí)別出的string信息2.2 類 FieldDescriptor 介紹
類 FieldDescriptor 的作用主要是對(duì) Message 中單個(gè)字段進(jìn)行描述,包括字段名、字段屬性、原始的 field 字段等。
其獲取獲取自身信息的函數(shù):
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()函數(shù)是來(lái)獲取該字段是什么類型的,在 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 中還可以判斷字段是否是必填,還是選填或者重復(fù):
bool?is_required()?const;?//?判斷字段是否是必填 bool?is_optional()?const;?//?判斷字段是否是選填 bool?is_repeated()?const;?//?判斷字段是否是重復(fù)值類 FieldDescriptor 中還可以獲取單個(gè)字段的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 中還有一個(gè)支持?jǐn)U展的函數(shù),函數(shù)如下:
//?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具體關(guān)于該函數(shù)的講解在 2.4 章。
2.3 類 Reflection 介紹
該類提供了動(dòng)態(tài)讀、寫 message 中單個(gè)字段能力。
讀單個(gè)字段的函數(shù)如下:
//?這里由于篇幅,省略了一部分代碼,后面的代碼部分也有省略,有需要的可以自行閱讀源碼。 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?//?讀取單個(gè)message字段寫單個(gè)字段的函數(shù)如下:
void?SetInt32(Message?*?message,?const?FieldDescriptor?*?field,?int32?value)?constvoid?SetString(Message?*?message,?const?FieldDescriptor?*?field,?std::string?value)?const獲取重復(fù)字段的函數(shù)如下:
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寫重復(fù)字段的函數(shù)如下:
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..新增重復(fù)字段設(shè)計(jì)如下:
void?AddInt32(Message?*?message,?const?FieldDescriptor?*?field,?int32?value)?constvoid?AddString(Message?*?message,?const?FieldDescriptor?*?field,?std::string?value)?const另外有一個(gè)較為重要的函數(shù),其可以批量獲取字段描述并將其放置到 vector 中:
void?Reflection::ListFields(const?Message?&?message,?std::vector<?const?FieldDescriptor?*?>?*?output)?const2.4 options 介紹
PB 允許在 proto 中自定義選項(xiàng)并使用選項(xiàng)。在定義 message 的字段時(shí),不僅可以定義字段內(nèi)容,還可以設(shè)置字段的屬性,比如校驗(yàn)規(guī)則,簡(jiǎn)介等,結(jié)合反射,可以實(shí)現(xiàn)豐富豐富多彩的應(yīng)用。
下面來(lái)介紹下:
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;?//?商戶組命名規(guī)范optional?uint32?length_min???????????=?50003??[default?=?0];?//?字段最小長(zhǎng)度optional?uint32?length_max???????????=?50004??[default?=?1024];?//?字段最大長(zhǎng)度optional?string?regex????????????????=?50005;?//?該字段的正則表達(dá)式 }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 反射的進(jìn)階使用
第二章給出了 PB 反射,以及具體的使用細(xì)節(jié),在本章中,作者結(jié)合自己日常的代碼,給出 PB 反射一些使用場(chǎng)景。并且以開發(fā)一個(gè)表單系統(tǒng)為例,講一下 PB 反射在開發(fā)表單系統(tǒng)中的進(jìn)階使用。
3.1 獲取 PB 中所有非空字段
在業(yè)務(wù)中,經(jīng)常會(huì)需要獲取某個(gè) Message 中所有非空字段,形成一個(gè) 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;?//?不支持轉(zhuǎn)換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通過(guò)上面的代碼,如果需要在 proto 中增加字段,不再需要修改原來(lái)的代碼。
3.2 將字段校驗(yàn)規(guī)則放置在 Proto 中
后臺(tái)服務(wù)接收到前端傳來(lái)的字段后,會(huì)對(duì)字段進(jìn)行校驗(yàn),比如必填校驗(yàn),長(zhǎng)度校驗(yàn),正則校驗(yàn),xss 校驗(yàn)等,這些規(guī)則我們常常會(huì)硬編碼在代碼中。但是隨著后臺(tái)字段的增加,校驗(yàn)規(guī)則代碼會(huì)變得越來(lái)越多,越來(lái)越難維護(hù)。如果我們把字段的定義和校驗(yàn)規(guī)則和定義放在一起,這樣是不是更好的維護(hù)?
示例 proto 如下:
syntax?=?"proto2";package?student;import?"google/protobuf/descriptor.proto";message?FieldRule{optional?uint32?length_min?=?1;?//?字段最小長(zhǎng)度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]; }然后我們自己實(shí)現(xiàn) xss 校驗(yàn),必填校驗(yàn),長(zhǎng)度校驗(yàn),選項(xiàng)校驗(yàn)等代碼。
示例校驗(yàn)最小長(zhǎng)度代碼如下:
#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并且選項(xiàng)非重復(fù)的才會(huì)校驗(yàn)字段長(zhǎng)度類型const?std::string?strValue?=?poReflect->GetString(oMessage,?poFiled);const?std::string?strName?=?poFiled->name();if?(oFieldRule.has_length_min())?{//?有才進(jìn)行校驗(yàn),沒(méi)有則不進(jìn)行校驗(yàn)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; }如上,如果需要校驗(yàn)最大長(zhǎng)度,必填,xss 校驗(yàn),只需要使用工廠模式,擴(kuò)展代碼即可。
新增一個(gè)字段或者變更某個(gè)字段的校驗(yàn)規(guī)則,只需要修改 Proto,不需要修改代碼,從而防止因變更代碼導(dǎo)致錯(cuò)誤。
3.3 基于 PB 反射的前端頁(yè)面自動(dòng)生成方案
在我們常見的運(yùn)營(yíng)系統(tǒng)中,經(jīng)常會(huì)涉及到各種各樣的表單頁(yè)面。在前后端交互方面,當(dāng)需要增加字段或者變更字段的校驗(yàn)規(guī)則時(shí),需要面臨如下問(wèn)題:
前端:針對(duì)新字段編寫 html 代碼,同時(shí)需要修改前端頁(yè)面;
后臺(tái):針對(duì)每個(gè)字段做接收,并進(jìn)行校驗(yàn)。
每增加或變更一個(gè)字段,我們都需要在前端和后臺(tái)進(jìn)行修改,工作量大,同時(shí)頻繁變更容易導(dǎo)致錯(cuò)誤。有什么方法可以解決這些問(wèn)題嗎?答案是使用 PB 的反射能力。
通過(guò)獲取 Message 中每個(gè)字段的描述然后返回給前端,前端根據(jù)字段描述來(lái)展示頁(yè)面,并且對(duì)字段進(jìn)行校驗(yàn)。同時(shí)通過(guò)這種方式,前后端可以共享一份表單校驗(yàn)規(guī)則。
在使用上述方案之后,當(dāng)我們需要增加字段或者變更字段的校驗(yàn)規(guī)則時(shí),只需要在 Proto 中修改字段,大大節(jié)省了工作量,同時(shí)避免了因發(fā)布帶來(lái)的風(fēng)險(xiǎn)問(wèn)題。
3.4 通用存儲(chǔ)系統(tǒng)
在運(yùn)營(yíng)系統(tǒng)中,前端輸入字段,傳入到后臺(tái),后臺(tái)校驗(yàn)字段之后,一般還需要把數(shù)據(jù)存儲(chǔ)到數(shù)據(jù)庫(kù)中。
對(duì)于某些運(yùn)營(yíng)系統(tǒng)來(lái)說(shuō),其希望能夠快速接入一些數(shù)據(jù),傳統(tǒng)開發(fā)常常會(huì)面臨如下問(wèn)題:
如何在不增加或變更表結(jié)構(gòu)的基礎(chǔ)上,如何快速接入數(shù)據(jù)?
如何零開發(fā)實(shí)現(xiàn)頻繁添加字段、新增渠道等需求?
如何兼容不同業(yè)務(wù)、不同數(shù)據(jù)協(xié)議(比如 PB 中的不同 message)?
答案是使用 PB 的反射,使得有結(jié)構(gòu)的數(shù)據(jù)轉(zhuǎn)換為非結(jié)構(gòu)的數(shù)據(jù),然后存儲(chǔ)到非關(guān)系型數(shù)據(jù)庫(kù)(在微信支付側(cè)一般存入到 table kv)中。
以 3.2 節(jié)中的 Proto 為例,舉例如下,學(xué)生類中定義了兩個(gè)字段,name 和 email 字段,原始信息為:
Student?oStudent; oStudent.set_name("xiaowei"); oStudent.set_email("test@tencent.com");通過(guò) PB 的反射,可以轉(zhuǎn)化為平鋪的結(jié)構(gòu):
[{"id":"1","value":"xiaowei"},{"id":"2","value":"test@tencent.com"}]轉(zhuǎn)化為平鋪結(jié)構(gòu)后,可以快速存入到數(shù)據(jù)庫(kù)中。如果現(xiàn)在學(xué)生信息里需要增加一個(gè)字段 address,則不需要修改表結(jié)構(gòu),從而完成存儲(chǔ)動(dòng)作。利用 PB 反射,可以完成有結(jié)構(gòu)數(shù)據(jù)和無(wú)結(jié)構(gòu)數(shù)據(jù)之間的轉(zhuǎn)換,達(dá)到存儲(chǔ)和業(yè)務(wù)解耦的特性。
四、總結(jié)
本文首先給出了 PB 的反射函數(shù),然后再結(jié)合自己平時(shí)負(fù)責(zé)的工作,給出了 PB 的進(jìn)階使用。通過(guò)對(duì) PB 的進(jìn)階使用,可以大大提高開發(fā)和維護(hù)的效率,同時(shí)提升代碼的優(yōu)雅度。有需要更進(jìn)一步研究 PB 的,可以閱讀其源代碼,不得不說(shuō),通過(guò)閱讀優(yōu)秀代碼能夠極大的促進(jìn)編程能力。
需要注意的是 PB 反射需要依賴大量計(jì)算資源,在密集使用 PB 的場(chǎng)景下,需要注意 CPU 的使用情況。
加入我們
微信支付境外支付團(tuán)隊(duì)在不斷追求卓越的路上尋找同路人:
https://careers.tencent.com/jobdesc.html?postId=1323514504423677952
或點(diǎn)擊閱讀原文。
11月26日19:30
我們邀請(qǐng)到?TAPD團(tuán)隊(duì)的明明?為大家分享
團(tuán)隊(duì)敏捷研發(fā)管理
在TAPD甘特圖的應(yīng)用與實(shí)踐
總結(jié)
以上是生活随笔為你收集整理的巧用 Protobuf 反射来优化代码,拒做 PB Boy的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 简单理解 Kafka 的消息可靠性策略
- 下一篇: 大牛书单 | 搜索大牛都读什么书?