Implementing gRPC service with Spring Boot

Programming Spike
10 min readNov 10, 2023

--

It has hit me like a wrecking ball that I have never had an encounter with implementing a server-client operation using gRPC. It means that I have been lying on my CV since 2022; not directly mind, but by saying that I have a substantial experience in writing high-performance Microservices. I don’t want to be that person anymore, so the time has come to discover this self-named “High performance, open source universal RPC framework”.

What is gRPC?

Remote procedure call (RPC) true to its name is an operation involving a client invoking a code running on a server. Such operation could be implemented within the boundaries of gRPC framework which in an essence implements a communication protocol over HTTP/2. The framework provides a specification for defining messages and services, serialisation mechanism, security mechanisms and various tools. gRPC recognises four RPC types:

  1. Unary RCP: Client sends one message and Server responds with one message (one to one).
  2. Server Streaming RPC: Client sends one message and Server responds with many messages (one to many).
  3. Client Streaming RPC: Client sends many message and Server responds with one message (many to one).
  4. Bidirectional Streaming RPC: Client sends many messages and Server response with many messages (many to many).

Protocol Buffers (Protobuf) is an open source solution employed by gRPC for serialising the messages. Protobuf provides its own language for defining the message and service structures. The structure definitions are stored in a .proto file. Protobuf compiler (Protoc) creates the service interface code and stubs in a chosen language from the Proto file.

Useful links

Introduction to gRPC: https://grpc.io/docs/what-is-grpc/introduction/

Core Concepts: https://grpc.io/docs/what-is-grpc/core-concepts/

gRPC Motivation and Design Principles: https://grpc.io/blog/principles/

When gRPC should be used?

Main reason for choosing gRPC is the requirement for inner service streaming of arbitrary data, where gRPC excels as it optimises for that. However, when designing the solution consider size of the messages and run the tests to check for the significant spikes in the memory usage; Protobuf could create copies of the serialised streams.

A programmer wanting to use gRPC for browser to server communication should consider the target browser support for HTTP/2; the protocol version was released in 2015 and the lack of support for HTTP/2 means gRPC can’t be used.

Latency gains over REST. The comparison I see is between REST with compressed JSON over HTTP/2 and gRPC with compressed Proto binary. Rest over HTTP/1.1 is most likely slower, because of the lack of multiplexing, header compression and the textual format of the transferred data; text is slower to process than binary data. REST over HTTP/2 comparison is not that clear to me and I would have to run tests to make a valuable opinion.

Useful links

HTTP/2 vs HTTP/1.1: https://www.cloudflare.com/en-gb/learning/performance/http2-vs-http1.1/

Benefits of Using Protocol Buffers: https://www.cloudflare.com/en-gb/learning/performance/http2-vs-http1.1/

Proto API

Every journey of implementing a web service begins with defining its API. This test gRPC-based web service is not going to be any different. I want to implement the Server Streaming RPC, where the client sends a request for the stream of server time readings and receives time readings in response messages.

syntax = "proto3";

import "google/protobuf/timestamp.proto";

option java_package = "com.pgse.grpc.generated";
option java_multiple_files = true;

service TimeReader {
// Server Streaming RPC
rpc readTime(TimeReaderRequest) returns (stream TimeReaderResponse) {}
}

message TimeReaderRequest {
// Streaming time duration in seconds
int32 streaming_time_duration = 1;
}

message TimeReaderResponse {
google.protobuf.Timestamp timestamp = 1;
}

The first statement in the Proto file must be the declaration of a version of a Protobuf language syntax you want to use; proto3 is the latest version at the time of writing. Next, I import the Protobuf Well-Known timestamp structure. Ensure that the imported structures are accessible on Protoc’s compilation path. I specify name of generated sources package and indicate that I want each generated structure in a separate Java file. I am not going to dive into the specifics of defining the service and message structures. Refer to the Proto3 language guide for that.

...
<build>
<plugins>
<plugin>
<groupId>com.github.os72</groupId>
<artifactId>protoc-jar-maven-plugin</artifactId>
<version>${com.github.os72.protoc-jar-maven-plugin.version}</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<includeMavenTypes>direct</includeMavenTypes>
<inputDirectories>
<include>src/main/resources</include>
</inputDirectories>
<outputTargets>
<outputTarget>
<type>java</type>
<addSources>none</addSources>
<cleanOutputFolder>true</cleanOutputFolder>
<outputDirectory>${project.basedir}/src/main/generated</outputDirectory>
</outputTarget>
<outputTarget>
<type>grpc-java</type>
<addSources>none</addSources>
<cleanOutputFolder>true</cleanOutputFolder>
<outputDirectory>${project.basedir}/src/main/generated</outputDirectory>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${io.grpc.protoc-gen-grpc-java.version}</pluginArtifact>
</outputTarget>
</outputTargets>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/main/generated</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
...

