Spring gRPC @ExceptionHandler未使用最新Context生成问题求助
解决@GrpcAdvice的ExceptionHandler无法获取拦截器中设置的Context问题
问题根源
你遇到的核心问题是:@ServerInterceptor中绑定的Context在服务方法执行(或异常抛出)后已被自动detach,而@GrpcAdvice的ExceptionHandler执行时,线程的Context已经回退到拦截器执行前的版本(即你说的版本5),自然拿不到拦截器中设置的用户元数据。
可行解决方案
方案1:将用户元数据存入ServerCall的Attributes
gRPC的ServerCall对象在整个调用生命周期内都可访问,包括ExceptionHandler阶段。你可以在拦截器中将元数据存入ServerCall的Attributes,再在异常处理器中取出:
- 定义元数据与Attribute的Key
// 定义请求头中的元数据Key private static final Metadata.Key<String> USER_ID_KEY = Metadata.Key.of("user-id", Metadata.ASCII_STRING_MARSHALLER); // 定义ServerCall Attributes的Key private static final Attributes.Key<String> USER_ATTRIBUTE_KEY = Attributes.Key.create("user-id");
- 拦截器中存储元数据到Attributes
@Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { String userId = headers.get(USER_ID_KEY); if (userId != null) { // 包装ServerCall,将用户ID存入Attributes call = new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) { @Override public Attributes getAttributes() { return super.getAttributes().toBuilder().set(USER_ATTRIBUTE_KEY, userId).build(); } }; // 同时设置Context供服务方法使用 Context context = Context.current().withValue(USER_ID_KEY, userId).fork(); return Contexts.interceptCall(context, call, headers, next); } return next.startCall(call, headers); }
- ExceptionHandler中读取Attributes里的元数据
@GrpcAdvice public class GrpcGlobalExceptionHandler { @GrpcExceptionHandler public StatusRuntimeException handleAnyException(Throwable ex, GrpcExceptionContext exceptionContext) { // 从ServerCall的Attributes中获取用户ID String userId = exceptionContext.getServerCall().getAttributes().get(USER_ATTRIBUTE_KEY); // 结合元数据构造错误响应 Status status = Status.INTERNAL.withDescription(String.format("处理失败,用户ID:%s", userId)); return status.asRuntimeException(); } }
方案2:自定义带Context的异常包装类
通过将异常与当前Context绑定,确保ExceptionHandler能拿到正确的上下文:
- 定义上下文感知的异常类
public class ContextAwareRuntimeException extends RuntimeException { private final Context context; public ContextAwareRuntimeException(Throwable cause, Context context) { super(cause); this.context = context; } public Context getContext() { return context; } }
- 拦截器中捕获异常并包装
@Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { String userId = headers.get(USER_ID_KEY); Context context = Context.current().withValue(USER_ID_KEY, userId).fork(); // 用Context包裹调用链,捕获异常并绑定当前Context return Contexts.interceptCall(context, call, headers, new ServerCallHandler<ReqT, RespT>() { @Override public ServerCall.Listener<ReqT> startCall(ServerCall<ReqT, RespT> call, Metadata headers) { ServerCall.Listener<ReqT> delegate = next.startCall(call, headers); return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(delegate) { @Override public void onHalfClose() { try { super.onHalfClose(); } catch (RuntimeException ex) { // 绑定当前Context后重新抛出 throw new ContextAwareRuntimeException(ex, Context.current()); } } }; } }); }
- ExceptionHandler中处理包装后的异常
@GrpcAdvice public class GrpcGlobalExceptionHandler { @GrpcExceptionHandler(ContextAwareRuntimeException.class) public StatusRuntimeException handleContextAwareException(ContextAwareRuntimeException ex) { // 从包装类中获取正确的Context Context context = ex.getContext(); String userId = context.get(USER_ID_KEY); Status status = Status.INTERNAL.withDescription(String.format("用户%s请求处理失败", userId)); return status.asRuntimeException(); } // 兜底处理其他异常 @GrpcExceptionHandler public StatusRuntimeException handleOtherExceptions(Throwable ex) { return Status.INTERNAL.withDescription("未知错误").asRuntimeException(); } }
关键注意事项
- 不要在ExceptionHandler中直接调用
Context.current(),此时拦截器绑定的Context已经被detach,会回到调用前的旧版本。 - 方案1更简洁,适合仅传递元数据的场景;方案2更灵活,适合需要完整Context的复杂场景。
内容的提问来源于stack exchange,提问作者Jonas
相关产品推荐
相关产品推荐

