聊一聊 gRPC 中的拦截器

2,921 阅读5分钟

今天我们继续 gRPC 系列。

前面松哥跟大家聊了 gRPC 的简单案例,也说了四种不同的通信模式,感兴趣的小伙伴可以戳这里:

  1. 一个简单的案例入门 gRPC
  2. 聊一聊 gRPC 的四种通信模式

今天我们来继续聊一聊 gRPC 中的拦截器。

有请求的发送、处理,当然就会有拦截器的需求,例如在服务端通过拦截器统一进行请求认证等操作,这些就需要拦截器来完成,今天松哥先和小伙伴们来聊一聊 gRPC 中拦截器的基本用法,后面我再整一篇文章和小伙伴们做一个基于拦截器实现的 JWT 认证的 gRPC。

gRPC 中的拦截器整体上来说可以分为两大类:

  1. 服务端拦截器
  2. 客户端拦截器

我们分别来看。

1. 服务端拦截器

服务端拦截器的作用有点像我们 Java 中的 Filter,服务端拦截器又可以继续细分为一元拦截器流拦截器

一元拦截器对应我们上篇文章中所讲的一元 RPC,也就是一次请求,一次响应这种情况。

流拦截器则对应我们上篇文章中所讲的服务端流 RPC、客户端流 RPC 以及双向流 RPC。

不过,在 Java 代码中,无论是一元拦截器还是流拦截器,代码其实都是一样的。不过如果你是用 Go 实现的 gRPC,那么这块是不一样的。

所以接下来的内容我就不去区分一元拦截器和流拦截器了,我们直接来看一个服务端拦截器的例子。

这里我就不从头开始写了,我们直接在上篇文章的基础之上继续添加拦截器即可。

服务端拦截器工作位置大致如下:

从这张图中小伙伴们可以看到,我们可以在服务端处理请求之前将请求拦截下来,统一进行权限校验等操作,也可以在服务端将请求处理完毕之后,准备响应的时候将响应拦截下来,可以对响应进行二次处理。

首先我们来看请求拦截器,实际上是一个监听器:

public class BookServiceCallListener<R> extends ForwardingServerCallListener<R> {
    private final ServerCall.Listener<R> delegate;

    public BookServiceCallListener(ServerCall.Listener<R> delegate) {
        this.delegate = delegate;
    }

    @Override
    protected ServerCall.Listener<R> delegate() {
        return delegate;
    }

    @Override
    public void onMessage(R message) {
        System.out.println("这是客户端发来的消息,可以在这里进行预处理:"+message);
        super.onMessage(message);
    }
}

这里我们自定义一个类,继承自 ForwardingServerCallListener 类,在这里重写 onMessage 方法,当有请求到达的时候,就会经过这里的 onMessage 方法。如果我们需要对传入的参数进行验证等操作,就可以在这里完成。

再来看看响应拦截器:

public class BookServiceCall<ReqT,RespT> extends ForwardingServerCall.SimpleForwardingServerCall<ReqT,RespT> {
    protected BookServiceCall(ServerCall<ReqT, RespT> delegate) {
        super(delegate);
    }

    @Override
    protected ServerCall<ReqT, RespT> delegate() {
        return super.delegate();
    }

    @Override
    public MethodDescriptor<ReqT, RespT> getMethodDescriptor() {
        return super.getMethodDescriptor();
    }

    @Override
    public void sendMessage(RespT message) {
        System.out.println("这是服务端返回给客户端的消息:"+message);
        super.sendMessage(message);
    }
}

小伙伴们可能发现了,我这里用到了很多泛型,请求类型和响应类型都不建议指定具体类型,因为拦截器可能会拦截多种类型的请求,请求参数和响应的数据类型都不一定一样。

这里是重写 sendMessage 方法,在这个方法中我们可以对服务端准备返回给客户端的消息进行预处理。

所以这个位置就相当于响应拦截器

最后,我们需要在启动服务的时候,将这两个拦截器配置进去,代码如下:

public void start() throws IOException {
    int port = 50051;
    server = ServerBuilder.forPort(port)
            .addService(ServerInterceptors.intercept(new BookServiceImpl(), new ServerInterceptor() {
                @Override
                public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
                    String fullMethodName = call.getMethodDescriptor().getFullMethodName();
                    System.out.println(fullMethodName + ":pre");
                    Set<String> keys = headers.keys();
                    for (String key : keys) {
                        System.out.println(key + ">>>" + headers.get(Metadata.Key.of(key, ASCII_STRING_MARSHALLER)));
                    }
                    return new BookServiceCallListener<>(next.startCall(new BookServiceCall(call), headers));
                }
            }))
            .build()
            .start();
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        BookServiceServer.this.stop();
    }));
}

这是我之前服务启动的方法,以前我们调用 addService 方法的时候,直接添加对应的服务就可以了,现在,我们除了添加之前的 BookServiceImpl 服务之外,还额外给了一个拦截器。

每当请求到达的时候,就会经过拦截器的 interceptCall 方法,这个方法有三个参数:

  • 第一个参数 call 是消费传入的 RPC 消息的一个回调。
  • 第二个参数 headers 则是请求的消息头,如果我们通过 JWT 进行请求校验,那么就从这个 headers 中提取出请求的 JWT 令牌然后进行校验。
  • 第三个参数 next 就类似于我们在 Java 过滤器 filter 中的 filterChain 一样,让这个请求继续向下走。

在这个方法中,我们请求头的信息都打印出来给小伙伴们参考了。然后在返回值中,将我们刚刚写的请求拦截器和响应拦截器构建并返回。

好啦,这样我们的服务端拦截器就搞好啦~无论是一元的 RPC 消息还是流式的 RPC 消息,都会经过这个拦截器,响应也是一样。

2. 客户端拦截器

客户端拦截器就比较简单了,客户端拦截器可以将我们的请求拦截下来,例如我们如果想为所有请求添加统一的令牌 Token,那么就可以在这里来做,方式如下:

ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
        .usePlaintext()
        .intercept(new ClientInterceptor() {
            @Override
            public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
                System.out.println("!!!!!!!!!!!!!!!!");
                callOptions = callOptions.withAuthority("javaboy");
                return next.newCall(method,callOptions);
            }
        })
        .build();
BookServiceGrpc.BookServiceStub stub = BookServiceGrpc.newStub(channel);

当我们的请求执行的时候,这个客户端拦截器就会被触发。

3. 小结

好啦,今天就和小伙伴们简单介绍一下服务端拦截器和客户端拦截器。下篇文章,松哥会通过一个 JWT 认证来和小伙伴们演示这个拦截器的具体用法。