I want the Proto API to be a separate Maven module. In the pom, I add the protoc-jar-maven-plugin which generates the Java code from the Proto file. When you use Protobuf well-known types it is important to set the includeMavenTypes plugin configuration option to direct, so that the well-known types are included. The build-helper-maven-plugin adds the generated Java classes to a target jar from a /src/main/generated source directory.

Useful links

Proto3 Language Guide: https://protobuf.dev/programming-guides/proto3/

Protocol Buffers Well Known Types: https://protobuf.dev/reference/protobuf/google.protobuf/

Protoc Jar Maven Plugin: https://github.com/os72/protoc-jar-maven-plugin

Build Helper Maven Plugin: https://www.mojohaus.org/build-helper-maven-plugin/

The Proto API Implementation: https://github.com/acrolisnun/grpc-spring-boot

Spring Boot Server

There is almost always a Spring Boot starter and this time is no different. There is a grpc-server-spring-boot-starter which provides abstractions that make setting up the gRPC server and client quite straightforward. I add the starter and the generated Proto Java code to a Server Module pom.

...
<dependency>
<groupId>com.pgse.grpc</groupId>
<artifactId>grpc-proto</artifactId>
<version>${com.pgse.grpc-proto.version}</version>
</dependency>

<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>${net.devh.grpc-server-spring-boot-starter.version}</version>
</dependency>
...

I create a gRPC service class which must be annotated with a GrpcService annotation. The annotation is annotated with the Spring’s Service and Bean annotations. The service class extends the Proto generated service class TimeReaderGrpc.TimeReaderImplBase and overrides its dummy implementation of the readTime method. The method has two parameters — TimeReaderRequest and StreamObserver. TimeReaderRequest is the Proto generated request type carrying the request data as defined in the Proto file. StreamObserver receives and sends the stream messages; important to remember that it’s not thread safe and it’s asynchronous.

package com.pgse.grpc.server.service;

import com.google.protobuf.Timestamp;
import com.pgse.grpc.generated.TimeReaderGrpc;
import com.pgse.grpc.generated.TimeReaderResponse;
import io.grpc.stub.ServerCallStreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

@GrpcService
public class TimeReaderGrpcService extends TimeReaderGrpc.TimeReaderImplBase {

private final Timestamp.Builder timestampBuilder = Timestamp.newBuilder();
private final TimeReaderResponse.Builder timeReaderResponseBuilder = TimeReaderResponse.newBuilder();

@Override
public void readTime(com.pgse.grpc.generated.TimeReaderRequest request,
io.grpc.stub.StreamObserver<com.pgse.grpc.generated.TimeReaderResponse> responseObserver) {
ServerCallStreamObserver<TimeReaderResponse> serverCallStreamObserver
= (ServerCallStreamObserver<TimeReaderResponse>) responseObserver;
int streamingTimeDuration = request.getStreamingTimeDuration() * 1000;
long start = System.currentTimeMillis();
long now = start;

while(now - start <= streamingTimeDuration) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (serverCallStreamObserver.isReady()) {
serverCallStreamObserver.onNext(timeReaderResponseBuilder.setTimestamp(readCurrentTime()).build());
}
now = System.currentTimeMillis();
}
serverCallStreamObserver.onCompleted();
System.exit(0);
}

private Timestamp readCurrentTime() {
long millis = System.currentTimeMillis();
return timestampBuilder.setSeconds(millis / 1000)
.setNanos((int) ((millis % 1000) * 1000000)).build();
}
}

The onNext method of the StreamObserver receives the response type value as defined in the Proto file; when you work with an Unary RPC, the method can be called at most once. The onCompleted method completes the stream observation which means you shouldn’t call the onNext method as it will result in an error.

  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.16)

