写点什么

Kratos 微服务框架 API 工程化指南

作者:喵个咪
  • 2023-01-21
    湖南
  • 本文字数:20623 字

    阅读完需:约 68 分钟

Kratos 微服务框架 API 工程化指南

Kratos 的 RPC 默认使用的是gRPC,与此同时我们还可以通过 gRPC 的grpc-gateway功能对 RESTfull 进行支持。这样,我们就可以同时支持 gRPC 和 REST 了。而这一切 Kratos 都已经封装好,无需知道底层的一切,用就好了。


gRPC 是基于Protobuf作为接口规范的描述语言(IDL,Interface Description Language)。换句通俗的话来说,gRPC 使用 Protobuf 来设计和管理 API。我们只需要编写一套 Protobuf 文件,就能够支持 gRPC 协议和 RESTfull 协议。Protobuf 支持很多编程语言,比如:C++、Java、JavaScript、Python、Go、Ruby、Objective-C、C#……这也就意味着,它很适合多语言异构化架构,这样的场景在现实中是很稀松平常的,这使得 Protobuf 具有很强的实用性。


Protobuf 具有序列化后数据量更小、序列化/反序列化速度更快、更简单的特性;而 JSON 则相反,序列化后数据量较大,序列化和反序列化速度不优的特性,但是前端对 JSON 是原生支持,对前端极其友好。那么,我们可以在服务之间使用 gRPC 进行通讯,服务与前端之间可以通过 RESTfull 进行通讯。


Protobuf 和 gRPC 已经发展了许多年,极其稳定,生态链丰富。它具有强大的工具链可供使用,只要你想得到的,都能够找得到相对应的工具。没有合适的工具也没有关系,它的工具是使用插件方式来实现可扩展性的,因此我们可以容易的开发出自己的工具插件,Kratos 就为此开发了自己的一系列的工具插件方便开发使用。


综上,我们可知使用 gRPC/protobuf 的好处:


  1. 一套 proto,同时支持 gRPC 协议和 RESTfull 协议;

  2. 支持多编程语言,适合多语言异构化架构;

  3. gRPC 协议,数据量小、序列化/反序列化速度更快、更简单,适合服务之间通讯;

  4. RESTfull 协议,数据量较大、序列化/反序列化速度较慢、前端原生支持 JSON,适合同前端的通讯。

  5. 强大的工具链,使用插件的方式实现强大的可扩展性,可方便的扩展。


那么,这篇文章将会带来一些什么呢?


  1. Protobuf 设计 API 的一丢丢基本知识;

  2. 相关工具链的使用方法;

  3. 如何实施工程化的方法。

工具安装

工欲善其事,必先利其器。


让我们先安装所需要的工具。

安装 protoc

protoc 是一款用 C++编写的工具,其可以将 proto 文件翻译为指定语言的代码。


具体用法可以使用protoc --help命令查看。

goctl 一键安装

$ goctl env check -i -f --verbose                                 [goctl-env]: preparing to check env
[goctl-env]: looking up "protoc"[goctl-env]: "protoc" is not found in PATH[goctl-env]: preparing to install "protoc""protoc" installed from cache[goctl-env]: "protoc" is already installed in "/Users/keson/go/bin/protoc"
复制代码

macOS 安装

brew install protobuf
复制代码

Ubuntu 安装

sudo apt update; sudo apt upgradesudo apt install libprotobuf-dev protobuf-compiler
复制代码

非 Windows 系统源代码安装

  1. 进入 protobuf release 下载页面下载;

  2. 解压并进入文件夹:

  3. 设置编译目录

  4. 安装检测

  5. 安装及编译

  6. 配置环境变量

  7. 在文件结尾添加环境变量

  8. 使用 source 命令,使配置文件生效

非 Windows 系统源二进制文件安装

  1. 进入 protobuf release 下载页面,选择适合自己操作系统的压缩包文件下载;

  2. 解压文件:

  3. 拷贝 protoc 文件

  4. 拷贝头文件

Windows 安装

在 Windows 下可以使用包管理器ChocoScoop来安装。


  • Choco

  • Scoop

后端工具

后端工具都可以使用go install进行安装:


  • 用于生成 struct 代码:

  • 用于生成 grpc 服务代码:

  • 用于生成 rest 服务代码:

  • 用于生成 kratos 的错误定义代码:

  • 用于生成消息验证器代码:

  • 用于生成 OpenAPI V2 文档:

  • 用于生成 OpenAPI V3 文档:

前端工具

这是 protobuf.js 提供的一个 Protobuf 转换为 Typescript 的工具:


pnpm i pbts -g
复制代码


另,我还找到一个基于 pbts 开发的在线工具:https://pb.brandonxiang.top/

设计 API

在开始前,首先要说明的是,本文并不是一个 Protobuf 或者 gRPC 的教程,这方面,谷歌官方以及其他第三方(gRPC-Gateway)提供的资料已经足够详尽了:


CURD

在现实场景下,业务代码写得最多的恐怕还属 CURD(增、删、改、查)了,不说多,80%是肯定有的,可以说,只要搞定了 CURD,就搞定了大部分的业务代码的编写。


