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.
- Create proper interceptor objects and make the client/server to recognize them
- Implement core functions
- 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:
- https://grpc.github.io/grpc-java/javadoc/io/grpc/ForwardingClientCall.html
- https://grpc.github.io/grpc-java/javadoc/io/grpc/ClientCall.Listener.html
Illustrate Basic Call Flow
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:
- https://grpc.github.io/grpc-java/javadoc/io/grpc/ForwardingServerCall.SimpleForwardingServerCall.html
- https://grpc.github.io/grpc-java/javadoc/io/grpc/ForwardingServerCallListener.html
Basic Call Flow
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.