2023-11-05 19:10:39.459 INFO 63796 --- [ main] c.p.g.s.GrpcServerSpringBootApplication : Starting GrpcServerSpringBootApplication using Java 17.0.8
2023-11-05 19:10:39.461 INFO 63796 --- [ main] c.p.g.s.GrpcServerSpringBootApplication : No active profile set, falling back to 1 default profile: "default"
2023-11-05 19:10:39.945 INFO 63796 --- [ main] g.s.a.GrpcServerFactoryAutoConfiguration : Detected grpc-netty-shaded: Creating ShadedNettyGrpcServerFactory
2023-11-05 19:10:40.140 INFO 63796 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: TimeReader, bean: timeReaderGrpcService, class: com.pgse.grpc.server.service.TimeReaderGrpcService
2023-11-05 19:10:40.140 INFO 63796 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl
2023-11-05 19:10:40.140 INFO 63796 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService
2023-11-05 19:10:40.251 INFO 63796 --- [ main] n.d.b.g.s.s.GrpcServerLifecycle : gRPC Server started, listening on address: *, port: 9090
2023-11-05 19:10:40.261 INFO 63796 --- [ main] c.p.g.s.GrpcServerSpringBootApplication : Started GrpcServerSpringBootApplication in 1.128 seconds (JVM running for 1.685)

When the Spring Boot gRPC Server application starts you see that by default the server listens on the 9090 port. You can also see that the gRPC service gets registered.

grpcurl --plaintext localhost:9090 list 
grpcurl --plaintext localhost:9090 list TimeReader
grpcurl --plaintext -d '{"streaming_time_duration": "10"}' localhost:9090 TimeReader/readTime

You can quickly test the gRPC service with a grpcurl Go command line tool.

  • The first command lists all the available services.
  • The second command list all the available methods for a given service.
  • The third command calls the chosen method with the required arguments.

Useful links

gRPC Server Spring Boot Starter: https://github.com/yidongnan/grpc-spring-boot-starter#grpc-server

gRPC Performance Best Practices: https://grpc.io/docs/guides/performance/

StreamObserver API: https://grpc.github.io/grpc-java/javadoc/io/grpc/stub/StreamObserver.html

grpcurl Go Command Line Tool: https://github.com/fullstorydev/grpcurl

The gRPC Server Implementation: https://github.com/acrolisnun/grpc-spring-boot

Spring Boot Client

Analogous to the gRPC server module, a client module adds a gRPC Spring Boot Starter from the net.devh Maven groupId, but targeting the gRPC client. The starter provides components for channel configuration, intercepting request/responses before and after they are handled by channels, and stub transformers.

...
<dependency>
<groupId>com.pgse.grpc</groupId>
<artifactId>grpc-proto</artifactId>
<version>${com.pgse.grpc-proto.version}</version>
</dependency>

<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>${net.devh.grpc-client-spring-boot-starter.version}</version>
</dependency>
...

The gRPC client runs in a docker container and connects to the gRPC server running in a docker container. A gRPC channel provides a connection to the gRPC server. It is an abstraction over HTTP/2 connection and should be reused when making multiple gRPC calls to the same endpoint. Multiple clients can be created from the same channel. Channels are thread safe.

The first thing you might want to do when configuring a gRPC client Spring Boot application is to configure the channels. In my project I create a simple channel configuration in the application.yaml. The address property is the static IP of the gRPC server. I don’t want to add TLS and the server supports HTTP/2 — therefore, the negotationType property is set to plaintext, which means that the data is send unencrypted and the server doesn’t have to be upgraded to HTTP/2.

grpc:
client:
time-reader-0:
address: 'static://host.docker.internal:9090'
negotiationType: plaintext

I create an application class which declares a TimeReader stub field annotated with a GrpcClient annotation parametrised with the time-reader-0 channel configuration name. The annotation creates, configures and injects an instance of a TimeReaderStub class. The GrpcClient annotation shouldn’t be used if you want the stub to be a Bean. That also means that you can’t use the Autowired or Inject annotations with the GrpcClient annotated stubs. To declare the stub as a Bean you have to use a GrpcClientBean annotation.

package com.pgse.grpc.client;