以下是一个 gRPC 官方提供的示例,是一个书店的接口,里面包含了基本的 Protobuf 的语法和用法,以及 gRPC 服务和 REST 服务的设计。


syntax = "proto3";
package endpoints.examples.bookstore;
option java_multiple_files = true;option java_outer_classname = "BookstoreProto";option java_package = "com.google.endpoints.examples.bookstore";
option go_package = "endpoints/examples/bookstore;bookstore";

import "google/api/annotations.proto";import "google/protobuf/empty.proto";
// A simple Bookstore API.//// The API manages shelves and books resources. Shelves contain books.service Bookstore { // Returns a list of all shelves in the bookstore. rpc ListShelves(google.protobuf.Empty) returns (ListShelvesResponse) { // Define HTTP mapping. // Client example (Assuming your service is hosted at the given 'DOMAIN_NAME'): // curl http://DOMAIN_NAME/v1/shelves option (google.api.http) = { get: "/v1/shelves" }; } // Creates a new shelf in the bookstore. rpc CreateShelf(CreateShelfRequest) returns (Shelf) { // Client example: // curl -d '{"theme":"Music"}' http://DOMAIN_NAME/v1/shelves option (google.api.http) = { post: "/v1/shelves" body: "shelf" }; } // Returns a specific bookstore shelf. rpc GetShelf(GetShelfRequest) returns (Shelf) { // Client example - returns the first shelf: // curl http://DOMAIN_NAME/v1/shelves/1 option (google.api.http) = { get: "/v1/shelves/{shelf}" }; } // Deletes a shelf, including all books that are stored on the shelf. rpc DeleteShelf(DeleteShelfRequest) returns (google.protobuf.Empty) { // Client example - deletes the second shelf: // curl -X DELETE http://DOMAIN_NAME/v1/shelves/2 option (google.api.http) = { delete: "/v1/shelves/{shelf}" }; }}
// A shelf resource.message Shelf { // A unique shelf id. int64 id = 1; // A theme of the shelf (fiction, poetry, etc). string theme = 2;}
// Response to ListShelves call.message ListShelvesResponse { // Shelves in the bookstore. repeated Shelf shelves = 1;}
// Request message for CreateShelf method.message CreateShelfRequest { // The shelf resource to create. Shelf shelf = 1;}
// Request message for GetShelf method.message GetShelfRequest { // The ID of the shelf resource to retrieve. int64 shelf = 1;}
// Request message for DeleteShelf method.message DeleteShelfRequest { // The ID of the shelf to delete. int64 shelf = 1;}
复制代码


需要说明的是,REST 的接口是由google.api.http这个option提供的。上面这一套接口定义,既可以生成 gRPC 的服务,又可以生成 REST 的服务,而这是根据 protoc 调用的插件决定的,这方面内容不是这部分所要阐述的,暂且不表,且看后面部分。

Kratos Errors

在实际应用当中,存在着一个问题:gRPC 状态码 和 REST HTTP 状态码 是不一样的。为了解决这个问题,就需要一个映射表,用来互相转换状态码。


以下就是一个映射表的示例:


syntax = "proto3";
// 定义包名package api.kratos.v1;import "errors/errors.proto";
// 多语言特定包名,用于源代码引用option go_package = "kratos/api/helloworld;helloworld";option java_multiple_files = true;option java_package = "api.helloworld";
enum ErrorReason { // 设置缺省错误码 option (errors.default_code) = 500;
// 为某个枚举单独设置错误码 USER_NOT_FOUND = 0 [(errors.code) = 404];
CONTENT_MISSING = 1 [(errors.code) = 400];}
复制代码


它利用了 Protobuf 的enumoption关键字实现了这样一个状态码的映射。再由 protoc 插件生成的代码实现映射和互换。

Message Validator

在实际应用当中,需要对接口的参数进行一些校验,比如:用户名的长度只能够大于或者小于某一个长度,身份证、手机号、EMail 等特定格式的有效校验。


其实,都不过是一些字符串、数字类型和布尔类型校验的简单规则。如果手写校验代码,都是一些机械无比的重复代码,而且要作修改起来也很痛苦。


那么,有什么办法可以解决这个问题吗?必须有:规则写在 Protobuf 里面,利用proto-gen-validate插件生成代码,使用 Kratos Validate 中间件 作支持。


以下是proto-gen-validate插件的示例接口:


syntax = "proto3";
package examplepb;
import "validate/validate.proto";
message Person { uint64 id = 1 [(validate.rules).uint64.gt = 999];
string email = 2 [(validate.rules).string.email = true];
string name = 3 [(validate.rules).string = { pattern: "^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$", max_bytes: 256, }];
Location home = 4 [(validate.rules).message.required = true];
message Location { double lat = 1 [(validate.rules).double = {gte: -90, lte: 90}]; double lng = 2 [(validate.rules).double = {gte: -180, lte: 180}]; }}
复制代码


只需要利用validate.rulesoption 就可以定义规则了,简单明了,又方便。

