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

Spring Boot中如何利用gRPC拦截器绑定/更新日志MDC

解决gRPC请求中MDC上下文传递的问题

你遇到的问题其实很典型:gRPC的拦截器线程和实际处理请求的工作线程是分离的,而MDC是线程绑定的,所以你在拦截器线程里设置的MDC,到了工作线程就丢失了。而且你原来的代码里,finally块会立即清空MDC,这就导致后续工作线程完全拿不到上下文了。

下面给你两种可行的解决方案,第一种直接针对拦截器场景,最容易落地:

方案一:包装ServerCall.Listener,在回调中传递MDC

gRPC的请求处理逻辑是通过ServerCall.Listener的各个回调方法(比如onMessage、onHalfClose)执行的,这些回调会在工作线程中触发。我们可以包装这个Listener,在每个回调执行前恢复MDC上下文,执行后再清理。

修改你的拦截器代码如下:

import io.grpc.ForwardingServerCallListener;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import org.slf4j.MDC;

import java.util.Map;
import java.util.UUID;

public class GrpcMdcInterceptor implements ServerInterceptor {
    private static final String MDC_DATA_KEY = "requestID";

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(final ServerCall<ReqT, RespT> call, 
                                                                 final Metadata headers, 
                                                                 final ServerCallHandler<ReqT, RespT> next) {
        // 生成唯一请求ID
        final String requestId = UUID.randomUUID().toString();
        final String mdcData = String.format("[requestID=%s]", requestId);
        
        // 保存当前拦截器线程的MDC快照(如果有其他上下文需要传递的话)
        final Map<String, String> mdcSnapshot = MDC.getCopyOfContextMap();
        
        // 获取原始的Listener并包装
        ServerCall.Listener<ReqT> originalListener = next.startCall(call, headers);
        
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<>(originalListener) {
            @Override
            public void onMessage(ReqT message) {
                // 恢复MDC上下文并设置请求ID
                restoreMdcContext();
                try {
                    super.onMessage(message);
                } finally {
                    MDC.clear();
                }
            }

            @Override
            public void onHalfClose() {
                restoreMdcContext();
                try {
                    super.onHalfClose();
                } finally {
                    MDC.clear();
                }
            }

            @Override
            public void onCancel() {
                restoreMdcContext();
                try {
                    super.onCancel();
                } finally {
                    MDC.clear();
                }
            }

            @Override
            public void onComplete() {
                restoreMdcContext();
                try {
                    super.onComplete();
                } finally {
                    MDC.clear();
                }
            }

            @Override
            public void onReady() {
                restoreMdcContext();
                try {
                    super.onReady();
                } finally {
                    MDC.clear();
                }
            }

            private void restoreMdcContext() {
                if (mdcSnapshot != null) {
                    MDC.setContextMap(mdcSnapshot);
                }
                MDC.put(MDC_DATA_KEY, mdcData);
            }
        };
    }
}

为什么这个方案有效?

  • 我们在拦截器线程中先保存MDC的快照,然后包装原始的Listener。
  • 当工作线程触发Listener的回调时,我们先恢复MDC上下文并设置请求ID,再执行实际的处理逻辑。
  • 每个回调执行完后清空MDC,避免线程复用导致上下文污染。

方案二:利用gRPC Context传递请求ID(更灵活的方式)

如果你的系统中有异步处理场景,或者希望在更多地方复用请求ID,可以用gRPC的Context来传递:

  1. 首先定义一个Context键:
import io.grpc.Context;

public class GrpcContextKeys {
    public static final Context.Key<String> REQUEST_ID = Context.key("requestId");
}
  1. 修改拦截器,把请求ID放入Context:
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(final ServerCall<ReqT, RespT> call, 
                                                             final Metadata headers, 
                                                             final ServerCallHandler<ReqT, RespT> next) {
    String requestId = UUID.randomUUID().toString();
    // 将请求ID放入gRPC Context
    Context context = Context.current().withValue(GrpcContextKeys.REQUEST_ID, requestId);
    // 用新的Context包装后续调用
    return Contexts.interceptCall(context, call, headers, next);
}
  1. 然后可以通过AOP切面或者日志过滤器,在日志输出前从Context中获取requestID并设置到MDC:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.slf4j.MDC;

@Aspect
public class GrpcMdcAspect {
    @Before("execution(* com.yourpackage.grpc.*.*(..))")
    public void setMdcBeforeGrpcMethod() {
        String requestId = GrpcContextKeys.REQUEST_ID.get();
        if (requestId != null) {
            MDC.put("requestID", String.format("[requestID=%s]", requestId));
        }
    }

    @After("execution(* com.yourpackage.grpc.*.*(..))")
    public void clearMdcAfterGrpcMethod() {
        MDC.remove("requestID");
    }
}

这种方式更适合复杂场景,比如异步处理时,gRPC的Context会自动传递到子线程(只要用Context.current()来创建子线程)。

额外注意事项

  • 如果你的gRPC方法是异步实现的(比如返回ListenableFuture),确保在异步任务中也正确传递MDC或者Context。
  • 不管用哪种方案,都要注意清理MDC,避免线程池中的线程复用导致上下文残留。

这样修改后,你的gRPC请求日志应该就能和REST请求一样,显示唯一的requestID了。

内容的提问来源于stack exchange,提问作者Jim M.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:13:51