4. gRPC 拦截器(Interceptor)原理:客户端拦截器与服务端拦截器的工作机制,拦截器链
gRPC 拦截器(Interceptor)是框架提供的一种 AOP(面向切面编程) 机制,允许开发者在 RPC 调用链的关键节点插入自定义逻辑,而无需修改业务服务代码。拦截器是结构化日志、事务边界注入、错误处理与监控等横切关注点的核心载体。
4.1 拦截器的工作机制
gRPC 拦截器分为两大类:客户端拦截器(Client Interceptor)和 服务端拦截器(Server Interceptor)。它们分别在调用发起方和调用接收方拦截 gRPC 请求/响应的生命周期。
| 维度 | 客户端拦截器 | 服务端拦截器 |
|---|---|---|
| 拦截位置 | 在客户端发起调用前、收到响应后 | 在服务端收到请求后、返回响应前 |
| 典型用途 | 客户端日志、请求超时、重试、负载均衡元数据注入 | 认证鉴权、请求校验、服务端日志、事务边界、错误格式化 |
| 调用链方向 | 请求出站 → 响应入站 | 请求入站 → 响应出站 |
| 拦截粒度 | 一元调用(Unary)与流式调用(Streaming)分别处理 | 一元调用(Unary)与流式调用(Streaming)分别处理 |
4.2 拦截器链(Interceptor Chain)
gRPC 支持注册 多个拦截器,它们会按照注册顺序形成一条 拦截器链。每个拦截器通过调用 handler 或 invoker 将控制权传递给链中的下一个拦截器,最终到达真正的 RPC 处理器。
4.3 客户端拦截器原理
客户端拦截器通过实现 UnaryClientInterceptor 或 StreamClientInterceptor 接口来工作。以一元拦截器为例,其函数签名如下:
// Go 示例:客户端一元拦截器签名
type UnaryClientInterceptor func(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error
拦截器内部必须调用 invoker 才能将请求传递给下一个拦截器或真正的 gRPC 调用。在 invoker 前后可以插入自定义逻辑:
// Go 示例:客户端日志拦截器
func clientLoggingInterceptor(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
log.Printf("[Client] 请求方法: %s, 请求体: %v", method, req)
err := invoker(ctx, method, req, reply, cc, opts...) // 调用下一个拦截器或实际RPC
log.Printf("[Client] 响应: %v, 错误: %v", reply, err)
return err
}
4.4 服务端拦截器原理
服务端拦截器通过实现 UnaryServerInterceptor 或 StreamServerInterceptor 接口来工作。以一元拦截器为例:
// Go 示例:服务端一元拦截器签名
type UnaryServerInterceptor func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error)
服务端拦截器必须调用 handler 才能将请求传递给业务方法。在 handler 前后可以执行认证、日志、事务管理等操作:
// Go 示例:服务端事务拦截器
func transactionInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
tx := db.BeginTx(ctx, nil) // 开启事务
ctx = context.WithValue(ctx, "tx", tx) // 注入事务到上下文
resp, err := handler(ctx, req) // 执行业务方法
if err != nil {
tx.Rollback() // 业务失败,回滚事务
return nil, err
}
tx.Commit() // 业务成功,提交事务
return resp, nil
}
4.5 拦截器链的洋葱模型图示
下图展示了客户端与服务端拦截器链的完整调用流程,清晰呈现洋葱模型的请求/响应穿越路径:
4.6 拦截器注册与链式组合
在 gRPC 中,拦截器通过 grpc.UnaryClientInterceptor / grpc.StreamClientInterceptor 和 grpc.UnaryServerInterceptor / grpc.StreamServerInterceptor 选项注册。多个拦截器按注册顺序组合成链。
// Go 示例:服务端注册多个拦截器
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
authInterceptor, // 最外层:认证
loggingInterceptor, // 中间层:日志
transactionInterceptor, // 最内层:事务(靠近业务)
),
grpc.ChainStreamInterceptor(
authStreamInterceptor,
loggingStreamInterceptor,
),
)
// Go 示例:客户端注册多个拦截器
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(
grpc_retry.UnaryClientInterceptor(), // 重试拦截器
clientLoggingInterceptor, // 日志拦截器
),
)
grpc.WithUnaryInterceptor 只能注册 一个 拦截器。如果需要注册多个,必须使用 grpc.ChainUnaryInterceptor(自 gRPC-Go v1.28.0 起支持)。grpc.ChainUnaryInterceptor 会自动将多个拦截器组合成链,按注册顺序从外到内执行。
4.7 拦截器链的执行顺序对比
| 阶段 | 客户端拦截器链 | 服务端拦截器链 |
|---|---|---|
| 请求阶段(入站/出站) | 拦截器 1 → 拦截器 2 → 拦截器 3 → 实际调用 | 拦截器 1 → 拦截器 2 → 拦截器 3 → 业务 Handler |
| 响应阶段(出站/入站) | 实际调用 → 拦截器 3 → 拦截器 2 → 拦截器 1 | 业务 Handler → 拦截器 3 → 拦截器 2 → 拦截器 1 |
| 错误传播 | 错误从实际调用逐层向上返回,每层可处理或包装 | 错误从业务 Handler 逐层向上返回,每层可处理或包装 |
4.8 流式拦截器的特殊处理
流式 RPC 的拦截器与一元拦截器不同,它拦截的是 流对象(ClientStream / ServerStream)的创建过程,而非单个消息。流式拦截器可以包装流对象,在流操作(Send、Recv)前后插入逻辑。
// Go 示例:服务端流式拦截器包装
func streamLoggingInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// 包装 ServerStream,在 Recv/Send 时记录日志
wrappedStream := &loggingServerStream{ServerStream: ss}
return handler(srv, wrappedStream)
}
type loggingServerStream struct {
grpc.ServerStream
}
func (l *loggingServerStream) RecvMsg(m interface{}) error {
err := l.ServerStream.RecvMsg(m)
log.Printf("[Stream] 收到消息: %v, 错误: %v", m, err)
return err
}
func (l *loggingServerStream) SendMsg(m interface{}) error {
log.Printf("[Stream] 发送消息: %v", m)
return l.ServerStream.SendMsg(m)
}