GRPC with Gradle and Spring Boot - a practical guide

Oleg Iskovski
7 min readNov 22, 2023

There are enough explanations about what GRPC is, how it works, its performance compared to REST, and so on. Here I want to show a practical example of how to use GRPC in Java with Spring Boot and Gradle, possible communication ways, configurations, and exception handling. Let’s go.

What I used:

  • Java 17
  • Intellij IDEA 2022 Ultimate
  • Gradle 8.4
  • Spring Boot 2.5.4

We will create one project with three modules:

  • grpc-common
  • grpc- client
  • grpc-server
Project structure

Grpc-common will contain proto files, generated classes, and some common classes for both server and client implementations. For both the server and client, we will use the yidongnan gRPC Spring Boot Starter dependency.

Also, need to inform IDE about the generated code:

This is build.gradle of grpc-common. It will generate all Java code from the proto files in the src/main/proto folder to build the directory of the project:

generated folders with Java classes

Let’s create a protobuf file (Pay attention to data types and default values):

User service with a couple of methods and POJOs will be generated.

Now, let’s write a server-side.

grpc-server build.gradle

Our gRPC server will listen on port 9090:

application.yaml

As we are not talking about regular REST applications, you will not see a regular Rest Controller here - it’s role will be played by the UserService, reducing one communication layer with its help.

Now let’s add the appropriate dependencies to grpc-client:

grpc-client build.gradle

We are now ready to write client code. UserController will be used to test the client’s methods. Spring Boot web server runs by default on port 8080.

First, need some clarification on main concepts:

Channel - abstraction on connection. Since a client can have multiple connections to different servers (and use load-balancing), real TCP connections are presented by a sub-channel.

Stub - client with generated code behind for calling server methods. Stub can be synchronous, asynchronous and streaming.

So, first need to create connection and to initiate stub with this connection. It can be done in two ways - static and dynamic;

Static:

Statically define address in properties file
Inject with bean name defined in properties

This way configuration details can be defined also in the properties file.

Dynamic:

Configuration also created dynamically

Configuration documentation can be found in gRPC repo. This one is a more flexible approach, defined in code and created dynamically. You might have noticed, that communication is defined as ‘plaintext’. This is enough for test purposes, but for production you need to setup TLS to have messages encrypted.

To create a stub, need to use one of predefined methods and to pass as an argument previously created channel:

UserServiceGrpc is an auto-generated class from the protobuf file we defined.

We want to have a TraceID (or authorization token) propagated to the server from a client when a call is made. In dynamic configuration you might see a GrpcClientInterceptor that we configured while creating a channel. This one will enrich calls with required headers before each call is made. On the server side a GrpcServerInterceptor will be responsible for reading this header and processing it as required.

We’ve finished with the preparation part and let’s move on to UserController. We can see here a couple of methods using different call paradigms:

Unary synchronous call made with blocking stub. This call blocks a calling thread until a response comes from the server.

Unary asynchronous call made with future stub and returns a ListenableFuture. Here a calling thread is released and a future object must be used to obtain result.

Unidirectional server-side streaming made with stub (streaming one). Here client calls server once, but can get multiple responses as they are received from server. Same, but in reverse direction Unidirectional client-side streaming.

And, of course, a Bidirectional streaming, which is a union of the previous two.

Use streaming RPCs when handling a long-lived logical flow of data, to optimize the application, not gRPC.

Let’s start with ‘create user’ API:

As we can see here, we just call a method, generated from a protobuf file, on a created stub object. But, wait a minute… what if any exception happens while method executes? There is no try-catch defined, how will we process it?

Quite similar approach as we use in Spring REST communication - exception handler class annotated with @ControllerAdvice annotation that will intercept all defined exceptions. Actually, all errors that happen while gRPC calls will be of kind io.grpc.StatusRuntimeException. From this exception the code will be extracted and converted to a proper HttpStatus response. Google rpc API already contains some predefined codes.

Now let’s take a look at server side implementation. For this we need only to extend a UserServiceImplBase generated class and implement all defined methods.

UserService.crete() on server side

First, you can see a strange object StreamObserver at method signature. With help of this object we will return a value instead of the classic ‘return’ statement. That is what ‘onNext’ method at line 7 actually does - sends a message back. As for regular unary calls, ‘onCompleted()’ will tell ‌gRPC that call is finished in case of success, or ‘onError’ will return ‌error code (This is a first approach - just to return a code itself instead of throwing an exception).

On the other side, ‘getById’ method is a little bit different here:

UserService.getById() on server side

No explicitly returned code can be seen here. In case of entity ‌not found - an EntityNotFoundException will be thrown and ‘onCompleted’ will not be called. So, how will this exception be returned to the client, you may ask?

First of all we need to make some configuration on the server to forward exceptions:

And, like on client, define some AOP Advice to catch those exceptions and translate them to appropriate error Status code, as we mentioned before:

Next we will see how FutureStub is working. Look at ‘notifyUser’ method:

UserService.notifyUser() on client side

Context is connected to incoming request, and as client call finishes before server process, context will not be forwarded. For this to work, need to fork context, according to Eric’s Anderson answer on stackoverflow. As this call is asynchronous, we add a callback for processing its result. Pay attention, that we’re calling the same ‘getById’, but with a different stub here.

Time to move on to streaming. This as an example of server-side streaming:

UserService.getAll() on client side

As a first argument we pass an Empty object - in gRPC there is no possibility to pass no arguments and that’s why this predefined empty object exists. To wait until all results will come back and only after it returns a response to the client we will use CompletableFuture and pass it to stream observer.

Observer on client side

What exactly is going on here? On each message that comes from server, ‘onNext’ is called and we add an income User to list. When last user is sent and server will call ‘onComplete’, we will complete the CompletableFuture we mentioned previously and pass as an argument populated User list. This list will be returned to UserService. All this is on the client side. And what happens on the server side?

UserService.getAll() on server side

Here on each user we call ‘onNext’ and when all users are sent, we call ‘onComplete’. One call from client to server, and multiple calls from server to client, transported on the same stream of single HTTP/2 connection.

Next is client-side streaming:

UserService.delete() on client side

Same way using CompletableFuture. Iterating over user ids and on each call ‘onNext’ sending it to server. When finished - ‘onCompleted’. On server:

UserService.getAll() on server side

Receive StreamObserver and return StreamObserver.

Observer on server side

On each UserId sent by client, we remove User and add id to list. When client calls ‘onCompleted’, we send back id list and call ‘onComplete’ on server side.

And the last case - bidirectional streaming.

UserService.creteMultiple() on client side

Looks similar except method’s argument - also StreamObserver. Iterate over inputs, send one by one to server and wait for userIdList.

Observer on client side

Add each incoming id of the created user to the list. When all created - complete future.

Since we have covered main cases, want to show a concept of deadlines and canceling rpc calls. Deadline, actually, is kind of timeout - how long a client will wait until it cancels the call.

UserService.getByName() on client side

If call does not ends within 3 seconds, on client StatusRuntimeException will be thrown with DEADLINE_EXCEEDED code.

UserService.getByName() on server side

On the server side we define a callback in case a deadline is exceeded. But the code will continue to run.

Also we can explicitly cancel calls on the client side:

UserService.getByName() on client side

If we do it, on calling ‘future.get()’ we will have a CancellationException. For blocking stub and for streaming canceling call will look different.

UserService.getByName() on server side

On the server, same as for the deadline, we define a callback.

Despite the fact that there are many other configurations, subtleties and pitfalls exists, to start with gRPC it will be enough.

--

--