0x00 背景
从两年前开始,我们使用 grpc/grpc-java 作为后端服务的通讯方式,但面向前端时仍然使用 HTTP API 。
随着 PTA | 程序设计类实验辅助教学平台 的功能越来越多,以及逐步的服务化拆分,新服务上线,后端所提供的 API(RPC)也逐渐增多。
当修改影响到 HTTP API 时,每次需要先增加(修改)protobuf 定义,编写后端业务逻辑,再修改 controller 添加对应的 HTTP 接口,通常需要在 3-4 个项目中分别修改、开 PR、跑 CI 以及等待 review,过于繁杂。且大部分情况下 controller 只是对 gRPC 接口在 HTTP 的简单转换,并没有任何业务逻辑:
目前社区对此的主要项目为 grpc-ecosystem/grpc-gateway 一个使用 golang 编写的 protoc 插件,在 proto 文件中声明 HTTP endpoint、method 等信息,生成转换代码。
但我们后端项目都是 kotlin + spring boot 开发,提供 HTTP API 的服务并不仅仅是协议的转换,还包含了用户身份验证、限流、日志等一系列功能,转换成 golang 开发不太现实。因此我们决定参考 grpc-ecosystem/grpc-gateway 的定义方式,自行编写 protoc 插件,生成 spring MVC 的 controller。
0x01 特性
- 支持 google api 中 HttpRule 的部分语法,包括 method,path variable,query parameter,post body 等:
- 生成 spring MVC handler method,使用 spring MVC 现有机制静态分发:
- Request body 与返回值支持 json 或 protobuf 两种类型(通过 content-type 或 accept 头定义)。
- Query parameter 中的嵌套 protobuf message,可以直接使用 json string。
不支持的功能
- 在 path 中定义 segment (可以用 additional binding 绕过)。
- 在 path 中嵌套定义字段(只能定义 request 中最外层存在的变量)。
不同的行为
- Query parameter 每个参数都需要显示定义(golang 的库默认映射所有不在 path 中的顶层参数到 query 中),但可以设置默认值。
- 在 service 中设置所属的 zone 以及对应的后端服务名称,在编译时传入不同的参数跳过某些 service 的生成:
0x02 生成器
生成器是这一项目核心的一部分,protoc 提供了非常方便且易于扩展的插件机制,在 proto 编译过程中,调用插件并通过标准输入传入 CodeGeneratorRequest ,这本身就是个 proto 类型,包含了所有 proto 定义文件,命令行参数等信息。运行完毕后将 CodeGeneratorResponse 打印到标准输出,包含生成的所有文件相对路径以及内容,protoc 会负责写入对应的文件。
首先在程序入口从标准输入中构建 CodeGeneratorRequest,并注册对应的 extension:
protoc 保证了构建请求中文件的依赖关系符合拓扑顺序,因此我们可以直接按顺序构建 FileDescriptor,每个 service 生成对应的 controller:
代码生成器自身使用了 square/javapoet ,可以非常方便的生成 Java 源代码,具体实现可以直接参考源码。
0x03 生成前端代码
平时与前端的合作流程中,我们需要将 HTTP 接口整理成文档、或直接让前端查看后端这些声明来调用 API,容易遇到接口变化更新不及时、漏看了参数等问题。
然而目前对接口的声明中已经包含了一个前端请求所需要的全部信息,我们可以直接根据这些信息生成 redux action 发请求的代码。
首先我们在生成 controller 的过程中,同时对每个 method 生成一个 json。其中 body 是 proto 的一个 message,前端可以将这个转换为 flow type 或 typescript 定义。
例如对于前面的 Rejudge:
对于最终发出一次 rpc 所需要的信息就是整个 request,而生成的后端转换代码就是将参数从 post body、path 以及 query parameter 中取出来,重新构造出 rpc 所需的 request 类型。前端却是依照我们的设计(例如为了符合 RESTful),将参数放在该放的地方。这两者是互逆的过程。
0x04 总结
gRPC gateway generator spring 生成的控制器文件可以大量减少编写转换代码的重复劳动,更关注实现业务本身,同时我们生成的代码尽量使用了 spring MVC 自身的功能,减少生成代码中动态匹配路径等造成的 bug。
我们希望能够逐渐使用声明式写法减少重复劳动,降低代码 review 工作的心智负担,通过一份类型定义(proto)优化前后端项目的开发体验,静态检查以及所执行的代码。