文章目录
前言
所谓反射机制,就是能够在运行时知道任意类的所有属性和方法,能够调用任意对象的任意方法和属性。这种动态获取的信息以及动态调用对象方向的功能称为反射机制。
如果用一句话来总结反射实现的关键,可概括为获取系统元信息。
元信息:即系统自描述信息,用于描述系统本身。举例来讲,即系统有哪些类?类中有哪些字段、哪些方法?字段属于什么类型、方法又有怎样的参数和返回值?…
不像Jave,python等语言,C++本身没有反射机制,但使用protobuf时,通过proto文件产生响应的message和service,可以提供反射机制,运行时可以通过proto获取任意message和任意service的属性和方法,并调用、修改和设置。
相关应用场景
Protobuf是一种常见的数据序列化方式,常常用于后台微服务之间传递数据。
在处理ProtoBuf Message数据时,经常需要根据一个输入的字符串,读取Message中对应属性的取值,并修改取值。;或者给定一个ProtoBuf对象,如何自动遍历该对象的所有字段,将其转换为json格式,或者在Bigtable中根据pb对象的字段自动写填写列名和对应的value。
在写代码时,经常会遇到一些丑陋的、圈复杂度较高、较难维护的关于 PB 的使用代码:
- 对字段的必填校验硬编码在代码中:如果需要变更校验规则,则需要修改代码
- 一个字段一个 if 校验,复杂度较高:对传进来的字段每个字段都进行多种规则校验,例如长度,XSS,正则校验等。
- 想要获取 PB 中所有的非空字段,形成一个 map<string,string>,需要大量的 if 判断和重复代码;
- 在后台服务间传递数据,由于模块由不同的人开发,导致相同字段的命名不一样,从一个 PB 中挑选一部分内容到另外一个 PB 中,需要大量的 GET 和 SET 代码。
以上都痛点可以通过反射机制解决。
一、ProtoBuf 反射原理概述
- 通过Message获取单个字段的FieldDescriptor
- 通过Message获取其Reflection
- 通过Reflection来操作FieldDescriptor,从而动态获取或修改单个字段
1、获取message和service的属性和方法
protobuf通过Descriptor获取任意message或service的属性和方法,Descriptor主要包括了一下几种类型:
描述符 | 方法 |
---|---|
FileDescriptor | 获取Proto文件中的Descriptor和ServiceDescriptor |
Descriptor | 获取类message属性和方法,包括FieldDescriptor和EnumDescriptor |
FieldDescriptor | 获取message中各个字段的类型、标签、名称等 |
EnumDescriptor | 获取Enum中的各个字段名称、值等 |
ServiceDescriptor | 获取service中的MethodDescriptor |
MethodDescriptor | 获取各个RPC中的request、response、名称等 |
在类 Descriptor 中 可以获取自身信息的函数
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获取
也就是说,如果能够获取到proto文件的FileDescriptor,就能获取proto文件中的所有的内容。那么如何获取proto文件的FileDescriptor呢?protobuf提供多种方法。
1.1 使用protoc将proto文件生成.h和.cc文件
这种方法直接根据生成的类来获取响应的FileDescriptor,比如现在有test.proto文件,那么可以通过DescriptorPool::generated_pool()获取到其FileDescriptor
const FileDescriptor* fileDescriptor = DescriptorPool::generated_pool()->FindFileByName(file);
并且对于任意的message和service都可以根据其名称,通过DescriptorPool对应的Descriptor和ServiceDescriptor
1.2 只使用proto文件,不使用protoc进行编译
这种情况需要手动解析proto文件,再获取FileDescriptor。protobuf提供了响应的解析器compiler,通过compoiler可以方便的获取proto文件的FileDescriptor
const FileDescriptor* GetFileDescriptorFromProtoFile(const std::string &protoRoot, const std::string &protoFile){
compiler::DeskSourceTree sourceTree;
sourceTree.MapPath("", protoRoot);
FileErrorCollector errorCollector;
compiler::Importer importer(&sourceTree, &errorCollector);
return importer.Import(protoFile);
}
1.3 非 .proto 文件 ,转换成.proto
- 可以从远程读取,如将数据与数据元信息一同进行 protobuf 编码并传输:
message Req {
optional string proto_file = 1;
optional string data = 2;
}
- 从 Json 或其它格式数据中转换而来
无论 .proto 文件来源于何处,我们都需要对其做进一步的处理和注册,将其解析成内存对象,并构建其与实例的映射,同时也要计算每个字段的内存偏移。可总结出如下步骤:
- 提供 .proto (范指 ProtoBuf Message 语法描述的元信息)
- 解析 .proto 构建 FileDescriptor、FieldDescriptor 等,即 .proto 对应的内存模型(对象)
- 之后每创建一个实例,就将其存到实例工厂相应的实例池中
- 将 Descriptor 和 instance 的映射维护到表中备查
- 通过 Descriptor 可查到相应的 instance,又由于了解 instance 中字段类型(FieldDescriptor),所以知道字段的内存偏移,那么就可以访问或修改字段的值
2、调用message的属性和方法
如下是通过字符串type_name调用message的属性和方法的流程图;
Person是自定义的pb类型,继承自Message. MessageLite作为Message基类,更加轻量级一些。
通过Descriptor能获得所需消息的Message* 指针。Message class 定义了 New() 虚函数,用来返回本对象的一份新实体,具体流程如下:
- 通过 DescriptorPool 的 FindMessageTypeByName 获得了元信息 Descriptor。
- 根据 MessageFactory 实例工厂和Descriptor获得了Message的默认实例Message*指针 default instance
- 通过new()构造一个可用的消息对象
2.1根据type name反射自动创建实例
通过字符串"person" 创建新的 person message对象。线程安全
// 先获得类型的Descriptor .
auto descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("Person");
//利用Descriptor拿到类型注册的instance. 这个是不可修改的.
auto prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
// 构造一个可用的消息.
auto instance = prototype->New(); //创建新的 person message对象。
第一步我们通过 DescriptorPool 的 FindMessageTypeByName 获得了元信息 Descriptor。
DescriptorPool 为元信息池,对外提供了诸如 FindServiceByName、 FindMessageTypeByName 等各类接口以便外部查询所需的Service或者Message元信息。当 DescriptorPool 不存在时需要查询的元信息时,将进一步到 DescriptorDatabase 中去查找。 DescriptorDatabase 可从 硬编码或磁盘中查询对应名称的 .proto 文件内容,解析后返回查询需要的元信息。
不难看出,DescriptorPool 和 DescriptorDatabase 通过缓存机制提高了反射运行效率。DescriptorDatabase 从磁盘中读取 .proto 内容并解析成 Descriptor 并不常用,实际上我们在使用 protoc 生成 xxx.pb.cc 和 xxx.pb.h 文件时,其中不仅仅包含了读写数据的接口,还包含了 .proto 文件内容。阅读任意一个 xxx.pb.cc 的内容,你可以看到如下类似代码
static void AddDescriptorsImpl() {
InitDefaults();
// .proto 内容
static const char descriptor[] GOOGLE_PROTOBUF_ATTRIBUTE_SECTION_VARIABLE(protodesc_cold) = {
"\n\022single_int32.proto\"\035\n\010Example1\022\021\n\010int3"
"2Val\030\232\005 \001(\005\" \n\010Example2\022\024\n\010int32Val\030\377\377\377\377"
"\001 \003(\005b\006proto3"
};
// 注册 descriptor
::google::protobuf::DescriptorPool::InternalAddGeneratedFile(
descriptor, 93);
// 注册 instance
::google::protobuf::MessageFactory::InternalRegisterGeneratedFile(
"single_int32.proto", &protobuf_RegisterTypes);
}
其中 descriptor 数组存储的便是 .proto 内容。这里当然不是简单的存储原始文本字符串,而是经过了 SerializeToString 序列化处理,而后将结果以 硬编码的形式保存在 xxx.pb.cc 中,真是充分利用了自己的高效编码能力。
硬编码的 .proto 元信息内容将以