OpenAPI

OpenAPI 是一个用于描述 REST API 的描述格式,包含端点、参数、输入输出格式、说明、认证等,本质上它是一个 JSON 或者 YAML 格式文档,而文件内的 Schema 则是有 OpenAPI 所定义的。

OpenAPI JSON 范例

以下是一个 OpenAPI v3 的 JSON 文件范例:


{  "openapi": "3.0",  "info": {    "version": "1.0.0",    "title": "OpenAPI Petstore",    "license": {      "name": "MIT"    }  },  "servers": [    {      "url": "https://petstore.openapis.org/v1",      "description": "Development server"    }  ],  "paths": {    "/pets": {      "get": {        "summary": "List all pets",        "operationId": "listPets",        "tags": [          "pets"        ],        "parameters": [          {            "name": "limit",            "in": "query",            "description": "How many items to return at one time (max 100)",            "required": false,            "schema": {              "type": "integer",              "format": "int32"            }          }        ],        "responses": {          "200": {            "description": "An paged array of pets",            "headers": {              "x-next": {                "schema": {                  "type": "string"                },                "description": "A link to the next page of responses"              }            },            "content": {              "application/json": {                "schema": {                  "$ref": "#/components/schemas/Pets"                }              }            }          },          "default": {            "description": "unexpected error",            "content": {              "application/json": {                "schema": {                  "$ref": "#/components/schemas/Error"                }              }            }          }        }      },      "post": {        "summary": "Create a pet",        "operationId": "createPets",        "tags": [          "pets"        ],        "responses": {          "201": {            "description": "Null response"          },          "default": {            "description": "unexpected error",            "content": {              "application/json": {                "schema": {                  "$ref": "#/components/schemas/Error"                }              }            }          }        }      }    },    "/pets/{petId}": {      "get": {        "summary": "Info for a specific pet",        "operationId": "showPetById",        "tags": [          "pets"        ],        "parameters": [          {            "name": "petId",            "in": "path",            "required": true,            "description": "The id of the pet to retrieve",            "schema": {              "type": "string"            }          }        ],        "responses": {          "200": {            "description": "Expected response to a valid request",            "content": {              "application/json": {                "schema": {                  "$ref": "#/components/schemas/Pets"                }              }            }          },          "default": {            "description": "unexpected error",            "content": {              "application/json": {                "schema": {                  "$ref": "#/components/schemas/Error"                }              }            }          }        }      }    }  },  "components": {    "schemas": {      "Pet": {        "required": [          "id",          "name"        ],        "properties": {          "id": {            "type": "integer",            "format": "int64"          },          "name": {            "type": "string"          },          "tag": {            "type": "string"          }        }      },      "Pets": {        "type": "array",        "items": {          "$ref": "#/components/schemas/Pet"        }      },      "Error": {        "required": [          "code",          "message"        ],        "properties": {          "code": {            "type": "integer",            "format": "int32"          },          "message": {            "type": "string"          }        }      }    }  }}
复制代码

OpenAPI YAML 范例

以及 OpenAPI v3 的 YAML 文件范例:


openapi: "3.0"info:  version: 1.0.0  title: OpenAPI Petstore  license:    name: MITservers:- url: https://petstore.openapis.org/v1  description: Development serverpaths:  /pets:    get:      summary: List all pets      operationId: listPets      tags:      - pets      parameters:      - name: limit        in: query        description: How many items to return at one time (max 100)        required: false        schema:          type: integer          format: int32      responses:        "200":          description: An paged array of pets          headers:            x-next:              schema:                type: string              description: A link to the next page of responses          content:            application/json:              schema:                $ref: '#/components/schemas/Pets'        default:          description: unexpected error          content:            application/json:              schema:                $ref: '#/components/schemas/Error'    post:      summary: Create a pet      operationId: createPets      tags:      - pets      responses:        "201":          description: Null response        default:          description: unexpected error          content:            application/json:              schema:                $ref: '#/components/schemas/Error'  /pets/{petId}:    get:      summary: Info for a specific pet      operationId: showPetById      tags:      - pets      parameters:      - name: petId        in: path        required: true        description: The id of the pet to retrieve        schema:          type: string      responses:        "200":          description: Expected response to a valid request          content:            application/json:              schema:                $ref: '#/components/schemas/Pets'        default:          description: unexpected error          content:            application/json:              schema:                $ref: '#/components/schemas/Error'components:  schemas:    Pet:      required:      - id      - name      properties:        id:          type: integer          format: int64        name:          type: string        tag:          type: string    Pets:      type: array      items:        $ref: '#/components/schemas/Pet'    Error:      required:      - code      - message      properties:        code:          type: integer          format: int32        message:          type: string
复制代码

OpenAPI 工具

以上文本当中的 Schema,有些可以望文生义,也有一些根本看不出来意义。可是,真要让人去阅读,只会有一个感受:头大。它主要还是给程序读取的,展现在 UI 之上,才能够真正的应用起来。


现在,市面上有非常非常多的工具可以读取 OpenAPI JSON / YAML 文档:



