Kitex Tool 解析第二期 —— 文件是怎么自动生成的

547 阅读6分钟

1. 前言

大家好,这是我解读 kitex 源码的第二篇文章,是继续上回未分析的部分而写的,希望大家可以阅读一下,这次我使用的是和上次相反的思路来分析代码的。

对上一篇文章有兴趣的朋友可以看这里,上一篇文章主要介绍了 Kitex tool 大致生成文件的思路,但是我并没有讲述它是如何从template 变成 go 文件的过程,而是将它忽略了。

上述未补充的思路将由这篇文章进行补充。

这次文章需要了解 golang 中的 template (tpl),希望大家看之前先去了解一下它的用法。我会带大家一点破面,慢慢的来解析这些代码

2. 介绍

CloudWeGo-Kitex

Kitex[kaɪt’eks] 为字节跳动内部的 Golang 微服务 RPC 框架,具有高性能强可扩展的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。

3. 相反的开始,从”终点“开始

克隆 Kitex 的代码,

git clone https://github.com/cloudwego/kitex.git

打开并观察一下kitex tool 的具体目录结构,

我们可以发现, 模板文件中的绝大部分就储存在其中的 internal/pkg/tpl 目录之中

 root@root:/kitex/tool$ tree
 .                  
 ├── cmd            
 │   └── kitex      
 │       ├── args.go
 │       └── main.go
 └── internal       
     └── pkg        
         ├── generator            
         │   ├── completor.go     
         │   ├── feature.go       
         │   ├── generator.go     
         │   ├── generator_test.go
         │   └── type.go          
         ├── log                  
         │   └── log.go           
         ├── pluginmode           
         │   ├── protoc       
         │   │   ├── plugin.go
         │   │   ├── protoc.go
         │   │   └── util.go
         │   └── thriftgo
         │       ├── convertor.go
         │       ├── file_tpl.go
         │       ├── patcher.go
         │       ├── plugin.go
         │       └── struct_tpl.go
         ├── tpl
         │   ├── bootstrap_tpl.go
         │   ├── build_tpl.go
         │   ├── client_tpl.go
         │   ├── handler_tpl.go
         │   ├── invoker_tpl.go
         │   ├── main_tpl.go
         │   ├── server_tpl.go
         │   └── service_tpl.go
         └── util
             └── util.go
 ​
 ​

于是我们取出一个较为简单 一个模板文件作为示例:

eg: main_tpl.go

 package tpl
 ​
 // MainTpl is the template for generating main.go.
 ​
 var MainTpl = `
 package main
 ​
 import (
   {{- range $path, $alias := .Imports}}
   {{$alias }}"{{$path}}"
   {{- end}}
 )
 ​
 func main() {
     svr := {{.PkgRefName}}.NewServer(new({{.ServiceName}}Impl))
 ​
     err := svr.Run()
 ​
     if err != nil {
         log.Println(err.Error())
     }
 }
 `

这是 官方示例 kitex-examples hello 文件夹中的 main.go

 package main
 ​
 import (
   "log"
 ​
   api "github.com/cloudwego/kitex-examples/hello/kitex_gen/api/hello"
 )
 ​
 func main() {
   svr := api.NewServer(new(HelloImpl))
 ​
   err := svr.Run()
   if err != nil {
     log.Println(err.Error())
   }
 }
 ​

有没有发现这两个文件的结构基本一致?只是在占位符 {{ }} 中的变量被替换成为了具体的变量,因此我们可以从这些变量出发,来看看它们的作用。

在 IDE 中搜索 PkgRefName, 结果发现它存在于 tool/internal/pkg/generator/type.go

1.jpg

