Utilice la reflexión de Protobuf para optimizar el código y rehúse ser PB Boy

Autor: iversonluo, ingeniero de desarrollo de aplicaciones de Tencent WXG

Algunos estudiantes de back-end se llaman a sí mismos SQL Boy, porque su principal actividad es agregar, eliminar, modificar y verificar la base de datos. ¿Los estudiantes que a menudo tratan con Proto también se llamarían PB Boy? Porque la mayor parte del trabajo es también CONFIGURAR y OBTENER Proto. Frente a una gran cantidad de código repetitivo y feo, ¿hay una solución mejor además de las macros? Este artículo combina la reflexión de PB para brindar algunas prácticas de optimización de código en mi trabajo de desarrollo de sistemas operativos.

1. Antecedentes

Protobuf (en lo sucesivo, PB) es un método de serialización de datos común, que a menudo se usa para transferir datos entre microservicios de back-end.

El trabajo principal actual del autor es el manejo de formularios, y los formularios generalmente involucran una gran cantidad de entrada de datos. La persona que llama al formulario generalmente formatea los datos como JSON y luego los envía a CGI, y CGI y los servicios de back-end y los servicios de back-end usarán PB para transferir datos antes .

Al escribir código, a menudo nos encontramos con un código PB desagradable, complejo y difícil de mantener :

  1. La verificación requerida del campo está codificada en el código: si necesita cambiar las reglas de verificación, debe modificar el código;

  2. Una verificación para cada campo, la complejidad ciclomática es alta: se realizan una variedad de verificaciones de reglas en cada campo del campo entrante, como la longitud, XSS, verificación regular, etc., una si se verifica el código, la complejidad ciclomática del código Muy alto

  3. Para obtener todos los campos no vacíos en PB para formar un mapa <cadena, cadena>, se requieren muchos juicios y códigos repetidos;

  4. Para transferir datos entre servicios en segundo plano, debido a que los módulos son desarrollados por diferentes personas, los nombres de los mismos campos son diferentes, para seleccionar una parte del contenido de un PB a otro PB, se requieren muchos códigos GET y SET.

¿Hay alguna forma de resolver los problemas anteriores?

La respuesta es usar la reflexión PB .

En segundo lugar, el uso de la reflexión PB

La definición general de reflexión es la siguiente: Un programa informático puede acceder, detectar y modificar su propio estado o comportamiento mientras se está ejecutando.

El diagrama de clases de protobuf es el siguiente:

En la figura anterior, podemos ver que la clase Message hereda de la clase MessageLite, y la clase Person personalizada de negocios hereda de la clase Message.

La clase Descriptor y la clase Reflection se agregan en Message, que es una dependencia débil.

Nombre de la clase Descripción de la clase
Descriptor Describa el mensaje, incluido el nombre del mensaje, la descripción de todos los campos, el contenido del archivo proto original, etc.
FieldDescriptor Describe un solo campo en Mensaje, incluidos nombres de campo, atributos de campo, campos de campo originales, etc.
Reflexión Proporciona la capacidad de leer y escribir dinámicamente un solo campo en el mensaje

Entonces, los pasos generales para usar la reflexión PB son los siguientes:

1. 通过Message获取单个字段的FieldDescriptor
2. 通过Message获取其Reflection
3. 通过Reflection来操作FieldDescriptor,从而动态获取或修改单个字段

Función para obtener descripción y reflexión:

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

Función para obtener FieldDescriptor:

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

Las tres categorías anteriores se presentan a continuación.

2.1 Introducción al descriptor de clase

La clase Descriptor describe principalmente el Mensaje, incluido el nombre del mensaje, la descripción de todos los campos, el contenido del archivo proto original, etc. Las funciones contenidas en esta clase se presentan a continuación.

La primera es la función para obtener su propia información:

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.

En la clase Descriptor, el FieldDescriptor se puede obtener mediante los siguientes métodos:

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获取

Donde FieldDescriptor* field(int index)y FieldDescriptor* FindFieldByNumber(int number)la función indexy el numbersignificado no son lo mismo, como sigue:

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

Entre los campos phone, indexes 5, pero numberes 2.

También hay una función que usamos a menudo en la depuración:

std::string Descriptor::DebugString(); // 将message转化成人可以识别出的string信息

2.2 Introducción a Class FieldDescriptor

La función de la clase FieldDescriptor es principalmente describir un solo campo en Message, incluidos los nombres de campo, los atributos de campo y los campos de campo originales.

Su función para obtener información sobre sí mismo:

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.

