You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring gRPC @ExceptionHandler未使用最新Context生成问题求助

解决@GrpcAdvice的ExceptionHandler无法获取拦截器中设置的Context问题

问题根源

你遇到的核心问题是:@ServerInterceptor中绑定的Context在服务方法执行(或异常抛出)后已被自动detach,而@GrpcAdvice的ExceptionHandler执行时,线程的Context已经回退到拦截器执行前的版本(即你说的版本5),自然拿不到拦截器中设置的用户元数据。

可行解决方案

方案1:将用户元数据存入ServerCall的Attributes

gRPC的ServerCall对象在整个调用生命周期内都可访问,包括ExceptionHandler阶段。你可以在拦截器中将元数据存入ServerCall的Attributes,再在异常处理器中取出:

  1. 定义元数据与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");
  1. 拦截器中存储元数据到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);
}
  1. 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能拿到正确的上下文:

  1. 定义上下文感知的异常类
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;
    }
}
  1. 拦截器中捕获异常并包装
@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());
                    }
                }
            };
        }
    });
}
  1. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 15:36:14