这些工具当中,最常见的是本家的 Swagger UI(OpenAPI 在成为开放标准之前是 Swagger 产品线当中的一部分),它经常被内嵌到 Web 框架里面。

Protobuf 生成 OpenAPI 工具

现在 OpenAPI 有两个版本:v2 和 v3。


主流的 protoc 插件也刚好对应有两个:


  1. OpenAPI v2 使用 grpc-gateway 出的protoc-gen-openapiv2

  2. OpenAPI v3 使用谷歌出品的 gnostic 下的protoc-gen-openapi


正常来说,只要是使用了google.api.http这个option定义的 API,使用这两个插件就能够生成 OpenAPI 文档。


但是,实际应用中,我们还希望能够提供更多更丰富的一些信息,比如:描述信息、版本号、版权信息、认证信息……显然,光凭着google.api.http的定义是不够的。这两个插件提供了各自的option,可以定义这些信息。

Protobuf 中如何定义 OpenAPI V2 注解

syntax = "proto3";
package grpc.gateway.examples.internal.proto.examplepb;
import "protoc-gen-openapiv2/options/annotations.proto";
option go_package = "github.com/grpc-ecosystem/grpc-gateway/v2/examples/internal/proto/examplepb";option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "A Bit of Everything"; version: "1.0"; contact: { name: "gRPC-Gateway project"; url: "https://github.com/grpc-ecosystem/grpc-gateway"; email: "none@example.com"; }; license: { name: "BSD 3-Clause License"; url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt"; }; extensions: { key: "x-something-something"; value { string_value: "yadda"; } } }; // Overwriting host entry breaks tests, so this is not done here. external_docs: { url: "https://github.com/grpc-ecosystem/grpc-gateway"; description: "More about gRPC-Gateway"; } schemes: HTTP; schemes: HTTPS; schemes: WSS; consumes: "application/json"; consumes: "application/x-foo-mime"; produces: "application/json"; produces: "application/x-foo-mime"; security_definitions: { security: { key: "BasicAuth"; value: { type: TYPE_BASIC; } } security: { key: "ApiKeyAuth"; value: { type: TYPE_API_KEY; in: IN_HEADER; name: "X-API-Key"; extensions: { key: "x-amazon-apigateway-authtype"; value { string_value: "oauth2"; } } extensions: { key: "x-amazon-apigateway-authorizer"; value { struct_value { fields { key: "type"; value { string_value: "token"; } } fields { key: "authorizerResultTtlInSeconds"; value { number_value: 60; } } } } } } } security: { key: "OAuth2"; value: { type: TYPE_OAUTH2; flow: FLOW_ACCESS_CODE; authorization_url: "https://example.com/oauth/authorize"; token_url: "https://example.com/oauth/token"; scopes: { scope: { key: "read"; value: "Grants read access"; } scope: { key: "write"; value: "Grants write access"; } scope: { key: "admin"; value: "Grants read and write access to administrative information"; } } } } } security: { security_requirement: { key: "BasicAuth"; value: {}; } security_requirement: { key: "ApiKeyAuth"; value: {}; } } security: { security_requirement: { key: "OAuth2"; value: { scope: "read"; scope: "write"; } } security_requirement: { key: "ApiKeyAuth"; value: {}; } } responses: { key: "403"; value: { description: "Returned when the user does not have permission to access the resource."; } } responses: { key: "404"; value: { description: "Returned when the resource does not exist."; schema: { json_schema: { type: STRING; } } } } responses: { key: "418"; value: { description: "I'm a teapot."; schema: { json_schema: { ref: ".grpc.gateway.examples.internal.proto.examplepb.NumericEnum"; } } } } responses: { key: "500"; value: { description: "Server error"; headers: { key: "X-Correlation-Id" value: { description: "Unique event identifier for server requests" type: "string" format: "uuid" default: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$" } }; schema: { json_schema: { ref: ".grpc.gateway.examples.internal.proto.examplepb.ErrorResponse"; } } } } tags: { name: "echo rpc" description: "Echo Rpc description" extensions: { key: "x-traitTag"; value { bool_value: true; } } } extensions: { key: "x-grpc-gateway-foo"; value { string_value: "bar"; } } extensions: { key: "x-grpc-gateway-baz-list"; value { list_value: { values: { string_value: "one"; } values: { bool_value: true; } } } }};
message ErrorResponse { string correlationId = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", title: "x-correlation-id", description: "Unique event identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; ErrorObject error = 2;}
message ErrorObject { int32 code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9]$", title: "code", description: "Response code", format: "integer" }]; string message = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[a-zA-Z0-9]{1, 32}$", title: "message", description: "Response message" }];}
// ABitOfEverything service is used to validate that APIs with complicated// proto messages and URL templates are still processed correctly.service ABitOfEverythingService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_tag) = { description: "ABitOfEverythingService description -- which should not be used in place of the documentation comment!" external_docs: { url: "https://github.com/grpc-ecosystem/grpc-gateway"; description: "Find out more about EchoService"; } };
// Create a new ABitOfEverything // // This API creates a new ABitOfEverything rpc Create(ABitOfEverything) returns (ABitOfEverything) { option (google.api.http) = { post: "/v1/example/a_bit_of_everything/{float_value}/{double_value}/{int64_value}/separator/{uint64_value}/{int32_value}/{fixed64_value}/{fixed32_value}/{bool_value}/{string_value=strprefix/*}/{uint32_value}/{sfixed32_value}/{sfixed64_value}/{sint32_value}/{sint64_value}/{nonConventionalNameValue}/{enum_value}/{path_enum_value}/{nested_path_enum_value}/{enum_value_annotation}" }; } rpc CreateBody(ABitOfEverything) returns (ABitOfEverything) { option (google.api.http) = { post: "/v1/example/a_bit_of_everything" body: "*" }; }}
复制代码