从以下以下内容可知,此为 渲染 (render) 模板的文件,那是谁调用的它呢,我们可以继续往上找

 package generator
 ​
 import (
   "bytes"
   "path/filepath"
   "strings"
   "text/template"
 ​
   "github.com/cloudwego/kitex/tool/internal/pkg/util"
 )
 ​
 // File .
 type File struct {
   Name    string
   Content string
 }
 ​
 // PackageInfo contains information to generate a package for a service.
 type PackageInfo struct {
   Namespace    string            // a dot-separated string for generating service package under kitex_gen
   Dependencies map[string]string // package name => import path, used for searching imports
   *ServiceInfo                   // the target service
 ​
   // the following fields will be filled and used by the generator
   Codec            string
   Version          string
   RealServiceName  string
   Imports          map[string]string // import path => alias
   ExternalKitexGen string
   Features         []feature
 }
 ​
 // AddImport .
 func (p *PackageInfo) AddImport(pkg, path string) {
   if pkg != "" {
     if p.ExternalKitexGen != "" && strings.Contains(path, KitexGenPath) {
       parts := strings.Split(path, KitexGenPath)
       path = filepath.Join(p.ExternalKitexGen, parts[len(parts)-1])
     }
     if strings.HasSuffix(path, "/"+pkg) || path == pkg {
       p.Imports[path] = ""
     } else {
       p.Imports[path] = pkg
     }
   }
 }
 ​
 // AddImports .
 func (p *PackageInfo) AddImports(pkgs ...string) {
   for _, pkg := range pkgs {
     if path, ok := p.Dependencies[pkg]; ok {
       p.AddImport(pkg, path)
     } else {
       p.AddImport(pkg, pkg)
     }
   }
 }
 ​
 // PkgInfo .
 type PkgInfo struct {
   PkgName    string
   PkgRefName string
   ImportPath string
 }
 ​
 // ServiceInfo .
 type ServiceInfo struct {
   PkgInfo
   ServiceName     string
   RawServiceName  string
   ServiceTypeName func() string
   Base            *ServiceInfo
   Methods         []*MethodInfo
   CombineServices []*ServiceInfo
   HasStreaming    bool
 }
 ​
 // AllMethods returns all methods that the service have.
 func (s *ServiceInfo) AllMethods() (ms []*MethodInfo) {
   ms = s.Methods
   for base := s.Base; base != nil; base = base.Base {
     ms = append(base.Methods, ms...)
   }
   return ms
 }
 ​
 // MethodInfo .
 type MethodInfo struct {
   PkgInfo
   ServiceName            string
   Name                   string
   RawName                string
   Oneway                 bool
   Void                   bool
   Args                   []*Parameter
   Resp                   *Parameter
   Exceptions             []*Parameter
   ArgStructName          string
   ResStructName          string
   IsResponseNeedRedirect bool // int -> int*
   GenArgResultStruct     bool
   ClientStreaming        bool
   ServerStreaming        bool
 }
 ​
 // Parameter .
 type Parameter struct {
   Deps    []PkgInfo
   Name    string
   RawName string
   Type    string // *PkgA.StructB
 }
 ​
 var funcs = map[string]interface{}{
   "ToLower":    strings.ToLower,
   "LowerFirst": util.LowerFirst,
   "UpperFirst": util.UpperFirst,
   "NotPtr":     util.NotPtr,
   "HasFeature": HasFeature,
 }
 ​
 // Task .
 type Task struct {
   Name string
   Path string
   Text string
   *template.Template
 }
 ​
 // Build .
 func (t *Task) Build() error {
   x, err := template.New(t.Name).Funcs(funcs).Parse(t.Text)
   if err != nil {
     return err
   }
   t.Template = x
   return nil
 }
 ​
 // Render .
 func (t *Task) Render(data interface{}) (*File, error) {
   if t.Template == nil {
     err := t.Build()
     if err != nil {
       return nil, err
     }
   }
 ​
   var buf bytes.Buffer
   err := t.ExecuteTemplate(&buf, t.Name, data)
   if err != nil {
     return nil, err
   }
   return &File{t.Path, buf.String()}, nil
 }
 ​

4. 进入 generator.go 自底向上

从上面的分析中,我猜测 Render 被调用的可能性较高,于是我们深入进去可以到达另一个文件: tool/internal/pkg/generator.go

 package generator
 ​
 import (
   ...
 )
 ​
 // Constants . 
 // 这些是 kitex 生成过后的基本文件名
 const (
   KitexGenPath    = "kitex_gen"
   KitexImportPath = "github.com/cloudwego/kitex"
   DefaultCodec    = "thrift"
 ​
   BuildFileName     = "build.sh"
   BootstrapFileName = "bootstrap.sh"
   HandlerFileName   = "handler.go"
   MainFileName      = "main.go"
   ClientFileName    = "client.go"
   ServerFileName    = "server.go"
   InvokerFileName   = "invoker.go"
   ServiceFileName   = "*service.go"
 )
 ​
 ...
 ​
 // Generator generates the codes of main package and scripts for building a server based on kitex.
 // 这个接口定义了 生成 service 和 package main 的函数,我们看 GenerateMainPackage 因为我们是从 main_tpl 上来的嘛
 type Generator interface {
   GenerateService(pkg *PackageInfo) ([]*File, error)
   GenerateMainPackage(pkg *PackageInfo) ([]*File, error)
 }
 ​
 ...
 ​
 func (g *generator) GenerateMainPackage(pkg *PackageInfo) (fs []*File, err error) {
   // 设置 PakageInfo 的信息
   g.updatePackageInfo(pkg)
 ​
   // 设置了 generate 的任务
   tasks := []*Task{
     {
       // 生成的变量名: build.sh 为上方定义的 const
       Name: BuildFileName,
       // 输出的具体路径 eg: hello/build.sh 
       Path: filepath.Join(g.OutputPath, BuildFileName),
       // 用于 render 的 template
       Text: tpl.BuildTpl,
     },
     {
       Name: BootstrapFileName,
       Path: filepath.Join(g.OutputPath, "script", BootstrapFileName),
       Text: tpl.BootstrapTpl,
     },
   }
   // generate main.go with invoker when main package generate 
   // 这是 GenerateInvoker 的注释 查看是否生成了main.go 
   // 未生成就 add 进任务中 
   if !g.Config.GenerateInvoker {
     tasks = append(tasks, &Task{
       Name: MainFileName,
       Path: filepath.Join(g.OutputPath, MainFileName),
       Text: tpl.MainTpl,
     })
   }
   // 按序执行任务
   for _, t := range tasks {
     // 生成过就 skipped
     if util.Exists(t.Path) {
       log.Info(t.Path, "exists. Skipped.")
       continue
     }
     
     g.setImports(t.Name, pkg)
     handle := func(task *Task, pkg *PackageInfo) (*File, error) {
       // 注意, 这里就是点进来的入口
       return task.Render(pkg)
     }
     f, err := g.chainMWs(handle)(t, pkg)
     if err != nil {
       return nil, err
     }
     fs = append(fs, f)
   }
 ​
   handlerFilePath := filepath.Join(g.OutputPath, HandlerFileName)
   if util.Exists(handlerFilePath) {
     comp := newCompleter(
       pkg.ServiceInfo.AllMethods(),
       handlerFilePath,
       pkg.ServiceInfo.ServiceName)
     f, err := comp.CompleteMethods()
     if err != nil {
       if err == errNoNewMethod {
         return fs, nil
       }
       return nil, err
     }
     fs = append(fs, f)
   } else {
     task := Task{
       Name: HandlerFileName,
       Path: handlerFilePath,
       Text: tpl.HandlerTpl,
     }
     g.setImports(task.Name, pkg)
     handle := func(task *Task, pkg *PackageInfo) (*File, error) {
       return task.Render(pkg)
     }
     f, err := g.chainMWs(handle)(&task, pkg)
     if err != nil {
       return nil, err
     }
     fs = append(fs, f)
   }
   return
 }

