自定义gRPC handler处理钩子
业务中需要对各个接口定义统一的返回结构,例如
{
"code":200,
"message":"error message",
"data":"response"
}
官方推荐的方法为在proto文件中每个response,都按照标准格式来定义。但这种方法会增加开发成本。本文介绍一种通过定义统一的server handler,将原有response包装为标准格式的方法,以及其中的问题。
对于正常返回,gRPC会直接输出proto文件定义好的response,如下:
{"message":"test"}
对于错误返回,gRPC会包装一层error、code与message,如下:
{"error":"error returned","code":2,"message":"error returned"}
但这种异构的返回结构不利于接口调用方进行统一的处理。
开始尝试使用gRPC拦截器实现此功能,但这种方法需要在拦截器中使用proto中定义好的标准response对应的message类型,接收经过jsonpb序列化后的字节流,且涉及到go的interface类型到proto any类型的处理,增加了复杂度,并且在proto定义中也需要感知到标准response的具体格式,处理方式并不优雅。
gRPC runtime包提供了 WithForwardResponseOption
方法对每次response进行重定向处理。可以在 WithForwardResponseOption
中使用自定义的返回结构,将原有response内容添加到data中,如下:
// CustomizedResponse customized response
type CustomizedResponse struct {
Code int `json:"code"`
// Data returns response Data when status is ok
Data interface{} `json:"data"`
// Error returns Error message when status is not ok
Error string `json:"error"`
}
// HTTPSuccHandler mux server http handler
func HTTPSuccHandler(ctx context.Context, w http.ResponseWriter, m proto.Message) error {
resp := CustomizedResponse{
Code: 200,
Data: m,
Error: "",
}
bs, err := json.Marshal(&resp)
if err != nil {
return err
}
w.Write(bs)
return nil
}
使用 WithForwardResponseOption
处理response,会存在两个问题:
-
只能包装正常返回,对于错误返回无法处理:
{"error":"error returned","code":2,"message":"error returned"}
-
正常情况下,会在包装后的响应体后追加包装前的message内容,如下:
{"code":200,"data":{"message":"test"},"error":""}{"message":"test"}
gRPC gateway也提供了 WithProtoErrorHandler
来定制化错误返回。为了解决 WithForwardResponseOption
带来的问题,使用 WithForwardResponseOption
来拦截正确返回,然后将其包装为错误返回,这就将正常返回与错误返回都重定向到同一个处理函数中,进行统一处理。
-
WithForwardResponseOption
部分:// HTTPSuccHandler mux server http handler func HTTPSuccHandler(ctx context.Context, w http.ResponseWriter, m proto.Message) error { resp := CustomizedResponse{ Code: 200, Data: m, Error: "", } bs, err := json.Marshal(&resp) if err != nil { return err } return errors.New(successFlag + string(bs)) }
-
WithProtoErrorHandler
部分:// HTTPErrorHandler mux server http handler func HTTPErrorHandler( ctx context.Context, mux *runtime.ServeMux, m runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error, ) { // success error raw := err.Error() if strings.HasPrefix(raw, successFlag) { raw = raw[len(successFlag):] _, _ = w.Write([]byte(raw)) return } // normal error s, ok := status.FromError(err) if !ok { s = status.New(codes.Unknown, err.Error()) } resp := CustomizedResponse{ Code: int(s.Code()), Data: nil, Error: s.Message(), } bs, _ := json.Marshal(&resp) _, _ = w.Write(bs) }
正常接口返回:
{"code":200,"data":{"message":"test"},"error":""}
错误接口返回:
{"code":2,"data":null,"error":"error returned"}
通过在 WithForwardResponseOption
函数返回的正常结果前面添加特定标识符,然后将结果整体转发给 WithProtoErrorHandler
处理。这种处理方式也可避免正常情况下追加response的问题。
在 WithProtoErrorHandler
函数中对每一个进来的返回查看是否具备正常结果的特定标识符,来进行不同处理。
如果在proto文件中定义message时未指定额外的json tag,默认生成的pb文件中结构体字段所带的json tag为 omitempty
。就会导致正常情况下返回合理零值时,response也会将对应属性忽略。如下:
{"code":200,"data":{},"error":""}
可通过两种方式解决此问题:
-
通过
protoc-go-inject-tag
等外部插件,在定义proto时指定tag。 这种方式需要对每个待设置的属性都进行修改,开发成本较高。 -
在
WithForwardResponseOption
函数中进行统一处理。业务中只需要在正常响应中关注response,所以可以在
WithForwardResponseOption
中统一处理,如下:// HTTPSuccHandler mux server http handler func HTTPSuccHandler(ctx context.Context, w http.ResponseWriter, m proto.Message) error { mj := jsonpb.Marshaler{EmitDefaults: true} mjs, err := mj.MarshalToString(m) if err != nil { return err } resp := CustomizedResponse{ Code: 200, Error: "", } if err = json.Unmarshal([]byte(mjs), &resp.Data); err != nil { return err } bs, err := json.Marshal(&resp) if err != nil { return err } return errors.New(successFlag + string(bs)) }
返回零值响应如下:
{"code":200,"data":{"message":""},"error":""}
响应处理函数与正常流程相比,多了序列化、中间转发等额外操作,所以需要对其性能进行测试。
-
在proto中定义标准response性能
goos: darwin goarch: amd64 pkg: github.com/brickzzhang/grpc-helloworld/client cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkSayHello-12 403 2692788 ns/op PASS ok github.com/brickzzhang/grpc-helloworld/client 2.115s
-
通过handler构造统一返回性能
goos: darwin goarch: amd64 pkg: github.com/brickzzhang/grpc-helloworld/client cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkSayHello-12 494 2311517 ns/op PASS ok github.com/brickzzhang/grpc-helloworld/client 1.727s
通过benchmark结果可知,在添加统一处理函数后,本地通过http调用接口,平均每次调用时长比不加长约0.3ms。
文章中所有代码参见gRPC-Helloworld