Protobuf 中如何定义 OpenAPI V3 注解

syntax = "proto3";
package tests.openapiv3annotations.message.v1;
import "google/api/annotations.proto";import "openapiv3/annotations.proto";
option go_package = "github.com/google/gnostic/apps/protoc-gen-openapi/examples/tests/openapiv3annotations/message/v1;message";
option (openapi.v3.document) = { info: { title: "Title from annotation"; version: "Version from annotation"; description: "Description from annotation"; contact: { name: "Contact Name"; url: "https://github.com/google/gnostic"; email: "gnostic@google.com"; } license: { name: "Apache License"; url: "https://github.com/google/gnostic/blob/master/LICENSE"; } } components: { security_schemes: { additional_properties: [ { name: "BasicAuth"; value: { security_scheme: { type: "http"; scheme: "basic"; } } } ] } }};
service Messaging1 { rpc UpdateMessage(Message) returns(Message) { option(google.api.http) = { patch: "/v1/messages/{message_id}" body: "*" }; option(openapi.v3.operation) = { security: [ { additional_properties: [ { name: "BasicAuth"; value: { value: [] } } ] } ] }; }}
service Messaging2 { rpc UpdateMessage(Message) returns (Message) {}}
message Message { option (openapi.v3.schema) = { title: "This is an overridden message schema title"; };
int64 id = 1; string label = 2 [ (openapi.v3.property) = { title: "this is an overriden field schema title"; max_length: 255; } ];}
复制代码

代码生成

Protobuf 生成目标语言的代码使用的工具是 protoc,它是基于插件机制开发的,实际生成代码全靠插件。

插件生成文件一览表


这里要提醒一下,细心的你一定会发现,生成 OpenAPI 文档的参数里面各有一个--openapiv2_opt json_names_for_fields=true--openapi_out=naming=json,这两个参数的作用是一样的,那么它们是做什么用的呢?我们先来看下面这个消息定义:


// NonStandardMessageWithJSONNames maps odd field names to odd JSON names for maximum confusion.message NonStandardMessageWithJSONNames {  // Id represents the message identifier.  string id = 1 [json_name = "ID"];  int64 Num = 2 [json_name = "Num"];  int64 line_num = 3 [json_name = "LineNum"];  string langIdent = 4 [json_name = "langIdent"];  string STATUS = 5 [json_name = "status"];  int64 en_GB = 6 [json_name = "En_GB"];  string no = 7 [json_name = "yes"];
message Thing { message SubThing { string sub_value = 1 [json_name = "sub_Value"]; } SubThing subThing = 1 [json_name = "SubThing"]; } Thing thing = 8 [json_name = "Thingy"];}
复制代码


你一定发现了json_name这个参数,没错,就是为了它,proto 那两个参数就是它的开关。如果,字段定义了json_name参数之后,REST 的 JSON 字段名便会采用json_name所定义的字段名。这是一个非常有用的特性,因为前后端的命名规则不一致是常态,golang 用的是驼峰命名法,而前端用蛇形命名法的是很多,这就可以用上了。

生成代码的命令

生成 基础类型的 GO 代码

protoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto
复制代码


以上命令主要是 struct 和 enum 等基础类型

生成 grpc 服务的 GO 代码

protoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto
复制代码

生成 rest 服务的 GO 代码

protoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto
复制代码

生成 gRPC 状态码映射的 GO 代码

protoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto
复制代码

生成 参数校验的 GO 代码

protoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto
复制代码

生成 OpenAPI v2 json 文档

protoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto
复制代码

生成 OpenAPI v3 yaml 文档

protoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto
复制代码

实施工程化

好,我们现在已经知道如何去生成 API 的代码和文档了。但是,这还远远不够。因为我们不可能每次都去手打命令生成代码,这是不科学,不人道的,不现实的。


我们需要工程化,使之可管理。CI/CD、自动化也能够实现。


首先,我们把可用的方法列举出来,然后再一个个的讲解各个方法:


  1. BAT 批处理脚本(Windows)或者 Shell 脚本(非 Windows);

  2. Makefile;

  3. go:generate 注解;

  4. buf.build。


结论在前:推荐使用buf.build

1. BAT 批处理脚本(Windows)或者 Shell 脚本(非 Windows)