import com.google.protobuf.Timestamp;
import com.pgse.grpc.generated.TimeReaderGrpc;
import com.pgse.grpc.generated.TimeReaderRequest;
import com.pgse.grpc.generated.TimeReaderResponse;
import java.time.Instant;
import java.util.concurrent.CountDownLatch;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GrpcClientSpringBootApplication implements CommandLineRunner {

@Value("${streaming-time-duration}")
private int streamingTimeDuration;

private static final Logger LOG = LoggerFactory.getLogger(GrpcClientSpringBootApplication.class);
@GrpcClient("time-reader-0")
private TimeReaderGrpc.TimeReaderStub timeReaderStub;

public static void main(String[] args) {
SpringApplication.run(GrpcClientSpringBootApplication.class);
}

@Override
public void run(String... args) {
LOG.info("Streaming time duration has been set to: {}", streamingTimeDuration);
TimeReaderRequest timeReaderRequest = TimeReaderRequest.newBuilder()
.setStreamingTimeDuration(streamingTimeDuration).build();
CountDownLatch countDownLatch = new CountDownLatch(1);
TimeReaderStreamObserver timeReaderStreamObserver = new TimeReaderStreamObserver(countDownLatch);
timeReaderStub.withWaitForReady().readTime(timeReaderRequest, timeReaderStreamObserver);
try {
countDownLatch.await();
} catch (InterruptedException e) {
LOG.info("Streaming has been interrupted");
throw new RuntimeException(e);
}
}

private static class TimeReaderStreamObserver implements
io.grpc.stub.StreamObserver<com.pgse.grpc.generated.TimeReaderResponse> {

private final CountDownLatch countDownLatch;

public TimeReaderStreamObserver(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}

@Override
public void onNext(TimeReaderResponse timeReaderResponse) {
LOG.info("Server time reading: {}", toInstant(timeReaderResponse.getTimestamp()));
}

@Override
public void onError(Throwable throwable) {
LOG.info(throwable.getMessage());
LOG.info(throwable.getCause().getMessage());
}

@Override
public void onCompleted() {
LOG.info("Streaming has completed");
countDownLatch.countDown();
}

private Instant toInstant(Timestamp timestamp) {
long milliseconds = (timestamp.getSeconds() * 1000) + (timestamp.getNanos() / 1000000);
return Instant.ofEpochMilli(milliseconds);
}
}
}

In the run method, the TimeReaderStub fires the RPC with the request for 10 seconds of the server time readings. As an argument I pass the StreamObserver object which receives stream data and error information, and is notified when stream has successfully completed. By default, when the client can’t establish connection with the server it immediately exits with failure. I want the RPC to wait for the server to become available. That’s why I call the withWaitForReady method on the TimeReaderStub before calling the readTime method.

  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.16)

2023-11-09 19:50:19.994 INFO 1 --- [ main] c.p.g.c.GrpcClientSpringBootApplication : Starting GrpcClientSpringBootApplication using Java 17-ea on 88825eeb7061 with PID 1 (/app/classes started by root in /)
2023-11-09 19:50:20.000 INFO 1 --- [ main] c.p.g.c.GrpcClientSpringBootApplication : No active profile set, falling back to 1 default profile: "default"
2023-11-09 19:50:20.732 INFO 1 --- [ main] n.d.b.g.c.a.GrpcClientAutoConfiguration : Detected grpc-netty-shaded: Creating ShadedNettyChannelFactory + InProcessChannelFactory
2023-11-09 19:50:21.211 INFO 1 --- [ main] c.p.g.c.GrpcClientSpringBootApplication : Started GrpcClientSpringBootApplication in 1.857 seconds (JVM running for 2.417)
2023-11-09 19:50:22.856 INFO 1 --- [ault-executor-0] c.p.g.c.GrpcClientSpringBootApplication : Server time reading: 2023-11-09T19:50:22.801Z
2023-11-09 19:50:23.843 INFO 1 --- [ault-executor-0] c.p.g.c.GrpcClientSpringBootApplication : Server time reading: 2023-11-09T19:50:23.839Z
2023-11-09 19:50:24.846 INFO 1 --- [ault-executor-0] c.p.g.c.GrpcClientSpringBootApplication : Server time reading: 2023-11-09T19:50:24.842Z
2023-11-09 19:50:25.850 INFO 1 --- [ault-executor-0] c.p.g.c.GrpcClientSpringBootApplication : Server time reading: 2023-11-09T19:50:25.846Z
2023-11-09 19:50:26.850 INFO 1 --- [ault-executor-0] c.p.g.c.GrpcClientSpringBootApplication : Server time reading: 2023-11-09T19:50:26.847Z
...

When I start the gRPC client, the stub will wait until it can establish connection with the gRPC server. Once the connection is established, the clients sends the request and receives the server time readings every second for the 10 seconds.

Useful links

gRPC Client Spring Boot Starter: https://github.com/yidongnan/grpc-spring-boot-starter#grpc-server

gRPC Performance Best Practices: https://grpc.io/docs/guides/performance/

Wait for Ready Explanation: https://grpc.io/docs/guides/wait-for-ready/

The gRPC Client Implementation: https://github.com/acrolisnun/grpc-spring-boot

Summary

  • gRPC recognises 4 types of RPCs.
  • gRPC is a communication protocol over HTTP/2.
  • Consider gRPC for inner service streaming of lightweight messages.
  • Supported gRPC Spring Boot abstractions exist and are well documented.

If you got this far, please let me know what are your thoughts about gRPC. Thank you

--

--

No responses yet