如何为GRPC Server Interceptor绑定注解并获取调用方法详情
在gRPC Server拦截器中基于注解实现方法级条件拦截
要实现拦截器仅对带有指定注解的gRPC方法执行处理,核心是关联gRPC方法与注解元数据,再在拦截器中校验匹配。下面提供两种可行方案,分别基于Java注解和Protobuf自定义选项:
方案一:基于Java注解实现
1. 定义自定义方法注解
先创建一个运行时可见的注解,标记需要拦截的方法:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface GrpcIntercept { // 可添加自定义属性,比如拦截器类型、权限标识等 }
2. 在业务服务接口上标记注解
在你自己定义的gRPC业务接口(而非自动生成的Stub接口)的目标方法上添加注解:
public interface MyBusinessService { @GrpcIntercept void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver); // 无需拦截的方法不标记注解 void sayHi(HiRequest request, StreamObserver<HiResponse> responseObserver); }
确保自动生成的gRPC服务实现类(如MyServiceGrpc.MyServiceImplBase的子类)实现了这个业务接口。
3. 预构建方法-注解映射缓存
在应用启动时扫描所有gRPC服务实例,收集带有@GrpcIntercept注解的方法,建立gRPC方法全限定名与注解的映射(避免拦截器中重复反射影响性能):
import io.grpc.BindableService; import io.grpc.MethodDescriptor; import io.grpc.ServiceDescriptor; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; @Component public class GrpcAnnotationCache { private final Map<String, GrpcIntercept> methodAnnotationMap = new HashMap<>(); public GrpcAnnotationCache(List<BindableService> grpcServices) { for (BindableService service : grpcServices) { Class<?> serviceClass = service.getClass(); // 遍历服务实现的业务接口(排除gRPC自动生成的Stub接口) for (Class<?> iface : serviceClass.getInterfaces()) { if (iface.getName().endsWith("BusinessService")) { for (Method method : iface.getDeclaredMethods()) { GrpcIntercept annotation = method.getAnnotation(GrpcIntercept.class); if (annotation != null) { try { // 获取gRPC服务描述符,找到对应方法的全限定名 ServiceDescriptor serviceDescriptor = (ServiceDescriptor) serviceClass.getMethod("getServiceDescriptor").invoke(service); // 注意:proto方法名首字母大写,Java方法名是驼峰,需转换匹配 String protoMethodName = capitalizeFirstLetter(method.getName()); MethodDescriptor<?, ?> methodDescriptor = serviceDescriptor.findMethodByName(protoMethodName); if (methodDescriptor != null) { methodAnnotationMap.put(methodDescriptor.getFullMethodName(), annotation); } } catch (Exception e) { throw new RuntimeException("初始化gRPC注解缓存失败", e); } } } } } } } private String capitalizeFirstLetter(String str) { if (str == null || str.isEmpty()) return str; return str.substring(0, 1).toUpperCase() + str.substring(1); } public GrpcIntercept getAnnotation(String fullMethodName) { return methodAnnotationMap.get(fullMethodName); } }
4. 在拦截器中校验注解并执行逻辑
注入缓存实例,在拦截器中获取当前调用的方法全限定名,匹配注解后执行拦截逻辑:
import io.grpc.*; public class AnnotationInterceptor implements ServerInterceptor { private final GrpcAnnotationCache annotationCache; public AnnotationInterceptor(GrpcAnnotationCache annotationCache) { this.annotationCache = annotationCache; } @Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { String fullMethodName = call.getMethodDescriptor().getFullMethodName(); GrpcIntercept annotation = annotationCache.getAnnotation(fullMethodName); if (annotation != null) { // 执行自定义拦截逻辑,比如日志、权限校验、参数校验等 System.out.println("执行拦截处理:" + fullMethodName); // 包装Listener处理请求/响应生命周期 return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(next.startCall(call, headers)) { @Override public void onMessage(ReqT message) { // 可在此处理请求消息,比如脱敏、校验 super.onMessage(message); } @Override public void onHalfClose() { // 请求发送完成后的处理 super.onHalfClose(); } }; } // 无注解,直接放行 return next.startCall(call, headers); } }
方案二:基于Protobuf自定义选项实现(更原生)
如果不想依赖Java反射,可直接在Protobuf定义中添加自定义选项,标记需要拦截的方法,这种方式更贴合gRPC原生机制,也支持跨语言场景。
1. 在proto文件中定义自定义选项
syntax = "proto3"; import "google/protobuf/descriptor.proto"; // 定义方法级自定义选项,编号需在50001-99999的自定义范围内 extend google.protobuf.MethodOptions { bool need_intercept = 50001; } service MyService { rpc SayHello(HelloRequest) returns (HelloResponse) { // 标记该方法需要拦截 option (need_intercept) = true; } rpc SayHi(HiRequest) returns (HiResponse); } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } message HiRequest { string name = 1; } message HiResponse { string message = 1; }
2. 在拦截器中读取自定义选项
直接从MethodDescriptor中获取自定义选项的值,无需反射:
import io.grpc.*; import your.package.MyProto; // 替换为你的proto生成类路径 public class OptionBasedInterceptor implements ServerInterceptor { @Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { MethodDescriptor<ReqT, RespT> methodDescriptor = call.getMethodDescriptor(); // 获取自定义选项的值 Boolean needIntercept = methodDescriptor.getOptions().getExtension(MyProto.need_intercept); if (needIntercept != null && needIntercept) { // 执行拦截逻辑 return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(next.startCall(call, headers)) { @Override public void onMessage(ReqT message) { // 自定义处理逻辑 super.onMessage(message); } }; } return next.startCall(call, headers); } }
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| Java注解方式 | 符合Java开发者习惯,无需修改proto | 依赖反射,需处理Java与proto方法名映射 |
| Protobuf自定义选项 | 原生支持,无反射,跨语言友好 | 需要修改proto文件,注解属性扩展受限 |
内容的提问来源于stack exchange,提问作者Narain
相关产品推荐
相关产品推荐