BAT 批处理脚本

:: generate go struct codeprotoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto    :: generate grpc service codeprotoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto    :: generate rest service codeprotoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto    :: generate kratos errors codeprotoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto    :: generate message validator codeprotoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto    :: generate openapi v2 json docprotoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto    :: generate openapi v3 yaml docprotoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto
复制代码

Shell 脚本

#!/bin/bash
# generate go struct codeprotoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto # generate grpc service codeprotoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto # generate rest service codeprotoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto # generate kratos errors codeprotoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto # generate message validator codeprotoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto # generate openapi v2 json docprotoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto # generate openapi v3 yaml docprotoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto
复制代码


这个方法除了能用,没有别的好处了。它需要在每一组 proto 文件的同级目录下都冗余放一对脚本,如果要执行所有的生成脚本,另外还需要写一个脚本来调用生成脚本,维护起来很痛苦。

2. Makefile

Kratos官方layout就是使用的 Makefile 的方法来生成代码的。


它在根目录下的 Makefile 文件里:


.PHONY: api# generate api protoapi:    protoc --proto_path=./api \           --proto_path=./third_party \            --go_out=paths=source_relative:./api \            --go-http_out=paths=source_relative:./api \            --go-grpc_out=paths=source_relative:./api \           --openapi_out=fq_schema_naming=true,default_response=false:. \           $(API_PROTO_FILES)
.PHONY: conf# generate config define codeconf: protoc --proto_path=. \ --proto_path=../../../third_party \ --go_out=paths=source_relative:. \ ./internal/conf/*.proto
复制代码


根目录下的 Makefile 由app\{服务名}\service\Makefile引用,调用者在服务目录app\{服务名}\service\下调用make api执行代码生成。


这个方法很有局限性,掣手掣脚,你只能够依照严格的固定的项目结构来,只要有一些变动就完犊子了。


MonoRepo 的项目结构下,因为会有多个 Makefile 入口,所以没办法一键执行全部的 Makefile,必须借助第三方工具,比如 Shell 脚本。偷懒如我,总觉得很麻烦。

3. go:generate 注解

go1.4 版本之后,可以通过go generate命令执行一些go:generate注解下的预处理命令,可以拿来生成 API 代码之用。因为在非 Windows 系统下,命令如果带通配符,会执行出错,需要加sh -c才行,而 Windows 系统不存在这样的问题,可以直接执行,所以需要使用go:build注解来区分操作系统,go generate命令会根据操作系统执行相对应的 go 代码文件。所以,我写了两个 go 文件:

generate_windows.go

//go:build windows
// generate go struct code//go:generate protoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto
// generate grpc service code//go:generate protoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto
// generate rest service code//go:generate protoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto
// generate kratos errors code//go:generate protoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto
// generate message validator code//go:generate protoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto
// generate openapi v2 json doc//go:generate protoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto
// generate openapi v3 yaml doc//go:generate protoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto
package api
复制代码

generate_xnix.go

//go:build !windows// +build !windows
// generate go struct code//go:generate sh -c "protoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto"
// generate grpc service code//go:generate sh -c "protoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto"
// generate rest service code//go:generate sh -c "protoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto"
// generate kratos errors code//go:generate sh -c "protoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto"
// generate message validator code//go:generate sh -c "protoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto"
// generate openapi v2 json doc//go:generate sh -c "protoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto"
// generate openapi v3 yaml doc//go:generate sh -c "protoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto"
package api
复制代码


它可以很好的完成生成代码的任务。主流的 IDE(Goland、VSC)都可以很好的支持编辑界面执行注解。


要自动化吧,也能实现,只要在项目根目录执行go generate ./...就能够执行整个项目的go:generate注解。


但是,有一个很大的问题,它需要在每一组 proto 文件的同级目录下冗余一套 go 代码,维护起来就比较糟心了。

4. buf.build

buf.build是专门用于构建 protobuf API 的工具。


它总共有 3 组配置文件:buf.work.yamlbuf.gen.yamlbuf.yaml


另外,还有一个buf.lock文件,但是它不需要进行人工配置,它是由buf mod update命令所生成。这跟前端的 npm、yarn 等的 lock 文件差不多,golang 的go.sum也差不多。


它的配置文件不多,也不复杂,维护起来非常方便,支持远程 proto 插件,支持远程第三方 proto。对构建系统 Bazel 支持很好,对 CI/CD 系统也支持得很好。它还有很多优秀的特性。


buf.build 非常棒,用它,很方便。值得使用,值得推荐。

buf.work.yaml

它一般放在项目的根目录下面,它代表的是一个工作区,通常一个项目也就一个该配置文件。


该配置文件最重要的就是directories配置项,列出了要包含在工作区中的模块的目录。目录路径必须相对于buf.work.yaml,像../external就是一个无效的配置。


version: v1
directories: - api - third_party
复制代码

buf.gen.yaml

它一般放在buf.work.yaml的同级目录下面,它主要是定义一些 protoc 生成的规则和插件配置。


