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 处理器。

关键设计原则: 拦截器链采用 洋葱模型(Onion Model)。客户端拦截器按注册顺序从外到内执行请求拦截,响应则反向返回;服务端拦截器同样按注册顺序从外到内执行请求拦截,响应反向返回。这种模型天然支持在请求前后分别执行逻辑(如开启事务 → 执行业务 → 提交/回滚事务)。
实践建议: 在拦截器链中,日志拦截器 应放在最外层(最先注册),以便捕获最完整的请求/响应信息;事务拦截器 应放在内层(靠近业务处理器),确保事务边界精确包裹业务逻辑。

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 拦截器链的洋葱模型图示

下图展示了客户端与服务端拦截器链的完整调用流程,清晰呈现洋葱模型的请求/响应穿越路径:

gRPC 拦截器链 — 洋葱模型 客户端拦截器链 客户端拦截器 1(日志) 客户端拦截器 2(元数据注入) 客户端拦截器 3(重试) 实际 RPC 调用 网络传输 服务端拦截器链 服务端拦截器 1(认证) 服务端拦截器 2(事务) 服务端拦截器 3(日志) 业务 Handler 请求 → → 请求 响应 ← ← 响应

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-Go 中,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)
}
核心要点: 流式拦截器通过 包装流对象 实现对流式消息的拦截,而不是像一元拦截器那样直接拦截单个请求/响应。这种设计使得流式拦截器可以精细控制每个消息的发送和接收行为,非常适合在流式场景中注入日志、监控或事务上下文。