La cpp_type()función es obtener qué tipo de campo es. En PB, las categorías de tipos son las siguientes:

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
}

La clase FieldDescriptor también puede determinar si el campo es obligatorio, opcional o repetido:

bool is_required() const; // 判断字段是否是必填
bool is_optional() const; // 判断字段是否是选填
bool is_repeated() const; // 判断字段是否是重复值

En la clase FieldDescriptor, también puede obtener el indexo de un solo campo 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.

También hay una función que admite la extensión en la clase FieldDescriptor. La función es la siguiente:

// 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

La explicación específica sobre esta función se encuentra en el Capítulo 2.4.

2.3 Introducción a la reflexión

Esta clase proporciona la capacidad de leer y escribir dinámicamente un solo campo en el mensaje.

La función para leer un solo campo es la siguiente:

// 这里由于篇幅,省略了一部分代码,后面的代码部分也有省略,有需要的可以自行阅读源码。
int32 GetInt32(const Message & message, const FieldDescriptor * field) const

std::string GetString(const Message & message, const FieldDescriptor * field) const

const Message & GetMessage(const Message & message, const FieldDescriptor * field, MessageFactory * factory = nullptr) const // 读取单个message字段

La función para escribir un solo campo es la siguiente:

void SetInt32(Message * message, const FieldDescriptor * field, int32 value) const

void SetString(Message * message, const FieldDescriptor * field, std::string value) const

La función para obtener campos repetidos es la siguiente:

int32 GetRepeatedInt32(const Message & message, const FieldDescriptor * field, int index) const

std::string GetRepeatedString(const Message & message, const FieldDescriptor * field, int index) const

const Message & GetRepeatedMessage(const Message & message, const FieldDescriptor * field, int index) const

La función para escribir campos repetidos es la siguiente:

void SetRepeatedInt32(Message * message, const FieldDescriptor * field, int index, int32 value) const

void SetRepeatedString(Message * message, const FieldDescriptor * field, int index, std::string value) const

void SetRepeatedEnumValue(Message * message, const FieldDescriptor * field, int index, int value) const // Set an enum field's value with an integer rather than EnumValueDescriptor. more..

El nuevo diseño de campo repetido es el siguiente:

void AddInt32(Message * message, const FieldDescriptor * field, int32 value) const

void AddString(Message * message, const FieldDescriptor * field, std::string value) const

Además, hay una función más importante, que puede obtener descripciones de campo en lotes y colocarlas en el vector:

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

2.4 introducción de opciones

PB permite personalizar opciones y usar opciones en proto. Al definir el campo del mensaje, no solo puede definir el contenido del campo, sino también establecer las propiedades del campo, como las reglas de verificación, introducción, etc., combinado con la reflexión, puede realizar aplicaciones ricas y coloridas.

Aquí hay una introducción:

import "google/protobuf/descriptor.proto";

extend google.protobuf.FieldOptions {
  optional uint32 attr_id              = 50000; //字段id
  optional 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]"
  ];

El método de uso es el siguiente:

#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);

En tercer lugar, el uso avanzado de la reflexión PB

El capítulo 2 brinda detalles específicos sobre el uso y la reflexión sobre el PP. En este capítulo, el autor combina su código diario para ofrecer algunos escenarios de uso para la reflexión sobre el PP. Y tomando el desarrollo de un sistema de formularios como ejemplo, hable sobre el uso avanzado de la reflexión de PB en el desarrollo de un sistema de formularios.

3.1 Obtener todos los campos no vacíos en PB

En los negocios, a menudo es necesario obtener todos los campos no vacíos en un Mensaje para formar un mapa <cadena, cadena>, usando la reflexión PB para escribir lo siguiente:

#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

Con el código anterior, si necesita agregar campos en proto, ya no necesita modificar el código original.

3.2 Poner reglas de validación de campo en Proto

Una vez que el servicio en segundo plano recibe el campo desde la interfaz, verificará el campo, como verificación requerida, verificación de longitud, verificación regular, verificación xss, etc. Estas reglas a menudo están codificadas en el código. Pero con el aumento de campos de fondo, el código de la regla de verificación se volverá cada vez más y será cada vez más difícil de mantener. Si juntamos la definición de campo y las reglas y definiciones de verificación, ¿sería mejor mantenerlo?

El proto de muestra es el siguiente:

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];
}

Luego implementamos la verificación xss, verificación requerida, verificación de longitud, verificación de opciones y otros códigos.

El código de longitud mínima de verificación de muestra es el siguiente:

#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;
}