# 配置protoc生成规则version: v1
managed: enabled: false
plugins: # generate go struct code - name: go out: gen/api/go opt: paths=source_relative
# generate grpc service code - name: go-grpc out: gen/api/go opt: - paths=source_relative
# generate rest service code - name: go-http out: gen/api/go opt: - paths=source_relative
# generate kratos errors code - name: go-errors out: gen/api/go opt: - paths=source_relative
# generate message validator code - name: validate out: gen/api/go opt: - paths=source_relative - lang=go
复制代码

buf.yaml

它放置的路径,你可以视之为protoc--proto-path参数指向的路径,也就是 proto 文件里面import的相对路径。


需要注意的是,buf.work.yaml的同级目录必须要放一个该配置文件。


该配置文件的内容通常来说都是下面这个配置,不需要做任何修改,需要修改的情况不多。


version: v1
deps: - 'buf.build/googleapis/googleapis' - 'buf.build/envoyproxy/protoc-gen-validate' - 'buf.build/kratos/apis' - 'buf.build/gnostic/gnostic' - 'buf.build/gogo/protobuf'
breaking: use: - FILE
lint: use: - DEFAULT
复制代码

buf 的 IDE 插件安装

在 IDE 里面(VSC 和 Goland),远程的 proto 源码库会被拉取到本地的缓存文件夹里面,而这 IDE 并不知道,故而无法解析到依赖到的 proto 文件,但是,Buf 官方提供了插件,可以帮助 IDE 读取并解析 proto 文件,并且自带 Lint。


使用 Buf 生成代码

我有开源了一个 Kratos 的 CMS 项目kratos-blog,它是一个 MonoRepo 结构的项目,我们以它的项目结构来做讲解。


下面的目录树,是我化简后的目录树。


.├── buf.work.yaml├── buf.gen.yaml├── buf.yaml├── buf.lock├── api│   ├── admin│   │   └── service│   │       └── v1│   │           └── admin_errors.proto│   │           └── buf.openapi.gen.yaml│   │           └── i_user.proto│   └── buf.yaml
复制代码


大家可以看到,总共所需求的配置文件并不多。


buf.build使用buf generate命令进行构建,调用该命令必须在buf.work.yaml的同级目录下。执行了buf generate命令之后,将会在根目录下产生一个gen/api/go的文件夹,生成的代码都将被放在了这个目录下。


细心的你肯定早就发现了在api/admin/service/v1下面有一个buf.openapi.gen.yaml的配置文件,这是什么配置文件呢?我现在把该配置文件放出来:


# 配置protoc生成规则version: v1
managed: enabled: true optimize_for: SPEED
go_package_prefix: default: kratos-monolithic-demo/gen/api/go except: - 'buf.build/googleapis/googleapis' - 'buf.build/envoyproxy/protoc-gen-validate' - 'buf.build/kratos/apis' - 'buf.build/gnostic/gnostic' - 'buf.build/gogo/protobuf' - 'buf.build/tx7do/pagination'
plugins: # generate openapi v2 json doc# - name: openapiv2# out: ./app/admin/service/cmd/server/assets# opt:# - json_names_for_fields=true# - logtostderr=true
# generate openapi v3 yaml doc - name: openapi out: ./app/admin/service/cmd/server/assets opt: - naming=json # 命名约定。使用"proto"则直接从proto文件传递名称。默认为:json - depth=2 # 循环消息的递归深度,默认为:2 - default_response=false # 添加默认响应消息。如果为“true”,则自动为使用google.rpc.Status消息的操作添加默认响应。如果您使用envoy或grpc-gateway进行转码,则非常有用,因为它们使用此类型作为默认错误响应。默认为:true。 - enum_type=string # 枚举类型的序列化的类型。使用"string"则进行基于字符串的序列化。默认为:integer。 - output_mode=merged # 输出文件生成模式。默认情况下,只有一个openapi.yaml文件会生成在输出文件夹。使用“source_relative”则会为每一个'[inputfile].proto'文件单独生成一个“[inputfile].openapi.yaml”文件。默认为:merged。 - fq_schema_naming=false # Schema的命名是否加上包名,为true,则会加上包名,例如:system.service.v1.ListDictDetailResponse,否则为:ListDictDetailResponse。默认为:false。
复制代码


没错,它是为了生成OpenAPI v3文档。我之前尝试了放在根目录下的buf.gen.yaml,但是产生了错误,因为 OpenAPI v3 文档,它全局只能产生一个openapi.yaml文件。所以,没辙,只能单独对待了。


那么,怎么使用这个配置文件呢?还是使用buf generate命令,该命令还是需要在项目根目录下执行,但是得带--template参数去引入buf.openapi.gen.yaml这个配置文件:


buf generate --path api/admin/service/v1 --template api/admin/service/v1/buf.openapi.gen.yaml
复制代码


最终,在./app/admin/service/cmd/server/assets这个目录下面,将会生成出来一个文件名为openapi.yaml的文件。

与前端协同

API 并不是给后端自己把玩的玩物,还需要提供给前端调用的。


