如何为带标签的字段提供自定义编组器以实现替代行为
如何为带标签的字段提供自定义编组器以实现替代行为
我刚好之前在gRPC项目里处理过几乎一样的需求——用bytes存SHA哈希、UUID这类数据,但又不想用默认的二进制编组行为,想要针对这些特定字段做自定义的序列化/反序列化处理。下面就结合Go语言(看你的proto里指定了go_package,默认按Go来举例)给你一步步讲怎么实现:
1. 先给Proto字段加自定义标记
首先得让我们的编组器能识别出哪些字段需要特殊处理,这就需要在proto里定义自定义选项,给目标字段打上专属标签:
syntax = "proto3"; package hashes; option go_package = "./hashes"; import "google/api/annotations.proto"; import "google/protobuf/descriptor.proto"; // 定义一个自定义的字段选项,用来标记字段的实际类型(比如sha1、sha256、uuid) // 注意自定义选项的标签号要选50000以上的,这是官方留出来的用户自定义区间,不会和内置选项冲突 extend google.protobuf.FieldOptions { string field_type = 50000; } // 示例消息体,给需要特殊处理的bytes字段加上自定义标签 message HashData { bytes sha1 = 1 [(field_type) = "sha1"]; bytes sha256 = 2 [(field_type) = "sha256"]; bytes uuid = 3 [(field_type) = "uuid"]; // 普通bytes字段,用默认编组行为 bytes raw_binary = 4; }
这样一来,每个需要特殊处理的字段都有了明确的标记,后面的编组器就能精准识别它们了。
2. 实现自定义编组/解组逻辑
gRPC默认的编组器会把bytes字段按原始二进制处理,我们要做的就是包装默认的编组逻辑,遍历消息字段、识别自定义标签,然后对目标字段做转换。
针对gRPC二进制协议的自定义编组
如果是想修改gRPC本身的二进制编组行为(比如把UUID的bytes转成标准字符串再序列化),可以写一个自定义的Marshal和Unmarshal函数:
package main import ( "encoding/hex" "fmt" "strings" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/descriptorpb" ) // CustomMarshal 自定义序列化函数,处理带标记的bytes字段 func CustomMarshal(msg proto.Message) ([]byte, error) { // 先复制原始消息,避免修改原对象的内容 msgClone := proto.Clone(msg) msgRef := msgClone.ProtoReflect() // 遍历消息的所有字段 msgRef.Range(func(fieldDesc protoreflect.FieldDescriptor, fieldVal protoreflect.Value) bool { // 只处理bytes类型的字段 if fieldDesc.Kind() != protoreflect.BytesKind { return true } // 读取字段的自定义标签 fieldOpts := fieldDesc.Options().(*descriptorpb.FieldOptions) fieldType, ok := ptypes.GetExtension(fieldOpts, E_FieldType).(string) if !ok || fieldType == "" { return true } // 根据标签类型做对应的转换 rawBytes := fieldVal.Bytes() var processedBytes []byte switch fieldType { case "sha1", "sha256": // 把哈希的二进制转成十六进制字符串的字节流 processedBytes = []byte(hex.EncodeToString(rawBytes)) case "uuid": // 把16字节的UUID二进制转成标准的带连字符的字符串字节流 if len(rawBytes) != 16 { fmt.Printf("Warning: UUID field %s has invalid length\n", fieldDesc.Name()) return true } uuidStr := fmt.Sprintf("%x-%x-%x-%x-%x", rawBytes[0:4], rawBytes[4:6], rawBytes[6:8], rawBytes[8:10], rawBytes[10:]) processedBytes = []byte(uuidStr) } // 把转换后的值设置回字段 msgRef.Set(fieldDesc, protoreflect.ValueOfBytes(processedBytes)) return true }) // 用标准的proto序列化处理后的消息 return proto.Marshal(msgClone) } // CustomUnmarshal 自定义反序列化函数,把特殊处理的字节流转回去 func CustomUnmarshal(data []byte, msg proto.Message) error { // 先把数据反序列化到临时对象 tempMsg := proto.Clone(msg) if err := proto.Unmarshal(data, tempMsg); err != nil { return fmt.Errorf("failed to unmarshal raw data: %w", err) } tempRef := tempMsg.ProtoReflect() tempRef.Range(func(fieldDesc protoreflect.FieldDescriptor, fieldVal protoreflect.Value) bool { if fieldDesc.Kind() != protoreflect.BytesKind { return true } fieldOpts := fieldDesc.Options().(*descriptorpb.FieldOptions) fieldType, ok := ptypes.GetExtension(fieldOpts, E_FieldType).(string) if !ok || fieldType == "" { return true } // 把处理后的字节流转回原始二进制格式 processedBytes := fieldVal.Bytes() var rawBytes []byte var err error switch fieldType { case "sha1", "sha256": rawBytes, err = hex.DecodeString(string(processedBytes)) case "uuid": // 把带连字符的UUID字符串转成16字节的二进制 uuidStr := string(processedBytes) cleanStr := strings.ReplaceAll(uuidStr, "-", "") rawBytes, err = hex.DecodeString(cleanStr) } if err != nil { fmt.Printf("Failed to process field %s: %v\n", fieldDesc.Name(), err) return true } tempRef.Set(fieldDesc, protoreflect.ValueOfBytes(rawBytes)) return true }) // 把处理好的数据合并到目标消息里 proto.Merge(msg, tempMsg) return nil }
如果是给gRPC Gateway做JSON自定义编组
看你的proto里导入了google/api/annotations.proto,应该是用了gRPC Gateway,想要在HTTP接口的JSON响应里把这些bytes字段显示成可读的字符串(而不是默认的base64),那可以注册自定义的JSON编组器:
import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/protobuf/encoding/protojson" ) // NewCustomJSONMarshaler 创建自定义的JSON编组器 func NewCustomJSONMarshaler() runtime.Marshaler { return &runtime.JSONPb{ MarshalOptions: protojson.MarshalOptions{ UseProtoNames: true, // 用proto里的字段名而不是驼峰命名 // 自定义字段的序列化逻辑 Resolver: func(fieldDesc protoreflect.FieldDescriptor) protojson.ResolverFunc { // 只处理带自定义标签的bytes字段 fieldOpts := fieldDesc.Options().(*descriptorpb.FieldOptions) fieldType, ok := ptypes.GetExtension(fieldOpts, E_FieldType).(string) if !ok || fieldType == "" || fieldDesc.Kind() != protoreflect.BytesKind { return nil } return func(fieldVal protoreflect.Value) (interface{}, error) { rawBytes := fieldVal.Bytes() switch fieldType { case "sha1", "sha256": return hex.EncodeToString(rawBytes), nil case "uuid": if len(rawBytes) != 16 { return nil, fmt.Errorf("invalid UUID bytes length") } return fmt.Sprintf("%x-%x-%x-%x-%x", rawBytes[0:4], rawBytes[4:6], rawBytes[6:8], rawBytes[8:10], rawBytes[10:]), nil } return fieldVal.Bytes(), nil } }, }, UnmarshalOptions: protojson.UnmarshalOptions{ // 自定义字段的反序列化逻辑 Resolver: func(fieldDesc protoreflect.FieldDescriptor) protojson.ResolverFunc { fieldOpts := fieldDesc.Options().(*descriptorpb.FieldOptions) fieldType, ok := ptypes.GetExtension(fieldOpts, E_FieldType).(string) if !ok || fieldType == "" || fieldDesc.Kind() != protoreflect.BytesKind { return nil } return func(val interface{}) (protoreflect.Value, error) { // 把JSON里的字符串转成原始二进制 strVal, ok := val.(string) if !ok { return protoreflect.Value{}, fmt.Errorf("field %s expects string value", fieldDesc.Name()) } var rawBytes []byte var err error switch fieldType { case "sha1", "sha256": rawBytes, err = hex.DecodeString(strVal) case "uuid": cleanStr := strings.ReplaceAll(strVal, "-", "") rawBytes, err = hex.DecodeString(cleanStr) } if err != nil { return protoreflect.Value{}, fmt.Errorf("failed to parse %s: %w", fieldDesc.Name(), err) } return protoreflect.ValueOfBytes(rawBytes), nil } }, }, } }
然后在创建gRPC Gateway的路由时,把这个自定义编组器传进去就行:
// 示例:创建gRPC Gateway的mux mux := runtime.NewServeMux( runtime.WithMarshalerOption(runtime.MIMEWildcard, NewCustomJSONMarshaler()), )
一些要注意的小细节
- 自定义选项的标签号一定要选
50000以上的,这是官方规定的用户自定义区间,不会和内置选项冲突 - 错误处理逻辑要贴合你的业务场景,比如是跳过错误字段还是直接返回错误给调用方
- 如果是用其他语言(比如Java、Python),核心思路完全一致:先给字段加自定义标记,然后在自定义编组器里识别标记,做对应的类型转换
内容来源于stack exchange
相关产品推荐
相关产品推荐

