Introduction to Java gRPC Interceptor

Introduction

Hi I am Chung from System Engineering Department. I have been working on a project using microservices architecture and gRPC for a year and more. When providing technical supports to the project, especially on gRPC in Java side, I have found that web resources/discussions or even official documents are giving insufficient information to developers. This article is going to share about gRPC interceptor, which works different from interceptors of gRPC written by other languages.

Overview

Interceptor class in Java gRPC works as interface for intercepting incoming/outgoing calls. Instead of just sending calls out and getting responses, interceptors help to add cross-cutting behavior to calls and channel, before and after calls are sent.

Java gRPC interceptor has two types: Client Interceptor & Server Interceptor. This article will use following steps to give a brief introduction on how to use interceptor class. This article will only cover unary to unary call. Streaming call has slight difference from that of unary call in part of sending messages.

  1. Create proper interceptor objects and make the client/server to recognize them
  2. Implement core functions
  3. Illustrate basic call flow

Let's start working on Client Interceptor.

Client Interceptor

Creating a ClientInterceptor

ClientInterceptor has only one method to be implemented: interceptCall()

public class MyClientInterceptorA implements ClientInterceptor {
  
  @Override
  public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
      MethodDescriptor<ReqT, RespT> methodDescriptor,
      CallOptions callOptions, Channel channel) {
        // Here goes your codes.
    return channel.newCall(methodDescriptor, callOptions);
  }
}

Then attach to channel like:

ArrayList<ClientInterceptor> clientInterceptors = new ArrayList<>();
clientInterceptors.add(new MyClientInterceptorA());
clientInterceptors.add(new MyClientInterceptorB());

ManagedChannel channel = ManagedChannelBuilder
    .forAddress(host, port)
    .intercept(clientInterceptors)
    .usePlaintext()
    .build();
Implementing Core Functions

Instead of channel.newCall(), you can return a new class called ForwardingClientCall (or a simplified one SimpleForwardingClientCall)

return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
        channel.newCall(methodDescriptor, callOptions)) {
      @Override
      public void start(Listener<RespT> listener, Metadata headers) {
        super.start(listener, headers);
      }
      @Override
      public void sendMessage(ReqT message) { /* blah */ }
      @Override
      public void request(int numMessages) { /* blah */ }
      @Override
      public void halfClose() { /* blah */ }
      @Override
      public void cancel(String message, Throwable cause) { /* blah */ }
};

Moreover we can define our Listener instead of default listener in start():

ClientCall.Listener<RespT> listener =
    new ForwardingClientCallListener<RespT>() {
      @Override
      protected Listener<RespT> delegate() {
        return responseListener; // from ForwardingClientCall
      }
      @Override
      public void onReady() {
        super.onReady();
      }
      @Override
      public void onHeaders(Metadata headers) { /* blah */ }
      @Override
      public void onMessage(RespT message) { /* blah */ }
      @Override
      public void onClose(Status status, Metadata trailers) { /* blah */ }
    };
super.start(listener, headers);

Instead of what these methods function, how call is going around these method is more important. If you are really interested on their real usage, you may visit the official Java Docs:

Illustrate Basic Call Flow

f:id:chung_kabucom:20210329194308p:plain

This is drafted omitting some other classes and remaining only the core classes. In case you need to do logging or calculating event time, you now know where should put them to.

Server Interceptor

Creating a ServerInterceptor

Like ClientInterceptor, ServerInterceptor has only one method to be implemented: interceptCall()

public class MyServerInterceptorA implements ServerInterceptor {
  
  @Override
  public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
      ServerCall<ReqT, RespT> call, Metadata headers,
      ServerCallHandler<Reqt, RespT> next) {
        // Here goes your codes.
    return next.startCall(call, headers);
  }
}

Then attach to serverBuilder like:

// serverBuilder setting
serverBuilder.addService(ServerInterceptors.intercept(serviceDefinition, interceptors));
Implementing Core Functions

Similar to that of ClientInterceptor, ServerInterceptor has its own ForwardingServerCall and ServerCallListener, but the new ForwardingServerCall is redefinition of ServerCall from the argument.

// Instead of returning next.startCall(), you can return a ForwardingServerCall()
ServerCall<ReqT, RespT> wrapperCall = 
    new ForwardingServerCall.SimpleForwardingServerCall<>(call) {
      @Override
      public void request(int numMessages) { /* blah */ }
      @Override
      public void sendHeaders(Metadata headers) { /* blah */ }
      @Override
      public void sendMessage(RespT message) { /* blah */ }
      @Override
      public void close(Status status, Metadata trailers) { /* blah */ }

return next.startCall(wrapperCall, headers);

Moreover we can define our Listener instead of default listener returning:

ServerCall.Listener<ReqT> listener = next.startCall(wrapperCall, headers);
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<>(listener) {
    @Override
    public void onMessage(ReqT message) { /* blah */ }
    @Override
    public void onHalfClose() { /* blah */ }
    @Override
    public void onCancel() { /* blah */ }
    @Override
    public void onComplete() { /* blah */ }
    @Override
    public void onReady() { /* blah */ }
};

Instead of what these methods function, how call is going around these method is more important. If you are really interested on their real usage, you may visit the official Java Docs:

Basic Call Flow

f:id:chung_kabucom:20210330202347p:plain

Conclusion

In this article, we saw how gRPC Java Interceptors work on client side and server side. By adding proper methods inside your interceptors, you can do cross-cutting actions before, between and after message is sent/received. Most popular function implemented in interceptors might be logging since it is basically thread-safe (except MDC).

I will discuss more about thread-safe issue and application on interceptors in next article.