要与前端协同,无非就是为前端提供 API 文档。有两种途径可以达成这个目标:


  1. 提供 OpenAPI 文档;

  2. 通过 Protobuf 生成 TypeScript 或者 Javascript 代码。


方法 2 是我一开始使用的方法,我使用了pbts,它是ProtoBuf.js提供的一个 Protobuf 转 Typescript 的工具。它可以把 Schema 转换成 Typescript 代码。在初期,它的确给予了我一定的支撑。但是,它的缺陷很大,很多 Protobuf 的语法识别不了,很多内容都导出不了,比如:访问路径导出不了、gnostic/openapi的标签被识别为错误语法。总之,也就是一个聊胜于无的工具。可是,它还是无法成为真正有力的生产力工具。


后来,我仔细的研究了 OpenAPI。发现,它保存了最为完整的 API 信息。而且,OpenAPI 文档是前端最为熟悉的 API 文档。给前端使用的工具也相当之多。


我研究了很多的语言的很多 Web 框架,发现,大家都会将 Swagger UI 内嵌到项目里面,提供一个在线的文档。我体验了整个的开发流程之后,认可了这种方式提供 OpenAPI 文档:


首先,它能够保证提供的文档和在线跑的服务提供的 API 是一致的。


其次,一切都是全自动的,一切都由框架提供支持,不需要自己为此做任何支持性的工作。比如,生成文档,拷贝文档……


最后,在线的方式的好处是,前后端都可以利用 Swagger UI 来查看 API 文档,调试接口。OpenAPI 文档,也可以在线拿取到,如果前端不适应、不喜欢用 Swagger UI,那么他也可以导入到其他的工具里面去,比如:Apifox、PostMan……

怎样内嵌 Swagger UI

Kratos 官方本来是有一个swagger-api的项目的(现在已经被归档了),集成的是 OpenAPI v2 的 Swagger UI。这个项目呢,不好使,我在应用中,经常会读不出来 OpenAPI 的文档。还有就是 OpenAPI v2 不如 v3 功能强大。


因为没有支持,而我又需要跟前端进行沟通,所以我只好生成出 OpenAPI 文档之后,自行导入到 ApiFox 里面去使用,ApiFox 呢,挺好的,支持文件和在线两种方式导入,文档管理,接口测试的功能也都很强大。但是总是要去费神导出文档,这很让人抗拒——在开发的初期,接口变动是很高频的行为——难道就不能够全自动吗?程序只要一发布,接口就自动的跟随程序一起发布出去了。


对,说的就是集成 Swagger UI。


为了做到这件事,我需要做这么几件事情:


  1. 把 Buf 生成 OpenAPI 文档,编译运行程序写进 MakeFile 里面;

  2. 利用 golang 的Embedding Files特性,把openapi.yaml嵌入到服务程序里面;

  3. 集成 Swagger UI 到项目,并且读取内嵌的openapi.yaml文档。


那么,我们首先开始编写 Makefile:


# generate protobuf api go codeapi:    buf generate
# generate OpenAPI v3 docs.openapi: buf generate --path api/admin/service/v1 --template api/admin/service/v1/buf.openapi.gen.yaml buf generate --path api/front/service/v1 --template api/front/service/v1/buf.openapi.gen.yaml
# run applicationrun: api openapi @go run ./cmd/server -conf ./configs
复制代码


这样我们只需要运行make openapi就执行 OpenAPI 的生成了,调试运行的时候,输入make run命令就可以生成 OpenAPI 并运行程序。


Makefile 写好了,现在我们来到./app/admin/service/cmd/server/assets这个目录下面,我们在这个目录下面创建一个名为assets.go的代码文件:


package assets
import _ "embed"
//go:embed openapi.yamlvar OpenApiData []byte
复制代码


就这样,我们就把 openapi.yaml 内嵌进程序了。


最后,我们就需要来集成 Swagger UI 进来了。我为此封装了一个项目,要使用它,我们需要安装依赖库:


go get -u github.com/tx7do/kratos-swagger-ui
复制代码


在创建 REST 服务器的地方调用程序包里面的方法:


package server
import ( rest "github.com/go-kratos/kratos/v2/transport/http" swaggerUI "github.com/tx7do/kratos-swagger-ui"
"kratos-cms/app/admin/service/cmd/server/assets")
func NewRESTServer() *rest.Server { srv := CreateRestServer()
swaggerUI.RegisterSwaggerUIServerWithOption( srv, swaggerUI.WithTitle("Admin Service"), swaggerUI.WithMemoryData(assets.OpenApiData, "yaml"), )}
复制代码


自此我们就大功告成了!


假如 API 服务的端口是 8080,那么我们可以访问链接来访问 Swagger UI:


http://localhost:8080/docs/


同时,openapi.yaml 文件也可以在线访问到:


http://localhost:8080/docs/openapi.yaml

参考资料


发布于: 2023-01-21阅读数: 325
用户头像

喵个咪

关注

还未添加个人签名 2022-06-01 加入

还未添加个人简介

评论

发布
暂无评论
Kratos微服务框架API工程化指南_golang_喵个咪_InfoQ写作社区