这里只是举个例,我们可以发现在这里 render 函数被 GenerateMainPackage 调用了, 在这个环节, main.go, build.sh等模板将会被渲染完成, 自此我们可以通过这个函数再次向上探索.

5. 精妙的文件设计

这时候我们会发现它在一个文件中使用了两次,该文件为 plugin.go

2.jpg

      ├── pluginmode           
         │   ├── protoc       
         │   │   ├── plugin.go
         │   │   ├── protoc.go
         │   │   └── util.go
         │   └── thriftgo
         │       ├── convertor.go
         │       ├── file_tpl.go
         │       ├── patcher.go
         │       ├── plugin.go
         │       └── struct_tpl.go

我以 protoc 为例,主要原因是对于我来说,使用 grpc 是较多的。我们会发现文件夹的设计很有意思,它让我感到眼熟

 ├── pluginmode           
         ├── protoc          
         └── thriftgo

没错,如果你看过我的上篇文章,你会发现它的设计对应着一段代码, 一段 tool/cmd/kitex/main.go 中的代码

 package main
 ​
 import (
   ...
 )
 ...
 ​
 func main() {
   // run as a plugin
   switch filepath.Base(os.Args[0]) {
   case thriftgo.PluginName:
     os.Exit(thriftgo.Run())
   case protoc.PluginName:
     os.Exit(protoc.Run())
   }
 }  

这表明 代码的生成就是在这个 switch 中进行的,我们进入 protoc.Run() 继续观察, 很明显它执行了私有的 run()

3.jpg

忽略一些代码我们会看到这段注释, // generate files , 很明显在它下面就是代码就会生成

 package protoc
 ​
 func run(opts protogen.Options) error {
   ...
   // init plugin
   pp := new(protocPlugin)
   
   // generate files
   gen, err := opts.New(req)
   if err != nil {
     return err
   }
   pp.process(gen)
   gen.SupportedFeatures = gengo.SupportedFeatures
 ​
   // construct plugin response
   resp := gen.Response()
   out, err := proto.Marshal(resp)
   if err != nil {
     return err
   }
   if _, err := os.Stdout.Write(out); err != nil {
     return err
   }
   return nil
 }

于是我们挨个试探以下,于是我们会发现 pp.process(gen) 中有着一条熟悉的代码, GenerateMainPackage()

 func (pp *protocPlugin) process(gen *protogen.Plugin) {
     ...
     fs, err := pp.kg.GenerateMainPackage(&pp.PackageInfo)
     ...
   return
 }

至此,我们可以说简约的理顺了代码生成的原理执行顺序为

 tool/cmd/main.go (选择生成模式)
 |
 |
 tool/internal/pkg/pluginmode/protoc/protoc.go (生成grpc相关代码, 并将参数向下传递)
 |
 |
 tool/internal/pkg/generator/generator.go (将参数收集为 []Task, 并结合 template 输出具体代码)

6. 总结

文章的主体部分在此到一段落,那我们从文章中学到什么了呢 ? 这是需要思考总结的:

  1. 见识到了 企业中模板文件的使用方式和定义规范
  2. 见到了 好的代码就是最好的注释的 诠释案例
  3. 既然我们知道了生成的原理,我们自己能不能为了自己项目的需求,添加或者修改业务逻辑 以达到个性化的目的呢?

以上是我本人对于这次源码阅读的一些思考,希望可以对阅读本文的朋友带来一定的帮助,感谢大家耐心看完!

\