-
Notifications
You must be signed in to change notification settings - Fork 249
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5641fba
commit f9b36e0
Showing
1 changed file
with
296 additions
and
0 deletions.
There are no files selected for viewing
296 changes: 296 additions & 0 deletions
296
content/zh/docs/kitex/Tutorials/code-gen/thrift-reflection.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,296 @@ | ||
--- | ||
title: "Thriftgo 反射使用文档" | ||
date: 2024-09-20 | ||
weight: 1 | ||
keywords: ["Thriftgo 反射使用文档"] | ||
description: "" | ||
--- | ||
|
||
## 简介 | ||
|
||
Thriftgo 在 v0.3.0 版本提供了 Thrift IDL 反射功能,可以在运行时获取 Thrift IDL 的相关信息与描述符,并支持通过描述符来找到对应的 Go Type 并进行反射操作。我们可以在运行时使用这些反射查询接口,获取到 IDL 信息,以便进行各种查询和操作,以及更轻松的使用反射。 | ||
|
||
## 快速开始 | ||
|
||
例如有这样一个 IDL | ||
|
||
```go | ||
// sample.thrift | ||
namespace go demo | ||
|
||
struct A{ | ||
// hello | ||
1:required string hello | ||
} | ||
|
||
struct B{ | ||
// hello | ||
1:required string hello | ||
} | ||
|
||
enum Gender{ | ||
MALE | ||
FEMALE | ||
} | ||
|
||
service MyService{ | ||
B MyMethod(1:required A req) | ||
} | ||
``` | ||
|
||
以 Kitex Tool 为例,在代码生成时,额外添加 -thrift with_reflection 参数,执行如下命令 | ||
|
||
```go | ||
kitex -module xx -thrift with_reflection demo.thrift | ||
``` | ||
|
||
生成代码的目录如下: | ||
|
||
```go | ||
kitex_gen/ | ||
└── demo | ||
├── demo-reflection.go | ||
├── demo.go | ||
├── k-consts.go | ||
├── k-demo.go | ||
└── myservice | ||
├── client.go | ||
├── invoker.go | ||
├── myservice.go | ||
└── server.go | ||
|
||
``` | ||
|
||
相比原有场景,会多出 demo-reflection.go 文件。这个文件包含了 demo.thrift 的 IDL 信息记录以及反射相关接口。 | ||
|
||
```go | ||
// demo-reflection.go | ||
// IDL Name: demo | ||
// IDL Path: demo.thrift | ||
|
||
// 记录 Descriptor 与 Go Type 的对应关系 | ||
var file_demo_thrift_go_types = []interface{}{ | ||
(*A)(nil), // Struct 0: demo.A | ||
(*B)(nil), // Struct 1: demo.B | ||
(*Gender)(nil), // Enum 0: demo.Gender | ||
} | ||
var file_demo_thrift *thrift_reflection.FileDescriptor | ||
var file_idl_demo_rawDesc = []byte{0x1f, 0x8b, 0x8, 0x0, 0x0, ......} | ||
} | ||
|
||
// 初始化反射信息,并全局注册这个 IDL 的元信息 | ||
func init() { | ||
if file_demo_thrift != nil { | ||
return | ||
} | ||
type x struct{} | ||
builder := &thrift_reflection.FileDescriptorBuilder{ | ||
Bytes: file_idl_demo_rawDesc, | ||
GoTypes: file_demo_thrift_go_types, | ||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), | ||
} | ||
file_demo_thrift = thrift_reflection.BuildFileDescriptor(builder) | ||
} | ||
|
||
// 生成该 IDL 对应的 File Descriptor 的获取方法 | ||
func GetFileDescriptorForDemo() *thrift_reflection.FileDescriptor { | ||
return file_demo_thrift | ||
} | ||
// 为结构体生成 Descriptor 的获取方法 | ||
func (p *A) GetDescriptor() *thrift_reflection.StructDescriptor { | ||
return file_demo_thrift.GetStructDescriptor("A") | ||
} | ||
|
||
//..... | ||
``` | ||
|
||
当在 Golang 代码里引入这个包的时候,该 IDL 的元信息就会被初始化并加载记录,在后续的代码中就可以开始使用 Thrift 反射的 API 了。 | ||
|
||
## 获取 Descriptor 与 IDL 元信息 | ||
|
||
我们可以通过 Descriptor 来获取 IDL 里对应的信息。例如通过 StructDescriptor,来获取 IDL 在结构体定义时的信息,以及拿到字段的注释和注解等等: | ||
|
||
```go | ||
func main() { | ||
a := demo.NewA() | ||
// get struct descriptor from golang struct generated by thriftgo | ||
desc := a.GetDescriptor() | ||
fmt.Println(desc.Name) | ||
fmt.Println(desc.Annotations) | ||
|
||
// get field descriptor from struct descriptor | ||
helloDesc := desc.GetFieldByName("hello") | ||
fmt.Println(helloDesc.GetID()) | ||
fmt.Println(helloDesc.GetRequiredness()) | ||
|
||
// get type descriptor from field descriptor | ||
typeDesc := helloDesc.GetType() | ||
fmt.Println(typeDesc.GetName()) | ||
fmt.Println(typeDesc.IsStruct()) | ||
|
||
// get annotation and check annotation | ||
annotations := desc.getAnnotations() | ||
test.Assert(annotations["key"]=="value1") | ||
} | ||
``` | ||
|
||
具体来说,Descriptor 的种类以及他们之间的关系如下: | ||
|
||
这些 Descriptor 也是用 Thrift IDL 定义的:[https://github.com/cloudwego/thriftgo/blob/main/descriptor.thrift](https://github.com/cloudwego/thriftgo/blob/main/descriptor.thrift) | ||
|
||
除了在代码生成里提供 Golang 结构体找到 Descriptor 的方法以外,thrift_reflection 包内也提供了按照 Golang 类型或者名称查找 Descriptor 的方式: | ||
|
||
```go | ||
func main() { | ||
a := demo.NewA() | ||
desc := a.GetDescriptor() | ||
|
||
// 通过传入一个 golang 对象来找到其类型对应的 Descriptor | ||
desc = thrift_reflection.GetStructDescriptorByGoType((*demo.A)(nil)) | ||
desc = thrift_reflection.GetStructDescriptorByGoType(a) | ||
|
||
// 通过 struct name + filepath 来搜索结构体,如果 filepath 为空,则为模糊匹配第一个 struct name 相同的对象 | ||
desc = thrift_reflection.LookupStruct("A", "demo.thrift") | ||
|
||
// 通过 type descriptor 来找到对应的 struct descriptor | ||
typeDesc := desc.GetFieldByName("xxx").GetType() | ||
if typeDesc.IsStruct(){ | ||
typeDesc.GetStructDescriptor() | ||
} | ||
} | ||
``` | ||
|
||
同时也附带了一些常用的工具函数来进行 Descriptor 的查找: | ||
|
||
```go | ||
// 查询某结构体中直接和间接依赖的所有 Struct Descriptor | ||
allStructDescs, err := LookupIncludedStructsFromStruct(structDesc) | ||
// 查询某方法中直接和间接依赖的所有 Struct Descriptor | ||
allStructDescs, err := LookupIncludedStructsFromMethod(methodDesc) | ||
// 查询某 Type Descriptor 中直接和间接依赖的所有 Struct Descriptor | ||
allStructDescs, err := LookupIncludedStructsFromType(typeDesc) | ||
``` | ||
|
||
## 获取 GoTypes 与反射操作 | ||
|
||
Descriptor 也能和 Golang 反射类型配合使用,Descriptor 提供了 GetGoType 方法,用来获取对应的 Golang 类型: | ||
|
||
```go | ||
func main() { | ||
person := &simple.Person{ | ||
Name: "CloudWeGo Guy", | ||
Id: &simple.IDCard{ | ||
Number: "123", | ||
Age: 23, | ||
}, | ||
} | ||
// get struct descriptor | ||
pd := person.Descriptor() | ||
// get go type from struct descriptor | ||
fmt.Println(pd.GetGoType()) | ||
// get field descriptor | ||
fd := pd.GetFieldByName("id") | ||
// reflection | ||
idCardGoType, err := fd.GetGoType() | ||
if err != nil { | ||
idCardInstance := reflect.New(idCardGoType).Elem() | ||
fmt.Println(idCardInstance) | ||
} | ||
} | ||
``` | ||
|
||
类似的,除了 StructDescriptor,其他 Descriptor 也都有对应的 GoType 获取方法 | ||
|
||
要注意的是,Descriptor 查询到的信息是来自 IDL 的,名称和 Go 代码的结构体不一定完全一致,因为下划线转驼峰,或者重名避免策略,实际的 IDL 命名和 Golang 代码名称会有出入。例如某结构体的 Field 名叫 my_name,通过 Descriptor 拿到的 Name 为 my_name,但实际上 Golang 结构体里,这个 Field 是 MyName (下划线转驼峰命名) | ||
|
||
Descriptor 也可以配合 Golang 反射 API 使用。虽然无法通过 GetFieldByName 来直接查找,但 Descriptor 里的 Field 的顺序是和 Golang 反射里的一致的,可以像下面这段代码一样通过 Descriptor 查找反射字段 | ||
|
||
```go | ||
goType := structDescriptor.GetGoType() | ||
structEntity := reflect.New(goType).Elem() | ||
for idx, fd := range des.GetFields() { | ||
reflectField := structEntity.Field(idx) | ||
// xxxx | ||
} | ||
``` | ||
|
||
另外 Thriftgo 也提供了一些简单的关于 Descriptor 与 Golang 反射结合使用的 API,例如下面这段代码,展示了通过 fieldDescriptor 来反射设置某个结构体的字段的值: | ||
|
||
```go | ||
func TestReflection(t *testing.T) { | ||
|
||
p := thrift_reflection_test.NewPerson() | ||
p.Name = "CloudWeGo" | ||
|
||
nameFieldDesc := p.GetDescriptor().GetFieldByName("name") | ||
assert(t, nameDesc != nil) | ||
|
||
// test get instance value by field descriptor reflection api | ||
val, err := nameFieldDesc.GetInstanceValue(p) | ||
assert(t, err == nil) | ||
stringVal, ok := val.(string) | ||
assert(t, ok && stringVal == "CloudWeGo") | ||
|
||
// test set instance value by field descriptor reflection api | ||
err = nameFieldDesc.SetInstanceValue(p, "KiteX") | ||
assert(t, err == nil) | ||
assert(t, p.Name == "KiteX") | ||
|
||
} | ||
``` | ||
|
||
在 Thriftgo v0.3.0 版本中,由于 Thrift 反射功能刚刚发布,所以配合 Golang 反射使用的 API 封装较少,后续会根据用户建议与常用场景,完善这方面的 API。 | ||
|
||
## FAQ | ||
|
||
### Q:为什么初始化阶段出现错误会直接采用 panic 来停止整个程序 | ||
|
||
因为若初始化与注册 IDL 时出现错误,导致某个 IDL 无法被注册上,那么在后续的使用里,会影响到 IDL 信息之间的查询结果(例如找不到某个本应存在的 Descriptor),可能会因此导致用户的业务逻辑出错。所以 IDL 注册出错时直接 panic,以明显的方式提示用户。 | ||
|
||
### Q:初始化时报错 "thrift reflection: file 'xxx' is already registered" | ||
|
||
因为 Thrift IDL 元信息在注册时,是以 IDL 文件的 filepath 为唯一的 Key 的,所以当遇到有重复名称的 IDL 注册时,会报这个提示。这个设计与 Google Protobuf 反射的注册重复报错是类似的。解决方式是根据报错提示的路径找到对应的文件,重新用不同的 IDL 名称再生成代码即可。 | ||
|
||
### Q:考虑把反射信息的生成设置为默认行为吗 | ||
|
||
目前要生成反射信息,需要 thriftgo 额外开启参数。由于反射功能刚刚推出,可能在后续投入实际使用后可能会有修改,所以打算等该功能较为成熟后,再作为默认行为开启,从而尽可能在现阶段减少对不使用该功能的用户的打扰。 | ||
|
||
### Q:这个功能可以和公共结构体一起使用吗 | ||
|
||
[生成引用结构体](https://www.cloudwego.io/zh/docs/kitex/tutorials/code-gen/struct_reference_generator/) 公共结构体功能是将本地某个 IDL 对应的 Golang 代码全部指向远端公共仓库的 Golang 代码。Thriftgo 反射功能可以和公共结构体功能一起使用。但有一些需要注意的说明。 | ||
|
||
首先,公共结构体的远端仓库在生成代码时也需要生成 Thriftgo 反射内容。 | ||
|
||
理论上,公共结构体场景下,本地的这份 IDL 是要和远端的 IDL 完全一致的。但不排除存在微弱的不一致情况,例如远端 IDL 多了一些结构体,或者某些结构体下增加了新的字段。例如下面的案例: | ||
|
||
```go | ||
// 本地 IDL | ||
struct A{ | ||
1:required string hello; | ||
} | ||
``` | ||
|
||
```go | ||
// 远端 IDL | ||
struct A{ | ||
1:required string hello; | ||
2:required string hello2; | ||
} | ||
|
||
struct B{ | ||
1:required string hello; | ||
} | ||
``` | ||
|
||
公共结构体场景生成的 Golang 代码为: | ||
|
||
```go | ||
package xxx | ||
import "repo/xxxx/remote" | ||
|
||
type A = remote.A | ||
``` | ||
|
||
由于实际的 Golang 结构体是使用的远端的,所以在公共结构体场景下,这份 IDL 的元信息在反射注册时将以远端的 IDL 情况为准(也就是既有 A 结构体也有 B 结构体的那个 IDL)。 | ||
|
||
此外还有一种特殊情况目前无法支持(出现概率极小),当对远端的一个 IDL 进行了拆分,让本地的一份 IDL 对应到了远端的两个 IDL 时,这种场景无法成功注册反射信息。 |