Como arriba, si necesita verificar la longitud máxima, es obligatorio. Para la verificación xss, solo necesita usar el modo de fábrica y extender el código.

Para agregar un nuevo campo o cambiar la regla de verificación de un campo, solo necesita modificar el Proto, sin modificar el código, para evitar errores causados ​​por el cambio de código.

3.3 Esquema de generación automática de la página de inicio basado en la reflexión de PB

En nuestro sistema operativo común, a menudo están involucradas varias páginas de formulario. En términos de interacción de front-end y back-end, cuando necesita agregar campos o cambiar las reglas de verificación de los campos, debe enfrentar los siguientes problemas:

  • Interfaz: escriba el código html para el nuevo campo y necesita modificar la página de inicio;

  • Antecedentes: Reciba y verifique cada campo.

Cada vez que se agrega o cambia un campo, debemos modificarlo en el front-end y el back-end. La carga de trabajo es grande y los cambios frecuentes pueden conducir fácilmente a errores. ¿Hay alguna forma de solucionar estos problemas? La respuesta es utilizar el poder reflectante de PB.

Al obtener la descripción de cada campo en el Mensaje y devolverlo al front-end, el front-end mostrará la página de acuerdo con la descripción del campo y verificará el campo. Al mismo tiempo, de esta manera, la parte delantera y la trasera pueden compartir una regla de verificación de formulario.

Después de usar la solución anterior, cuando necesitamos agregar campos o cambiar las reglas de verificación de los campos, solo necesitamos modificar los campos en Proto, lo que ahorra mucho trabajo y evita el riesgo de liberación.

3.4 Sistema de almacenamiento general

En el sistema operativo, los campos de entrada del front-end se transfieren al back-end, y después de que el back-end verifica los campos, los datos generalmente deben almacenarse en la base de datos.

Para algunos sistemas operativos, espera poder acceder rápidamente a algunos datos. El desarrollo tradicional a menudo se enfrenta a los siguientes problemas:

  • ¿Cómo acceder rápidamente a los datos sin agregar o cambiar la estructura de la tabla?

  • ¿Cómo satisfacer las necesidades de agregar campos con frecuencia y agregar canales sin desarrollo?

  • ¿Cómo ser compatible con diferentes servicios y diferentes protocolos de datos (como diferentes mensajes en PB)?

La respuesta es usar la reflexión de PB para convertir datos estructurados en datos no estructurados y luego almacenarlos en una base de datos no relacional (generalmente almacenada en la tabla kv en el lado de pago de WeChat).

Tome Proto en la Sección 3.2 como ejemplo. Por ejemplo, como sigue, se definen dos campos en la clase del estudiante, los campos de nombre y correo electrónico, y la información original es:

Student oStudent;
oStudent.set_name("xiaowei");
oStudent.set_email("[email protected]");

A través del reflejo de PB, se puede transformar en una estructura de mosaico:

[{"id":"1","value":"xiaowei"},{"id":"2","value":"[email protected]"}]

Después de transformarse en una estructura en mosaico, se puede almacenar rápidamente en la base de datos. Si ahora es necesario agregar una dirección de campo a la información del estudiante, no es necesario modificar la estructura de la tabla para completar la acción de almacenamiento. Con la reflexión de PB, la conversión entre datos estructurados y datos no estructurados se puede completar para lograr las características de almacenamiento y desacoplamiento comercial.

Cuatro, resumen

Este artículo primero da la función de reflexión de PB, y luego brinda el uso avanzado de PB en combinación con el trabajo del que normalmente soy responsable. Mediante el uso avanzado de PB, se puede mejorar enormemente la eficiencia del desarrollo y el mantenimiento, y se puede mejorar la elegancia del código. Si necesita estudiar más PB, puede leer su código fuente Tengo que decir que leer un código excelente puede promover enormemente la capacidad de programación.

Cabe señalar que la reflexión de PB requiere una gran cantidad de recursos informáticos. En escenarios donde PB se usa de manera intensiva, debe prestar atención al uso de la CPU.

Únete a nosotros

El equipo de pagos en el extranjero de WeChat Pay está buscando compañeros de viaje en el camino hacia la búsqueda continua de la excelencia:

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

O haga clic para leer el texto original.

A las 19:30 del 26 de noviembre,
invitamos a Mingming del equipo de TAPD a compartir contigo la aplicación y práctica de la
gestión ágil de I + D
en equipo en el diagrama de Gantt de TAPD.

Supongo que te gusta

Origin blog.csdn.net/Tencent_TEG/article/details/110211622
Recomendado
Clasificación