中 进阶
gRPC拦截器与错误处理#
一句话答案#
gRPC 拦截器分客户端/服务端两侧,类似 Spring 的 AOP 切面,用于统一处理日志、认证、限流、链路追踪等横切关注点;错误处理通过 Status Code + Metadata 传递结构化错误信息。
核心要点
拦截器分类#
客户端拦截器(ClientInterceptor):
请求前 → 添加 Token/TraceId/Metadata → 发送 → 响应后 → 记录耗时/错误
服务端拦截器(ServerInterceptor):
收到请求 → 鉴权/限流/日志 → 转发给 Service 实现 → 返回前 → 后处理plaintext服务端拦截器示例#
public class AuthInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String token = headers.get(Metadata.Key.of("authorization", ASCII_STRING_MARSHALLER));
if (!validateToken(token)) {
call.close(Status.UNAUTHENTICATED.withDescription("Invalid token"), new Metadata());
return new ServerCall.Listener<>() {}; // 空实现,不再处理
}
return next.startCall(call, headers); // 放行
}
}
// 注册
Server server = ServerBuilder.forPort(9090)
.addService(ServerInterceptors.intercept(orderService, new AuthInterceptor()))
.build();java客户端拦截器示例#
public class TracingInterceptor implements ClientInterceptor {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions,
Channel next) {
return new ForwardingClientCall.SimpleForwardingClientCall<>(
next.newCall(method, callOptions)) {
@Override
public void start(Listener<RespT> listener, Metadata headers) {
headers.put(TRACE_ID_KEY, TraceContext.getCurrentTraceId());
super.start(listener, headers);
}
};
}
}javagRPC Status Code#
| Code | 含义 | 对应场景 |
|---|---|---|
| OK (0) | 成功 | - |
| CANCELLED (1) | 调用被取消(通常由调用方发起) | 客户端主动取消 / 上游请求已取消 |
| INVALID_ARGUMENT (3) | 参数错误 | 校验失败 |
| NOT_FOUND (5) | 资源不存在 | 按 ID 查询不到(列表查询为空应返回 OK + 空列表) |
| PERMISSION_DENIED (7) | 无权限 | RBAC 拒绝 |
| UNAUTHENTICATED (16) | 未认证 | Token 无效/过期 |
| RESOURCE_EXHAUSTED (8) | 资源耗尽 | 限流 |
| INTERNAL (13) | 内部错误 | 未知异常 |
| UNAVAILABLE (14) | 服务不可用 | 熔断/宕机 |
| DEADLINE_EXCEEDED (4) | 超时 | deadline 到期 |
结构化错误传递#
// 服务端返回详细错误
Status status = Status.INVALID_ARGUMENT
.withDescription("order_id must be positive");
Metadata trailers = new Metadata();
trailers.put(ERROR_DETAIL_KEY, "{\"field\":\"order_id\",\"reason\":\"negative\"}");
responseObserver.onError(status.asRuntimeException(trailers));
// 客户端解析
try {
stub.getOrder(request);
} catch (StatusRuntimeException e) {
Status status = e.getStatus();
Metadata trailers = e.getTrailers();
// 根据 status.getCode() 做不同处理
}java拦截器链执行顺序#
grpc-java:ServerInterceptors.intercept(service, i1, i2) / ClientInterceptors.intercept(channel, i1, i2)
→ 列表中最后一个(i2)最先执行:i2 → i1 → Service 实现 / 发送请求
→ 想按列表顺序执行用 ServerInterceptors.interceptForward / ClientInterceptors.interceptForward
grpc-go:ChainUnaryInterceptor(i1, i2) → i1 在最外层,按列表顺序执行plaintext面试回答(2分钟版)
gRPC 拦截器分客户端和服务端两侧,类似 Spring AOP 的切面机制。服务端 ServerInterceptor 拦截请求做鉴权、限流、日志;客户端 ClientInterceptor 在请求前注入 TraceId、Token 等 Metadata,响应后记录耗时。多个拦截器形成链(注意 grpc-java 的 intercept() 是列表里最后一个最先执行)。错误处理方面,gRPC 定义了 17 种 Status Code(0–16),比如 UNAUTHENTICATED 未认证、RESOURCE_EXHAUSTED 限流、DEADLINE_EXCEEDED 超时。服务端通过 Status + Metadata 返回结构化错误,客户端 catch StatusRuntimeException 按 Code 分类处理。实践中常用拦截器实现:JWT 鉴权拦截器、Sentinel 限流拦截器、OpenTelemetry 链路追踪拦截器。和 Spring 拦截器的区别是 gRPC 拦截器工作在 RPC 层而非 HTTP 层,粒度更细(可以拦截 Streaming 的每条消息)。
追问与易错
追问方向:
- “拦截器和 Spring AOP 什么关系?”→ 独立的,gRPC 拦截器在 RPC 层,Spring AOP 在 Bean 方法层
- “怎么做全局异常处理?”→ 服务端拦截器 catch 所有异常,统一转为 gRPC Status 返回
- “Deadline 怎么传播?”→ 客户端设置 deadline,以
grpc-timeout请求头(剩余时长)发给服务端;服务端把它设为当前 Context 的 deadline,在同一 Context 里再调下游时自动带上剩余时间(Java 靠 io.grpc.Context,Go 靠 ctx),每一跳检查剩余时间,到期返回 DEADLINE_EXCEEDED
易错点:
- ❌ “gRPC 用 HTTP 状态码”——用自己的 17 种 Status Code(0–16),放在 grpc-status trailer 里;正常响应的 HTTP 状态码都是 200
- ❌ “拦截器只能改 Metadata,不能碰消息体”——可以:Java 里包装 ServerCall.Listener#onMessage / ClientCall#sendMessage,Go 的 unary 拦截器直接拿到 req,都能读取或替换消息(流式场景逐条拦截)
- ❌ “UNAVAILABLE 就是服务挂了”——官方定义是”大概率是暂时性的,可退避重试”:连接失败、服务端正在关闭、负载均衡无可用节点、代理返回 HTTP 429/502/503 都会映射成它;主动限流按规范应返回 RESOURCE_EXHAUSTED